Chào các bạn, Hôm nay mình quay lại với series các bài viết về Design Pattern trong C#. Vậy Design pattern là cái gì? Tại sao chúng ta lại áp dụng Design Pattern?
Design pattern là cái gì
Chắc hẳn các bạn sinh viên IT khi đi học đều biết đến khái niệm lập trình hướng đối tượng (OOP) và các tính chất của lập trình hướng đối tượng. Biết được những khái niệm và các vận dụng OPP thì có thể code ngon lành cành đào, app chạy vù vù. Nhưng trong môi trường thực tế một phần mềm tốt cần có một số yêu cầu khác như đặt tên đúng cách, format code gọn gàng, dễ đọc. Nhưng quan trọng nhất là khả năng bảo trì được ( maintainable ). Bất kì phần code nào mà không có khả năng bảo trì để thích nghi với những yêu cầu thay đổi chóng mặt từ khách hàng thì bạn sẽ bị sếp chửi tơi bời vì trễ deadline và tới một giai đoạn nào đó chỉ có đập đi làm lại. Từ đó ra đời khái niệm Design Pattern.
Design Pattern là giải pháp tối ưu hóa, tái sử dụng và mở rộng cho các vấn đề lập trình. Nó là khuôn mẫu được đúc kết từ quá trình phát triển phần mềm để giải quyết các vấn đề nan giải trong lập trình và được nhiều người chứng minh.
Tại sao phải sử dụng Design Pattern?
- Giúp bạn tái sử dụng code và mở rộng một cách dễ dàng. Nó là khuôn mẫu đã được kiểm chứng. Là kim chỉ nam giúp bạn giải quyết các vấn đề trong lập trình.
- Tăng tốc độ phát triển phần mềm vì khi áp dụng design pattern có thể tránh các lỗi lớn, dễ dàng nâng cấp và bảo trì.
- Giúp các thành viên team có thể hiểu code của nhau một cách nhanh chóng.
Để follow theo series bài viết về design pattern thì bạn phải có kiến thức về OOP. Cấu trúc series bài viết bao gồm: Tìm hiểu về nguyên tắc SOLID sau đó đi vào các loại Design Pattern. Có 3 nhóm chính như sau: Creational Pattern, Structural Pattern, Behavioral Pattern. Cấu trúc series bài viết được tóm tắt theo hình bên dưới.
SOLID Design Principles
Trước khi đi vào các loại Design Pattern thì chúng ta nên tìm hiểu về nguyên tắc SOLID trong lập trình. Đã có khá nhiều bài viết nói về nguyên tắc này nên mình không đi sâu vào khái niệm nó là gì mà thay vào đó mình sẽ làm ví dụ nhiều hơn để hiểu về nó. SOLID là viết tắt của 5 nguyên tắc sau:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion
Single Responsibility Principle (SRP)
Nguyên tắc nói về khả năng đơn nhiệm, nói một các dễ hiểu là mỗi class nên chỉ chịu trách nhiệm về một nhiệm vụ cụ thể nào đó mà thôi. Để hiểu rõ hơn thì chúng ta sẽ đi vào ví dụ:
ví dụ 1:
public class Journal { private readonly List<string> _entries = new List<string>(); private static int _count = 0; public int AddNew(string entry) { _entries.Add($"{++_count}: " + entry); return _count; } public void RemoveEntry(int id) { _entries.RemoveAt(id); } public override string ToString() { return string.Join(Environment.NewLine, _entries); } public void Save(Journal journal, string fileName) { File.WriteAllText(fileName, journal.ToString()); } public Journal Load(string file) { // some code } public Journal Load(Uri uri) { // some code } }
Ở ví dụ 1. Class Journal
có thuộc tính là các entry, nhiệm vụ của nó là lưu trữ, thêm và xóa các entry. Ngoài ra còn có các phương thức lưu lại Journal, load Journal từ file hoặc uri. Việc class có 2 trách nhiệm sẽ phát sinh các vấn đề như sau: Giả sử có 2 app sử dụng class Journal. App 1 làm nhiệm vụ lưu trữ các entry trong Journal. App 2 làm nhiệm vụ save và load Journal. Nếu một ngày có sự thay đổi trong cách lưu trữ Entry hoặc Journal thì class Journal đã bị thay đổi. Chúng ta bắt buộc phải build và test lại cả 2 ứng dụng. Class Journal này đã vi phạm nguyên tắc SRP trong SOLID. Để refactor đoạn code này, chúng ta sẽ chia ra 2 class như sau.
public class Journal { private readonly List<string> _entries = new List<string>(); private static int _count = 0; public int AddNew(string entry) { _entries.Add($"{++_count}: " + entry); return _count; } public void RemoveEntry(int id) { _entries.RemoveAt(id); } public override string ToString() { return string.Join(Environment.NewLine, _entries); } } public class Persistence { public void Save(Journal journal, string fileName) { File.WriteAllText(fileName, journal.ToString()); } public Journal Load(string file) { // some code } public Journal Load(Uri uri) { // some code } }
Có vẻ tốt hơn nhiều. Mỗi class làm đúng nhiệm vụ của nó.
Xác định trách nhiệm (responsibility)
Trong ngữ cảnh của SRP, để xác định trách nghiệm thì chúng ta dựa trên lý do thay đổi. Nếu chúng ta nghĩ một class có nhiều hơn một động cơ để thay đổi thì có nghĩa chúng có nhiều hơn một trách nhiệm. Rất khó để chúng ta thấy được điều đó. Mình sẽ đi tiếp đến ví dụ thứ 2:
public interface IModel { void Dial(string pno); void Hangup(); void Send(char c); char Recv(); }
Ở ví dụ thứ 2 chúng ta có 1 interface IModel
. Trong IModel
có 2 nhiệm vụ: nhiệm vụ đầu tiên là quản lý kết nối Dial()
và Hangup()
, nhiệm vụ thứ 2 là gửi data Send()
và Recv()
. Vì thế chúng ta nên tách chúng ra.
Nhưng trong ví dụ này, Ứng dụng không thay đổi vì lý do nó có 2 trách nhiệm. Việc quyết định tách ra là không cần thiết, sẽ làm ứng dụng thêm phức tạp thêm. Cũng có nhiều ý kiến trái chiều về SRP. Đây là nguyên tắc dễ hiểu nhất và cũng là nguyên tắc khó áp dụng nhất. Sau đây là một số ví dụ cần phải quyết định đến việc tách nó ra:
- Persistence
- Validation
- Notification
- Error Handling
- Logging
- Class Selection / Instantiation
- Formatting
- Parsing
- Mapping
Open/Closed Principle
Nguyên tắc này đơn giản chỉ là “những class nên mở để có thể mở rộng và đóng cho việc thay đổi“. Để hiểu rõ hơn thì mình sẽ đi thẳng vào ví dụ cho các bạn hình dung dễ hơn.
Mình có một kịch bản như thế này: Ứng dụng đang quản một danh sách các sản phẩm ( Product). Mỗi sản phẩm có các thuộc tính như Màu sắc ( Color ) và kích thức ( Size ) và giá tiền ( Price ). Để khách hàng dễ dàng tiếp cận sản phẩm thì ứng dụng yêu cầu một tính năng lọc sản phẩm theo màu sắc. Mình sẽ hiện thực lại đoạn code theo yêu cầu trên như sau:
public enum Color { Red, Green, Blue } public enum Size { Small, Medium, Large, Huge } public class Product { public string Name { get; set; } public Color Color { get; set; } public Size Size { get; set; } public decimal Price { get; set; } public Product(string name, Color color, Size size, decimal price) { Name = name; Color = color; Size = size; Price = price; } } public class ProductFilter { public IEnumerable<Product> FilterByColor(IEnumerable<Product> products, Color color) { foreach (var product in products) if (product.Color == color) yield return product; } }
Đoạn code trên filter ngon lành theo Color. Và rồi một ngày đẹp trời sếp có thêm yêu cầu filter theo Size. Theo cách mọi người hay làm là mở đoạn code trong ProductFilter
thêm một phương thức FilterBySize()
. Đoạn code được add vào class ProductFilter
như sau:
public IEnumerable<Product> FilterBySize(IEnumerable<Product> products, Size size) { foreach (var product in products) if (product.Size == size) yield return product; }
Sau khi thêm và chạy, test lại (đừng quên test lại FilterByColor()
), ProductFilter
vẫn hoạt động hoàn hảo. Rồi một ngày đẹp trời khác, sếp bảo: “Hey boy, Tôi muốn thêm filter theo giá tiền, cái này giống cái cũ, làm nhanh nhé”. rồi một ngày đẹp trời nữa “Thêm cho tôi filter theo Color và Size luôn nhé” . Mỗi ngày đẹp trời như thế là bạn phải mở ProductFilter
ra thêm vào và phải test lại để đảm bảo tất cả các phương thức vẫn hoạt động ok. Nhiều ngày đẹp trời như thế thì sẽ trở thành ác mộng cho bạn. Có một pattern mà bạn có thể áp dụng trong tình huống này đó là Specification pattern. Phải refactor ngay đi trước khi mọi thứ dần tồi tệ hơn. =)).
/* Đoạn code define Color, Size, Product */ public interface ISpecification<T> { bool IsSatisfiedBy(T t); } public interface IFilter<T> { IEnumerable<T> Filter(IEnumerable<T> items, ISpecification<T> spec); } public class ProductFilter : IFilter<Product> { public IEnumerable<Product> Filter(IEnumerable<Product> items, ISpecification<Product> spec) { foreach (var product in items) if (spec.IsSatisfiedBy(product)) yield return product; } } public class ColorSpecification : ISpecification<Product> { private readonly Color _color; public ColorSpecification(Color color) { _color = color; } public bool IsSatisfiedBy(Product t) { return _color == t.Color; } } public class SizeSpecification : ISpecification<Product> { private readonly Size _size; public SizeSpecification(Size size) { _size = size; } public bool IsSatisfiedBy(Product t) { return _size == t.Size; } }
Nếu muốn thêm filter theo giá tiền trong khoảng min đến max thì chỉ cần viết thêm 1 class kế thừa interface ISpecification
như sau:
public class PriceSpecification : ISpecification<Product> { private readonly decimal _minPrice; private readonly decimal _maxPrice; public PriceSpecification(decimal minPrice, decimal maxPrice) { _minPrice = minPrice; _maxPrice = maxPrice; } public bool IsSatisfiedBy(Product t) { return _minPrice <= t.Price && t.Price <= _maxPrice; } }
Cách dùng
var products = new List<Product> { new Product("House", Color.Blue, Size.Large, 150), new Product("House", Color.Red, Size.Large, 100), new Product("Tree", Color.Green, Size.Medium, 5), new Product("Building", Color.Red, Size.Huge, 500) }; Console.WriteLine("Product (old)"); PrintProduct(products); Console.WriteLine("=============================================="); Console.WriteLine("= Single condition filter ="); Console.WriteLine("=============================================="); var pf = new ProductFilter(); var redColorFilter = new ColorSpecification(Color.Red); var newProducts = pf.Filter(products, redColorFilter); Console.WriteLine("Product (new)"); PrintProduct(newProducts); Console.WriteLine("=============================================="); Console.WriteLine("= Multiple condition filter ="); Console.WriteLine("=============================================="); var listOfFilter = new List<ISpecification<Product>>() { new PriceSpecification(0, 150), new ColorSpecification(Color.Green) }; var priceOrColorFilter = new OrSpericification(listOfFilter); var priceOrColorFilterProducts = pf.Filter(products, priceOrColorFilter ); Console.WriteLine("Product (new)"); PrintProduct(andFilterProducts);
Chúng ta có thể viết lọc theo nhiều điều kiện AND hoặc OR nếu muốn. vẫn đảm bảo không Modify class cũ mà vẫn mở rộng được ứng dụng. Đó là nguyên tắc Open Closed Principle.
Lời kết
Ở bài viết này chúng ta đã tìm hiểu 2 nguyên tắc trong số 5 nguyên tắc của SOLID. Những nguyên tắc còn lại sẽ được khám ở những bài viết tiếp theo. Chúc các bạn áp dụng thành công tinh thần của 2 nguyên tắc này trong dự án của bạn.
Xem thêm bài viết tại: http://levinh.net/
Tài liệu tham khảo:
https://github.com/levinhtxbt/design-patterns/tree/master/DesignPatterns/SOLID
https://en.wikipedia.org/wiki/SOLID
Permalink
Mình không thấy class OrSpericification ??
Permalink
Hi @jiang_xin:disqus ,
Bạn có thể xem full source code ở đây : https://github.com/levinhtxbt/design-patterns/blob/master/DesignPatterns/SOLID/OpenClosedPrinciple.cs
Have a nice day 🙂