CÁC NGUYÊN TẮC TRONG DESIGN PATTERN
What is Design Pattern?
Việc thiết kế chương trình hoặc phần mềm là một vấn đề thường xuyên phải thực hiện đối với các kỹ sư phần mềm. Tuy nhiên, không phải lúc nào việc này cũng được thực hiện một cách hoàn hảo, đôi khi vẫn sẽ xảy ra sai sót, rủi ro vì sẽ rất dễ bỏ qua các yêu cầu hay những giải pháp dù chúng được lặp lại nhiều lần. Điều này khiến công việc thiết kế phần mềm trở nên phức tạp và khó kiểm soát.
Đòi hỏi một kỹ sư phần mềm phải nắm bắt và hiểu hết được toàn bộ các giải pháp cho mọi vấn đề khi thiết kế. Việc này gần như rất khó thực hiện vì công nghệ phần mềm luôn thay đổi, với mỗi phần mềm lại có những yêu cầu khác nhau đòi hỏi việc đưa ra giải pháp cũng khác nhau, hơn nữa lại có quá nhiều giải pháp cho một vấn đề để các kỹ sư lựa chọn, mà không phải kỹ sư nào cũng nắm bắt hết được tất cả các giải pháp.
Tuy nhiên, vào năm 1995, bộ tứ GoF (Gang of Four) gồm Erich Gamma, Richard Helm, Ralph Johnson, và John Vlissides đã nỗ lực đưa ra cách xây dựng phần mềm hiệu quả hơn, với quyển sách “Design Patterns: Elements of Reusable Object Oriented Software”.
Quyển sách này đã tổng hợp và giới thiệu được hầu như toàn bộ các kỹ thuật phần mềm trên thế giới thành khái niệm về các design patterns.
Các design patterns có thể coi là một giải pháp đóng gói cho một vấn đề thiết kế chung. Nó giúp các kỹ sư phần mềm giới hạn được các giải pháp thiết kế phần mềm thành các ý tưởng và mô hình đóng gói mà không phụ thuộc vào bất cứ công nghệ nào, khiến việc thiết kế phần mềm dễ dàng kiểm soát hơn.
Ngoài ra, việc đưa ra khái niệm design patterns còn giúp cho việc trao đổi ý tưởng và giải pháp thiết kế giữa các kỹ sư cũng dễ dàng hơn vì mọi người đều có chung ý niệm về các giải pháp theo pattern.
Patterns for Patterns
Nguyên tắc của 1 pattern được tóm gọn trong 5 điểm chính sau:
- “Separate out the things that change from those that stay the same.”
- “Program to an interface, not an implementation”
- “Prefer composition over inheritance”
- “Delegate, delegate, delegate.”
- “You ain’t gonna need it”
**“Separate out the things that change from those that stay the same.”*hãy tách riêng những thứ có thể thay đổi và những thứ bất biến ra
Khi thiết kế chương trình, hãy cố gắng tách những mã không thay đổi riêng với các đoạn mã khác. Vì trong chương trình, việc thay đổi mã sẽ dẫn đến những ảnh hưởng và nguy cơ rất lớn đến chương trình nếu đoạn mã đó được sử dụng và ảnh hưởng ở nhiều nơi trong chương trình. Cách tốt nhất là tách những đoạn mã không đổi riêng; và những đoạn mã có thể thay đổi thì lời khuyên ở đây là nên thực hiện thay đổi có phạm vi ảnh hưởng cục bộ, tức là không ảnh hưởng tới các mã ở khu vực khác.
Việc tuân thủ nguyên tắc này giúp code của chúng ta tránh được việc lặp lại code (DRY – Don’t repeat yourself) và cải thiện khả năng bảo trì.
“Program to an interface, not an implementation”
Tức là khi tạo một class cho một đối tượng cụ thể, chúng ta không nên implementation ngay cho đối tượng đó mà hãy tạo một class cho đối tượng chung nhất đại diện cho đối tượng ta muốn implement (thường sẽ là class interface đối với các ngôn ngữ khác, trừ Ruby, vì Ruby không có interface).
Program to an interface, not an implementation
Ví dụ: khi muốn tạo class cho đối tượng ô tô Car, chúng ta không nên cài đặt ngay các thuộc tính và phương thức cho class Car, mà hay nghĩ tới việc cài đặt cho class Vehicle trước, sau đó cài đặt class Car kế thừa lại class Vehicle.
class Car
def drive
end
end
class Motor
def drive
end
end
class Plane
def fly
end
end
class Vehicle
def travel
end
end
class Car < Vehicle
end
class Motor < Vehicle
end
class Plane < Vehicle
end
“Prefer composition over inheritance”
Nếu đã lập trình OPP, chắc hẳn ai cũng thấy được sức mạnh của Kế thừa Inheritance. Nó thực sự rất tuyệt vời, và đôi khi là giải pháp cho hầu hết các vấn đề trong lập trình. Tuy nhiên không nên lúc nào cũng sử dụng Inheritance, vì nó sẽ gây cho chúng ta nhiều rắc rối khi thiết kế.
Chẳng hạn, với ví dụ ở trên, ta đã có class Car kế thừa class Vehicle, giờ ta muốn thêm phương thức mới dành cho loại có động cơ, thì ở đây ta có thể tách riêng 2 loại Vehicle gồm loại có động cơ engine và không có động cơ not_engine. Để làm việc này với Inheritance, ta phải tạo thêm 2 class VehicleEngine và class VehicleNotEngine, đồng thời sửa lại và giới hạn Vehicle chỉ gồm các phương thức method cơ bản chung nhất giữa loại có động cơ và không động cơ. Việc này gây ra xáo trộn và sẽ ảnh hưởng tới các method được các SubClass kế thừa từ trước, nếu không cẩn thận có thể sẽ gây ra lỗi.
class Vehicle
def start_engine
end
def stop_engine
end
end
class Car < Vehicle
end
class Bike < Vehicle
end
class Vehicle
def start_engine
end
def stop_engine
end
end
class Bike < VehicleNoEngine
end
class Car < VehicleEngine
end
class VehicleEngine < Vehicle
def start_engine
end
def stop_engine
end
end
class VehicleNoEngine < Vehicle
end
Vậy làm sao để giải quyết yêu cầu này mà không dùng tới Inheritance, câu trả lời ở đây là hãy sử dụng Composition. Composition là cách chúng ta gom tất cả các phương thức cần tạo vào một đối tượng riêng được cài đặt bởi một class mới, không phải SubClass, sau đó thực hiện tham chiếu các đối tượng của ta tới các đối tượng mới tạo.
Cụ thể, thay vì tạo 2 class VehicleEngine và class VehicleNotEngine, ta chỉ tạo một class mới chứa tất cả các phương thức liên quan tới động cơ đặt tên là Engine. Và class Car sẽ gọi các phương thức của động cơ bằng cách thông qua đối tượng của class Engine.
class Engine
def start
end
def stop
end
end
class Car < Vehicle
def initialize
@engine = Engine.new
end
def sunday_drive
@engine.start
@engine.stop
end
end
Có thể thấy ngay, giải pháp này thực sự hiệu quả và đem lại nhiều lợi ích:
- Engine code được tách riêng khỏi class Vehicle, vì thế nó có thể tái sử dụng một cách độc lập
- Hơn nữa, tách Engine cũng giúp đơn giản hóa class Vehicle.
- Tăng tính đóng gói
- Dễ dàng mở rộng thành nhiều loại Engine như hình
Prefer composition over inheritance
“Delegate, delegate, delegate.”
Composition chính là 1 cách có thể thay thế cho việc sử dụng Inheritance khá tuyệt vời. Nhưng khi kết hợp với Delegate sẽ tạo ra sức mạnh và sự linh hoạt hoàn hảo để thay thế Inheritance.
Delegation, dịch nghĩa là ủy nhiệm, ủy thác, phương pháp này đảm bảo các phương thức sử dụng từ các class khác luôn được cài đặt lại bởi chính class sử dụng mà không phụ thuộc vào class khác.
class Car < Vehicle
def initialize
@engine = Engine.new
end
def sunday_drive
start_engine
stop_engine
end
def start_engine
@engine.start
end
def stop_engine
@engine.start
end
end
“You ain’t gonna need it”
Trong thiết kế, việc có được một hệ thống được thiết kế tốt hoàn toàn là không có, các thiết kế luôn cần sự mềm dẻo, linh hoạt khi cần thay đổi yêu cầu và sửa lỗi. Vì vậy, các kỹ sư phần mềm luôn tìm cách thiết kế thêm các tính năng mở rộng, dự trù có thể sẽ phát triển sau này mà không có trong yêu cầu ở thời điểm hiện tại. Việc này rất tốt tuy nhiên nó có thực sự cần thiết không?
Nguyên tắc này khuyên bạn chỉ nên tập trung vào những điều mà bạn cần ngay bây giờ, những vấn đề không chắc chắn là cần sử dụng ngay thì không nên thực hiện mà hãy chờ đến lúc bạn thực sự cần nó. Vì những vấn đề mở rộng đó chưa chắc sau này đã được sử dụng. Thay vì mất thời gian nghĩ tới nó, hãy tập trung vào những yêu cầu bạn chắc chắn cần ngay bây giờ cho hệ thống.
14 Design Patterns phổ biến
- Template Method
- Strategy
- Observer
- Composite
- Iterator
- Command
- Adapter
- Proxy
- Decorator
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Interpreter
Design Pattern trong Ruby
Ruby có cách riêng của mình để làm việc, một cách làm thay đổi cách chúng ta tiếp cận nhiều vấn đề lập trình, bao gồm cả các vấn đề được giải quyết bởi các design patterns GoF cơ bản. Thật ngạc nhiên là sự kết hợp của Ruby và các design pattern GoF cơ bản không đem lại sự thay đổi qúa khác biệt hay bước ngoặt mới.
Trong thực tế, 3 pattern mới đã được tập trung vào cùng với sự phổ biến của Ruby:
- Internal Domain-Specific Language (DSL)
- Meta-programming
- Convention Not Configuration
|