Mẫu trang trí
Thuộc phần Lập trình hướng đối tượng trong hành trình C# của Coddy. Bài học 58 trên 70.
Mẫu Decorator là một mẫu cấu trúc cho phép bạn thêm hành vi mới vào các đối tượng bằng cách bọc chúng trong những đối tượng đặc biệt gọi là decorator. Khác với kế thừa, vốn thêm hành vi tại thời điểm biên dịch, decorator thêm chức năng một cách linh hoạt khi chạy mà không sửa đổi lớp ban đầu.
Mẫu này hoạt động bằng cách để các decorator triển khai cùng một giao diện với đối tượng mà chúng bao bọc. Mỗi decorator giữ tham chiếu đến đối tượng được bao bọc và chuyển tiếp các lệnh gọi đến đối tượng đó, đồng thời thêm hành vi riêng trước hoặc sau:
public interface ICoffee
{
string GetDescription();
decimal GetCost();
}
public class SimpleCoffee : ICoffee
{
public string GetDescription() => "Coffee";
public decimal GetCost() => 2.00m;
}
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
public virtual string GetDescription() => _coffee.GetDescription();
public virtual decimal GetCost() => _coffee.GetCost();
}
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Milk";
public override decimal GetCost() => _coffee.GetCost() + 0.50m;
}Sức mạnh của decorator nằm ở khả năng xếp chồng. Bạn có thể bọc một đối tượng đã được trang trí bằng một decorator khác:
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new MilkDecorator(coffee); // Thêm sữa hai lần!
Console.WriteLine(coffee.GetDescription()); // Cà phê, sữa, sữa
Console.WriteLine(coffee.GetCost()); // 3.00Mẫu Decorator phù hợp khi bạn cần bổ sung trách nhiệm cho các đối tượng mà không tạo ra quá nhiều lớp con. Mỗi decorator tập trung vào một mối quan tâm duy nhất, và bạn kết hợp chúng khi cần để tạo ra hành vi chính xác như mong muốn.
Thử thách
DễHãy xây dựng một hệ thống đặt pizza bằng mẫu Decorator. Bạn sẽ tạo một chiếc pizza cơ bản, sau đó thêm các loại nhân vào một cách linh hoạt. Mỗi lớp nhân sẽ bao bọc lớp trước đó và bổ sung vào cả phần mô tả lẫn giá tiền.
Bạn sẽ tổ chức code trong ba tệp:
Pizza.cs: Định nghĩa giao diệnIPizzatrong không gian tênPizzaShopvới hai phương thức:GetDescription()trả về một chuỗi vàGetPrice()trả về một số thập phân. Sau đó, tạo lớpPlainPizzatriển khai giao diện này. Lớp này sẽ trả về"Pizza"làm phần mô tả và8.00mlàm giá cơ bản.ToppingDecorators.cs: Tạo lớp trừu tượngToppingDecoratortriển khaiIPizzavà giữ tham chiếu đến chiếc pizza được bao bọc (truyền vào hàm khởi tạo). Các phương thức ảo của lớp sẽ ủy quyền cho chiếc pizza được bao bọc. Sau đó, tạo ba lớp trang trí cụ thể:CheeseTopping- nối thêm", Cheese"vào phần mô tả và cộng thêm1.50mvào giáPepperoniTopping- nối thêm", Pepperoni"vào phần mô tả và cộng thêm2.00mvào giáMushroomTopping- nối thêm", Mushrooms"vào phần mô tả và cộng thêm1.25mvào giá
Program.cs: Kết hợp mọi thứ bằng cách bắt đầu với một chiếc pizza cơ bản rồi bao bọc nó bằng các lớp nhân dựa trên đầu vào. Mỗi lớp trang trí nhân sẽ bao bọc chiếc pizza trước đó, từng lớp một, để tạo nên đơn hàng cuối cùng.
Bạn sẽ nhận một đầu vào:
- Danh sách các loại nhân cần thêm, phân tách bằng dấu phẩy (ví dụ:
cheese,pepperoni,cheese)
Bắt đầu với một PlainPizza, sau đó bao bọc nó bằng lớp trang trí phù hợp cho từng loại nhân theo thứ tự xuất hiện. Các loại nhân hợp lệ là cheese, pepperoni và mushrooms. Sau khi thêm tất cả các loại nhân, in phần mô tả trên một dòng và tổng giá trên dòng tiếp theo.
Ví dụ, nếu đầu vào là cheese,pepperoni,mushrooms, đầu ra sẽ là:
Pizza, Cheese, Pepperoni, Mushrooms
12.75Lưu ý rằng bạn có thể thêm cùng một loại nhân nhiều lần! Nếu đầu vào là cheese,cheese, kết quả sẽ là:
Pizza, Cheese, Cheese
11.00Điều này cho thấy tính linh hoạt của mẫu decorator: mỗi lớp trang trí bao bọc chiếc pizza mà nó nhận được, dù đó là pizza cơ bản hay pizza đã được trang trí, cho phép bạn xếp chồng các hành vi một cách linh hoạt!
Tự làm thử
using System;
using PizzaShop;
class Program
{
public static void Main(string[] args)
{
// Đọc danh sách các loại nhân pizza, phân tách bằng dấu phẩy
string input = Console.ReadLine();
string[] toppings = input.Split(',');
// TODO: Bắt đầu với một PlainPizza
// TODO: Lặp qua từng loại nhân và bọc pizza
// bằng decorator phù hợp dựa trên tên loại nhân:
// - "cheese" -> CheeseTopping
// - "pepperoni" -> PepperoniTopping
// - "mushrooms" -> MushroomTopping
// TODO: In ra mô tả và giá cuối cùng
}
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Các khái niệm cơ bản về lập trình hướng đối tượng
Tệp bên ngoàiKhông gian tên và chỉ thịGiới thiệu về lớp và đối tượngTừ khóa thisPhương thức và tham sốTrường và thuộc tínhHàm khởi tạoKhởi tạo đối tượngÔn tập: Máy tính đơn giản4Kế thừa
Cú pháp kế thừa cơ bản (:)Từ khóa 'base'Từ khóa virtual và overrideLớp sealedLớp cơ sở 'object'Ôn tập: hệ phân cấp nhân viên7Tính năng nâng cao
Nạp chồng toán tửBộ lập chỉ mục (this[])Ghi đè ToString()Phương thức mở rộngÔn tập: Danh sách tùy chỉnh10Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếSingleton an toàn với luồngMẫu FactoryMẫu Observer (sự kiện)Mẫu Strategy2Thuộc tính và thành viên tĩnh
Thuộc tính tự độngThuộc tính chỉ đọc hoặc chỉ ghiTrường và phương thức tĩnhLớp tĩnhThành viên có thân biểu thức5Đa hình và interface
Đa hình lúc biên dịch và lúc chạyInterface và lớp trừu tượngNhiều interfaceInterface tường minhUpcasting và downcastingÔn tập: bộ tính toán hình học8Các khái niệm OOP nâng cao
Ưu tiên composition hơn kế thừaGenerics (lớp và phương thức)Delegate và sự kiệnAttribute và reflectionGiao diện IDisposable và câu lệnh usingKiến thức cơ bản về dependency injection11Các mẫu thiết kế phần 2
Mẫu lệnhMẫu bộ chuyển đổiMẫu trang tríMẫu phương thức khuôn mẫuMẫu trạng tháiMẫu tổ hợpTự luyện tập: Trình biên dịch C# online