Khi lập trình bạn đã bao giờ gặp các lỗi như: code khó đọc, code trùng lặp, import bị vòng lặp, lỗi cú pháp,... thì các bạn nên thử áp dụng các mẫu design patterns trong source code của mình.
Thường thì một lập trình viên sẽ chủ yếu viết code đáp ứng giải quyết được các nghiệp vụ và tối ưu code sau khi hoàn thành. Trong các trường hợp ấy đôi khi có sử dụng qua một số phương pháp tối ưu giống mẫu design patterns tuy nhiên lại không biết đó là gì và phương pháp đó thế nào giống như mình vậy. :D Nên hôm nay mình sẽ hỗ trợ các bạn tìm hiểu và triển khai các mẫu design patterns thường được sử dụng.
1. Định nghĩa design pattern
Design pattern là các mẫu thiết kế giải quyết các vấn đề theo từng loại và mục đích sử dụng trong quá trình phát triển phần mềm.
Sử dụng design pattern khi dự án cần maintain về lâu dài, giải quyết chung một vấn đề, tái sử dụng các function, làm chung giữa nhiều members trong team,...
2. Phân loại mẫu design patterns
Design patterns chia làm 3 nhóm chính:
1- Creational design patterns:
Cung cấp các cơ chế đối tượng khởi tạo, giúp tăng độ linh hoạt và tái sử dụng mã nguồn đã tồn tại. Một số mẫu: Factory method, Abstract factory, builder, prototype, singleton
2- Structural design patterns:
Trình bày cách lắp ráp các đối tượng và classes vào trong cấu trúc lớp hơn, trong khi vẫn giữ được cấu trúc flexible và hiệu quả. Một số mẫu: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
3- Behavioral design pattern:
Liên quan đến các thuật toán và việc phân công trách nhiệm giữa các đối tượng. Một số mẫu: command, iterator, mediator, memento, observer, state, strategy, template method, visitor
3. Một số mẫu design patterns
1- Factory Method (aka: Virtual Constructor)
Factory Method thuộc nhóm creational design pattern, cung cấp interface cho creating objects trong 1 superclass, cho phép các subclasses để thay đổi loại đối tượng vừa được khởi tạo.
Problem:
Ứng dụng quản lý logistics phiên bản đầu tiên chỉ quản lý vận chuyển bằng xe tải, nên phần lớn code sẽ nằm trong Truck class.
Sau một thời gian, ứng dụng của bạn trở nên phổ biến hơn. Vào một ngày công ty nhận một chục yêu cầu từ các công ty vận tải biển vào trong hệ thống của bạn. Hiện tại, hầu hết code của bạn được ghép nối với class Truck. Việc thêm Ships vào
ứng dụng sẽ yêu cầu thực hiện thay đổi đối với toàn bộ codebase. Bạn sẽ nhận ra phải thay đổi với các điều kiện chuyển đổi hành vi của ứng dụng tùy thuộc vào class vận chuyểnSolution:
Factory method pattern sẽ hỗ trợ thay thế các đối tượng construction và gọi tới các phương thức factory đặc biệt. Đối tượng trả về là thường sẽ refer đến các "product"
Trong ví dụ này mình sẽ khởi tạo 1 interface Transport có phương thức deliver() tuy nhiên deliver() trong Transport không được implement thay vào đó sẽ return về các class kế thừa từ interface trên. Cả Truck và Ship class nên được implement Transport interface, sẽ khai báo phương thức deliver. Tương ứng với từng class khơi tạo sẽ có các phương thức khác nhau: Truck sẽ vận chuyển trên đất liền trong thùng còn Ship sẽ vận chuyển trên biển và trong container.Pros:
Bạn tránh được sự kết hợp chặt chẽ giữa người tạo và các sản phẩm cụ thể.
Nguyên tắc trách nhiệm đơn lẻ. Bạn có thể di chuyển mã tạo sản phẩm vào một nơi trong chương trình, giúp mã dễ hỗ trợ hơn.
Nguyên tắc mở/đóng. Bạn có thể giới thiệu các loại sản phẩm mới vào chương trình mà không làm hỏng mã máy khách hiện có.Cons:
Mã có thể trở nên phức tạp hơn vì bạn cần giới thiệu nhiều lớp con mới để triển khai mẫu.
Kịch bản tốt nhất là khi bạn giới thiệu mẫu vào hệ thống phân cấp hiện có của các lớp người tạo.
2- Singleton
Singleton thuộc nhóm creational design pattern cho phép chắc chắn 1 class chỉ có 1 instance và cung các điểm kết nối đến instance.
Problem:
Singleton pattern giải quyết được 2 vấn đề cùng lúc, tuy nhiên vi phạm tính single responsibility principleĐảm bảo class chỉ có 1 khởi tạo: Cho các trường hợp Clients có thể không nhận ra họ đang làm việc với các đối tượng giống nhau trong cùng một thời điểm
Cung cấp điểm truy cập đến khởi tạo đó: Giống như biến toàn cục, Singleton pattern dẫn bạn kết nối đến đối tượng từ bất cứ đâu trong chương trình. Tuy nhiên, nó cũng bảo vệ khởi tạo từ việc ghi đè bởi các code khác.
Solution:
Triển khai Singleton có 2 bước thông thường:Tạo mặc định constructor private, tránh các đối tượng khác sử dụng toán tử new với Singleton class
Tạo một phương thức creation tĩnh hoạt động như là một constructor. Trong phương thức này sẽ gọi private constructor và lưu nó trong giá trị tĩnh. Các lệnh gọi vào phương thức nào đều trả về đối tượng đã lưu trong cached.
Pros:
Bạn có thể chắc chắn 1 class chỉ có 1 instance.
Bạn có điểm kết nối đến instance đó.
Đối tượng Singleton chỉ khởi tạo khi có yêu cầu lần đầu tiên truy cập.Cons:
Vi phạm tính single responsibility principle.
Singleton có thể làm xấu design và tăng sự phụ thuộc giữa các components.
Một số môi trường đa luồng cần xử lý Singleton đặc biệt.
Khó tạo các unit test vì test framework dựa vào kế thừa và không thể tạo các data ghi đè các phương thức tĩnh.
3- Adapter
Adapter thuộc structural design pattern cho phép các đối tượng và các interface không tương thích cộng tác với nhau
Problem:
Khi truyền tải dữ liệu từ nhà cung cấp data chứng khoán sang Core hệ thống máy để đưa vào thư viện phân tích thì truyền tải các kiểu dữ liệu khác nhau, tuy nhiên mình(Core hệ thống máy) không thể can thiệp chuyển đổi dữ liệu của các kênh khác (thư viện phân tích, nhà cung cấp)Solution:
Tại Core hệ thống máy mình sẽ tạo đối tượng đặc biệt là adapter để chuyển đổi các interface của một đối đối tượng thành 1 đối tượng khác mà các kênh (thư viện phân tích, nhà cung cấp) có thể hiểu được
Ví dụ:
XML -> XML -> JSON => XML -> XML và Adapter(convert XML->JSON) -> JSONPros:
Nguyên tắc trách nhiệm đơn lẻ. Bạn có thể di chuyển mã tạo sản phẩm vào một nơi trong chương trình, giúp mã dễ hỗ trợ hơn.
Nguyên tắc mở/đóng. Bạn có thể giới thiệu các loại sản phẩm mới vào chương trình mà không làm hỏng mã máy khách hiện có.Cons:
Độ phức tạp của code tăng. Đôi khi chỉ cần thay đổi lớp dịch vụ sao cho phù hợp với phần còn lại của mã của bạn là đơn giản hơn.
4- Decorator
Decorator thuộc structural design pattern cho phép thêm các hành vi mới của đối tượng bằng cách cho đối tượng vào trong đối tượng đặc biệt wrapper chứa các hành vi mới
Problem:
Giả sử có Notifier class có các class con: SMS Notifier, Facebook Notifier và Slack Notifier. Một số người muốn sử dụng kết hợp gửi thông báo giữa SMS và Facebook, một số lại muốn gửi Slack và SMS, một số gửi thông báo Facebook và Slack,... Bạn phải tìm cách khác về cấu trúc class thông báo để phù hợp hơnSolution:
Tạo một đối tượng wrapper liên kết với Notifier. Wrapper chứa các phương thức giống với Notifier và ủy thác cho Wrapper tất cả các yêu cầu nhận được. Tuy nhiên, Wrapper nhỏ hơn có thể thay thế kết quả trước hoặc sau khi nó được chuyển đến Wrapper chính
Theo ví dụ, Notifier sẽ có class BaseDecorator và trong phương thức construction sẽ chứa tham số Notifier(wrapper: notifier) và phương thức send(message). Class BaseDecorator sẽ có các class con khác như: SMS Decorator, Facebook Decorator, Slack Decorator với phương thức send(message) khác nhau tương ứngPros:
Có thể mở rộng hành vi của đối tượng mà không cần tạo mới một subclass.
Bạn có thể thêm hoặc bớt nhiệm vụ của đối tượng trong thời điểm đang hoạt động.
Bạn có thể kết hợp chung bởi wrapping một đối tượng vào trong nhiều decorators khác nhau.
Bạn có thể chia nhỏ một class lớn thành nhiều biến thể hành vi của các class nhỏ hơn.Cons:
Rất khó khi xóa 1 wrapper cụ thể từ một ngăn xếp wrappers.
Khó khi khởi tạo một decorator khi hành vi không phụ thuộc vào trật tự của ngăn xếp wrappers.
Khởi tạo ban đầu của các đối tượng trông rất xấu.
5- Observer
Observer thuộc nhóm behavioral design pattern cho phép bạn định nghĩa các cơ chế đăng ký thông báo cho nhiều đối tượng về các events xảy ra mà đối tượng đang quan sát.
Problem:
Ví dụ khách hàng có thể ghé cửa hàng mỗi ngày và kiểm tra sản phẩm mới ra mắt có hàng không. Tuy nhiên khi sản phẩm đang trong quá trình vận chuyển thì hầu hết lần ghé đều vô nghĩa. Mặt khác, cửa hàng có thể gửi email(spam) đến tất cả khách hàng đồng thời về 1 sản phẩm sắp ra mắt. Điều này sẽ giúp một số khách hàng không phải đi đến cửa hàng liên tục. Đồng thời, nó sẽ làm phiền những khách hàng khác không quan tâm đến sản phẩm mới.Solution:
Khởi tạo các trạng thái đối tượng pub-sub.
Observer pattern hỗ trợ bạn thêm các cơ chế đăng ký cho các pub để các sub đăng ký hoặc hủy đăng ký các luồng event từ pud đó.Pros:
Nguyên tắc mở/đóng. Bạn có thể giới thiệu các loại sản phẩm mới vào chương trình mà không làm hỏng mã máy khách hiện có.
Bạn có thể thành lập mối quan hệ giữa các đối tượng ngay lập tứcCons:
Người đăng ký(sub) nhận thông báo theo thứ tự ngẫu nhiên
6- Template Method
Template method thuộc nhóm behavioral design pattern được định nghĩa là bộ khung thuật toán của lớp cha tuy nhiên cho phép các lớp con ghi đè lên các bước thuật toán mà không thay đổi cấu trúc của nó.
Problem:
Giả sử khi bạn xử lý data nhiều file khác nhau(doc, csv, pdf) nhưng sẽ có các bước chung trong quá trình xử lý.Solution:
Template Method pattern giúp chia nhỏ thuật toán thành loạt các bước, chuyển đổi các bước thành các phương thức và đưa một loạt lệnh gọi phương thức vào trong 1 "template method". Các bước có thể là trừu tượng hoặc là định nghĩa mặc định. Như ví dụ trên ta sẽ tạo một class DataMiner trong đó có các phướng thức là các bước ví dụ như: 1,2,3,4,... thì sẽ có class PDFMiner có các phương thức(2,3), DocMiner(1,2,3), .... và có thể thay đổi khởi tạo các phương thức nhưng không thay đổi các đặc tính của class DataMiner.Pros:
Bạn có thể cho phép class con ghi đè chỉ một số phần nhất định của thuật toán lớn, khiến chúng ít bị ảnh hưởng bởi những thay đổi xảy ra với các phần khác của thuật toán.
Bạn có thể lấy code trùng lặp vào class cha để không phải khởi tạo trong các class con.Cons:
Một số class con có thể bị giới hạn bởi khung thuật toán.
Bạn có thể vi phạm nguyên tắc thay thế Liskov bằng cách ngăn chặn triển khai mặc định thông qua class con.
Các phương thức ban đầu khó bảo trì hơn vì có nhiều bước hơn.
7- Strategy
Strategy thuộc nhóm behavioral design pattern được định nghĩa là một gia đình của thuật toán, đẩy từng phương thức vào trong một class mở rộng, và làm các đối tượng có thể hoán đổi được cho nhau
Problem:
Giả sử bạn cần di chuyển ra sân bay thì sẽ có các chiến lược(strategy) khác nhau: Đi xe đạp (0 phí, 30 phút); Đi xe buýt(tốn 2 đồng, 20 phút); Đi xe ô tô(3 đồng, 10 phút)Solution:
Tạo một interface Strategy là bối cảnh(context) và có các lớp khác nhau tùy theo các chiến lược. Theo cách này, bối cảnh trở nên độc lập với các chiến lược cụ thể, do đó bạn có thể thêm các thuật toán mới hoặc sửa đổi các thuật toán hiện có mà không cần thay đổi code của bối cảnh hoặc các chiến lược khác.Pros:
Bạn có thể cô lập các chi tiết triển khai của một thuật toán khỏi mã sử dụng thuật toán đó.
Bạn có thể thay thế kế thừa bằng thành phần.
Nguyên tắc Mở/Đóng. Bạn có thể giới thiệu các chiến lược mới mà không cần phải thay đổi ngữ cảnh.Cons:
Nếu bạn chỉ có một vài thuật toán và chúng hiếm khi thay đổi, thì không có lý do thực sự nào để làm phức tạp chương trình bằng các lớp và interface mới đi kèm với mẫu.
Người dùng phải nhận thức được sự khác biệt giữa các chiến lược để có thể chọn một chiến lược phù hợp.
Rất nhiều ngôn ngữ lập trình hiện đại có hỗ trợ kiểu chức năng cho phép bạn triển khai các phiên bản khác nhau của một thuật toán bên trong một tập hợp các hàm ẩn danh. Sau đó, bạn có thể sử dụng các hàm này chính xác như cách bạn sử dụng các đối tượng chiến lược, nhưng không làm tăng số lượng dòng code của bạn bằng các lớp và giao diện bổ sung.
Happy reading and happy coding!
Nguồn tham khảo:
https://refactoring.guru/design-patterns/creational-patterns
https://lptech.asia/kien-thuc/design-pattern-la-gi-cac-loai-design-pattern-thong-dung
dive-into-design-patterns