SOLID-notes

14
SOLID: Notes Ngô Nguyễn Chính Hà Nội 2013 1. Nguyên lí Thiết kế là gì? Nguyên lí Thiết kế (Principles of Design) là tập hợp các chỉ dẫn giúp chúng ta có thể tránh được các Bad Design. Có 3 đặc tính quan trọng của một Bad Design: o Rigidity (Tính cứng nhắc): Khó thay đổi, bởi vì mọi thay đổi sẽ ảnh hưởng lớn tới các bộ phận khác trong hệ thống o Fragility (Tính dễ gẫy): Khi thực hiện một thay đổi nào đó, hệ thống có thể bị phá vỡ o Immobility (Tính bất động): Khả năng tái sử dụng thấp (khó tái sử dụng) trong các ứng dụng khác 2. Một số Nguyên lí Thiết kế chính Open Close Principle (OCP) o Đặt vấn đề: Thiết kế ứng dụng và cài đặt chương trình nên quan tâm tới những thay đổi thường xuyên được thực hiện trong suốt quá trình phát triển và bảo trì hệ thống. Thông thường, có nhiều thay đổi xảy ra khi chúng ta thêm một feature mới vào ứng dụng. Khi đó, việc thay đổi/sửa mã nguồn hiện tại nên được giảm tối thiểu. Lí do là, các thay đổi/sửa mã nguồn hiện tại này có thể ảnh hưởng đến chức năng đã tồn tại theo cách không mong muốn. OCP nói rằng, Design hoặc Coding nên được thực hiện theo cách mà chức năng mới cần được bổ sung với những thay đổi tối thiểu trong code hiện tại. Design nên được thực hiện theo cách cho phép thêm chức năng mới giống như class mới. o Mục đích: Các thực thể (class, module và chức năng) nên Mở cho mở rộng và Đóng cho sửa/thay đổi. o Thí dụ: (1) Sau đây là ví dụ vi phạm nguyên tắc OCP. Thí dụ này thực hiện một GraphicEditor, xử lí vẽ các hình khác nhau. Nó rõ ràng là không theo nguyên tắc OCP khi GraphicEditor phải sửa cho mỗi lần shape mới được thêm vào. Có một số nhược điểm: o Khi mỗi shape mới được thêm vào, unit test của GraphicEditor cần được làm lại. o Khi mỗi type của shape được thêm vào, thời gian để thêm nó sẽ tăng lên do developer sẽ mất thêm thời gian để hiểu logic của GraphicEditor.

Transcript of SOLID-notes

Page 1: SOLID-notes

SOLID: Notes

Ngô Nguyễn Chính

Hà Nội 2013

1. Nguyên lí Thiết kế là gì?

Nguyên lí Thiết kế (Principles of Design) là tập hợp các chỉ dẫn giúp chúng ta có thể tránh được các Bad

Design.

Có 3 đặc tính quan trọng của một Bad Design:

o Rigidity (Tính cứng nhắc): Khó thay đổi, bởi vì mọi thay đổi sẽ ảnh hưởng lớn tới các bộ phận khác

trong hệ thống

o Fragility (Tính dễ gẫy): Khi thực hiện một thay đổi nào đó, hệ thống có thể bị phá vỡ

o Immobility (Tính bất động): Khả năng tái sử dụng thấp (khó tái sử dụng) trong các ứng dụng khác

2. Một số Nguyên lí Thiết kế chính

Open Close Principle (OCP)

o Đặt vấn đề:

Thiết kế ứng dụng và cài đặt chương trình nên quan tâm tới những thay đổi thường xuyên

được thực hiện trong suốt quá trình phát triển và bảo trì hệ thống.

Thông thường, có nhiều thay đổi xảy ra khi chúng ta thêm một feature mới vào ứng dụng. Khi đó,

việc thay đổi/sửa mã nguồn hiện tại nên được giảm tối thiểu. Lí do là, các thay đổi/sửa mã

nguồn hiện tại này có thể ảnh hưởng đến chức năng đã tồn tại theo cách không mong muốn.

OCP nói rằng, Design hoặc Coding nên được thực hiện theo cách mà chức năng mới cần được

bổ sung với những thay đổi tối thiểu trong code hiện tại. Design nên được thực hiện theo

cách cho phép thêm chức năng mới giống như class mới.

o Mục đích:

Các thực thể (class, module và chức năng) nên Mở cho mở rộng và Đóng cho sửa/thay đổi.

o Thí dụ:

(1) Sau đây là ví dụ vi phạm nguyên tắc OCP.

Thí dụ này thực hiện một GraphicEditor, xử lí vẽ các hình khác nhau. Nó rõ ràng là không theo

nguyên tắc OCP khi GraphicEditor phải sửa cho mỗi lần shape mới được thêm vào. Có một số

nhược điểm:

o Khi mỗi shape mới được thêm vào, unit test của GraphicEditor cần được làm lại.

o Khi mỗi type của shape được thêm vào, thời gian để thêm nó sẽ tăng lên do developer

sẽ mất thêm thời gian để hiểu logic của GraphicEditor.

Page 2: SOLID-notes

o Thêm shape mới có thể ảnh hưởng tới chức năng hiện có 1 cách không mong muốn,

ngay cả khi shape mới thêm vào hoạt động hoàn hảo.

Page 3: SOLID-notes

(2) Dưới đây là 1 thí dụ tuân thủ theo nguyên tắc OCP.

Trong thiết kế mới này, chúng ta bổ sung phương thức trừu tượng draw() trong GraphicEditor

cho vẽ các đối tượng, trong khi đó, chuyển các implement vào các class cụ thể.

Sử dụng OCP, tránh được các vấn đề trong design trước đó, vì GraphicEditor không thay đổi khi

class mới được thêm vào.

o Không yêu cầu Unit Test cho GraphicEditor

o Không cần hiểu source code của GraphicEditor

o Khi code cho vẽ hình được chuyển tới các class shape cụ thể, nó giảm được các risk

ảnh hưởng tới chức năng cũ khi chức năng mới được thêm

Page 4: SOLID-notes

o Tóm lại:

Nguyên lí này nên được áp dụng đối với các phần mà có nhiều khả năng sẽ thay đổi trong quá

trình phát triển.

Có nhiều mẫu thiết kế giúp chúng ta mở rộng code mà không thay đổi nó. Ví dụ, mẫu Decorator

giúp chúng ta tuân theo nguyên tắc OCP.

Ngoài ra, mẫu Factory hoặc Observer có thể được sử dụng để thiết kế 1 ứng dụng dễ dàng mở

rộng với ít thay đổi nhất trong code đã có

Dependency Inversion Principle

o Đặt vấn đề:

Trong 1 ứng dụng/chương trình, chúng ta có các class ở mức thấp “Low Level” – là các class

thực hiện các hoạt động cơ bản và primary; và các class ở mức cao “High Level” – là các class

bao hàm các logic phức tạp và dựa vào các class mức thấp.

Thông thường, chúng ta sẽ viết các class mức thấp, và sử dụng class mức thấp này để viết

các class mức cao phức tạp. Việc này dường như là cách làm logic. Nhưng đây không phải là

1 thiết kế linh hoạt.

Điều gì xảy ra nếu chúng ta thay đổi class mức thấp?

Lấy 1 ví dụ cổ điển của 1 module Copy, module này đọc các kí tự từ Keyboard và ghi chúng tới

Printer. Class mức cao chứa logic là Copy class. Class mức thấp là KeyboardReader và

PrinterWriter.

Trong một Bad Design sử dụng trực tiếp class mức thấp. Trong trường hợp này, nếu chúng ta

muốn thay đổi thiết kể để output trực tiếp tới class mới FileWriter thì chúng ta phải thay đổi Copy

class.

Để tránh các vấn đề như vậy, chúng ta có thể đưa ra 1 tầng trừu tượng (abstraction layer)

giữa class mức cao và class mức thấp. Các module mức cao có chứa logic phức tạp không nên

phụ thuộc vào các module mức thấp, và các lớp trừu tượng mới không phải được tạo ra dựa vào

các module mức thấp. Các module mức thấp được tạo ra dựa vào các lớp trừu tượng.

Theo nguyên tắc này, cách thiết kế một cấu trúc lớp bắt đầu từ module mức cao tới module mức

thấp:

High Level Classes --> Abstraction Layer --> Low Level Classes

o Mục đích:

Module mức cao không nên phụ thuộc vào module mức thấp. Cả 2 nên phụ thuộc vào lớp

trừu tượng.

Trừu tượng không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào trừu tượng.

Page 5: SOLID-notes

o Thí dụ:

(1) Sau đây là ví dụ vi phạm nguyên tắc Dependency Inversion Principle.

Chúng ta có class Manager, một class mức cao và class mức thấp Worker. Chúng ta cần thêm

module mới tới ứng dụng của chúng ta bởi vì trong công ty có 1 số super worker mới. Chúng ta

tạo 1 class mới SuperWorker.

Hãy giả định rằng, class Manager là một class chứa các logic rất phức tạp. Và bây giờ, chúng ta

phải thay đổi để thêm SuperWorker mới. Có một số nhược điểm:

o Chúng ta phải thay đổi class Manager (nên nhớ nó là class phức tạp, sẽ kéo theo time

và effort)

o Một vài chức năng hiện tại từ Manager có thể bị ảnh hưởng

o Unit test nên được làm lại

Các vấn đề này sẽ mất nhiều thời gian để giải quyết. Bây giờ, nó sẽ rất đơn giản nếu được thiết kế

theo Dependency Inversion Principle. Điều này có nghĩa là, chúng ta thiết kế class Manager, một

interface IWorker và class Worker (implement của IWorker interface). Khi chúng ta thêm

SuperWorker, tất cả những cái chúng ta phải làm là implement IWorker interface cho nó.

(2) Dưới đây là ví dụ tuân thủ theo nguyên tắc Dependency Inversion Principle.

Trong thiết kế mới này, một class trừu tượng được thêm thông qua IWorker interface. Các vấn

đề code bên trên được giải quyết:

o Class Manager không thay đổi

Page 6: SOLID-notes

o Giảm risk ảnh hưởng tới chức năng cũ trong Manager class

o Không cần làm lại unit test cho class Manager

o Tóm lại:

Khi nguyên lí này được thực hiện có nghĩa là class mức cao không làm việc trực tiếp với các

class mức thấp. Thay vào đó, sử dụng interface như tầng trừu tượng.

Trong trường hợp đó, việc tạo các các đối tượng mức thấp bên trong các đối tượng mức cao (nếu

cần thiết) không thể được thực hiện sử dụng toán tử new. Thay vào đó, một vài Creational design

pattern có thể được sử dụng, như Factory Method, Abstract Factory, Prototype.

Template Design Pattern là 1 thí dụ mà nguyên lí DIP được áp dụng.

Tất nhiên, việc tuân thủ nguyên lí này sẽ làm tăng effort và tăng độ phức tạp của code nhưng linh

hoạt hơn. Nguyên tắc này không thể được áp dụng với mỗi class và mỗi module.

Nếu một class chức năng mà có khả năng lớn là sẽ không thay đổi trong tương lai thì

chúng ta không cần phải áp dụng nguyên lí này.

Page 7: SOLID-notes

Interface Segregation Principle (ISP)

o Đặt vấn đề:

Khi thiết kế một ứng dụng, người ta thường quan tâm xem làm thế nào để trừu tượng hóa một

module, mỗi module có thể có 1 hoặc nhiều sub-modules.

Giả sử khi thiết kế một ứng dụng, chúng ta cài đặt một module trong ứng dụng đó bằng 1

class. Và chúng ta trừu tượng hóa nó bằng 1 interface. Mỗi sub-module trong module này có

thể được cài đặt bởi 1 hoặc nhiều methods trong class.

Bây giờ, chúng ta muốn mở rộng ứng dụng bằng cách thêm 1 module mới mà nó chỉ chứa

một vài sub-modules đang có của ứng dụng. Để làm điều này, chúng ta cần phải thêm 1 class

mới cho module mới này. Class mới này cần được cài đặt theo full interface. Và vì thế sẽ có

một số methods là dummy.

Một full interface như vậy được gọi là fat interface hay là populated interface.

Có populated interface như vậy là một giải pháp không tốt, nó có thể gây ra hành vi không phù

hợp trong hệ thống.

Nguyên tắc ISP nói rằng, clients không nên buộc phải cài đặt các interfaces mà chúng

không sử dụng. Thay vì sử dụng một fat interface, nên sử dụng nhiều interface nhỏ, mà mỗi

interface sử dụng/phục vụ cho một submodule.

o Mục đích:

Clients không nên buộc phải phục thuộc vào các interface mà chúng không được sử dụng.

o Thí dụ:

(1) Sau đây là ví dụ vi phạm nguyên tắc ISP.

Chúng ta có một class Manager được sử dụng để quản lí các worker.

Và chúng ta có 2 loại worker: Worker và SuperWorker. Cả 2 loại worker này làm việc và nghỉ ăn

trưa hàng ngày.

Nhưng bây giờ, có robot đến, chúng làm việc rất tốt nhưng không cần ăn trưa, do đó cũng không

cần nghỉ trưa. Một class Robot cần được implement của IWorker interface bởi vì robot làm việc.

Đây là lí do tại sao IWorker được coi là 1 polluted interface.

Nếu chúng ta giữ thiết kế hiện tại, class Robot mới buộc phải cài đặt phương thức ăn. Chúng ta có

thể viết một class dummy và có thể có tác dụng không mong muốn trong ứng dụng.

Theo nguyên tắc ISP, một thiết kế linh hoạt không có polluted interface. Trong trường hợp

của chúng ta, IWorker nên được chia thành 2 inteface khác nhau.

Page 8: SOLID-notes

(2) Sau đây là ví dụ tuân thủ theo nguyên tắc ISP.

Bằng việc phân chia IWorker thành 2 interface khác nhau, class mới Robot không bị bắt buộc thực

hiện phương thức ăn. Cũng như vậy, nếu chúng ta muốn thêm chức năng khác cho Robot giống

như Recharging, chúng ta tạo 1 interface khác IRechargeable với 1 phương thức recharge.

Page 9: SOLID-notes

o Tóm lại:

Nếu một design có một vài fat interface, và chúng không thể thay đổi. Chúng ta có một số client

class mà nó chỉ muốn sử dụng một vài methods từ các fat interface. Chúng ta có thể sử dụng

Adapter Pattern để giải quyết trong trường hợp này,

Giống như các nguyên tắc, ISP đòi hỏi thêm time và effort, tăng độ phức tạp của code. Nhưng nó

tạo ra 1 thiết kế linh hoạt. Nếu chúng ta áp dụng nó nhiều hơn mức cần thiết, code tạo ra sẽ chứa

nhiều interface với các phương thức duy nhất. Vì vậy, việc áp dụng nên dựa trên kinh nghiệm và

Page 10: SOLID-notes

xác định được các phần có thể mở rộng trong tương lai.

Single Responsibility Principle

o Đặt vấn đề:

Một responsibility được coi là một lí do (reason) để thay đổi.

Nguyên tắc này nói rằng, nếu chúng ta có 2 lí do để thay đổi cho 1 class, chúng ta phải phân

chia các chức năng đó thành 2 class. Mỗi class sẽ chỉ xử lí 1 responsibility và tương lai, nếu

chúng ta cần thực hiện 1 thay đổi, chúng ta sẽ làm nó trong class xử lí nó.

Khi chúng ta cần thực hiện một thay đổi trong class mà có nhiểu responsibility, thay đổi có thể ảnh

hưởng tới chức năng khác của class.

Nguyên tắc SRP là nguyên tắc đơn giản và trực quan, nhưng trong thực tế, đôi khi khó có thể làm

cho nó đúng.

o Mục đích:

Một class chỉ nên có duy nhất 1 lí do thay đổi.

o Thí dụ:

Giả sử chúng ta cần 1 đối tượng để giữ một email message, chúng ta sử dụng IEmail interface

như thí dụ bên dưới. Có vẻ như mọi thứ đều tốt.

Xem xét kĩ hơn, chúng ta có thể thấy IEmail interface và Email class đang có 2 responsibilities.

Một responsibility là việc sử dụng các class trong một số giao thức email như POP3 hoặc IMAP.

Nếu giao thức khác phải được hỗ trợ các đối tượng cần được tuần tự theo cách khác và code nên

được bổ sung để hỗ trợ giao thức mới.

Một responsibility khác là dành cho Content field. Ngay cả khi nội dung là 1 chuỗi có lẽ chúng ta

muốn trong tương lai để hỗ trợ HTML hoặc các định dạng khác.

Nếu chúng ta giữ duy nhất một class, mỗi thay đổi cho 1 responsibility có thể ảnh hưởng tới cái

khác:

o Thêm 1 giao thức mới sẽ tạo ra sự cần thiết phải thêm mã cho parsing và serializing

các nội dung đối với từng loại field

o Thêm 1 loại content mới (giống nhứ html) làm chúng ta thêm code cho mỗi giao thực

được thực hiện

Page 11: SOLID-notes

Chúng ta có thể tạo một interface mới và class được gọi Icontent và Content để phân chia các

responsibilities. Khi chỉ có 1 responsibility cho từng class đưa cho chúng ta 1 thiết kế linh hoạt hơn:

o Thêm 1 giao thức mới làm thay đổi duy nhật trong class Email

o Thêm 1 loại content mới được hỗ trợ làm thay đổi duy nhất trong class Content

o Tóm lại:

Nguyên tắc SRP đưa ra 1 cách tốt để xác định các class trong phase design của 1 ứng dụng và

nó nhắc chúng ta nghĩ đến tất cả các cách mà 1 class có thể phát triển. Cần phân định tốt “trách

nhiệm” được thực hiện chỉ khi hình ảnh đầy đủ về cách ứng dụng sẽ làm việc được hiểu.

Liskov's Substitution Principle(LSP)

o Đặt vấn đề:

Thông thường khi chúng ta thiết kế một module chương trình và chúng ta sẽ tạo ra một vài

class hierarchies. Sau đó chúng ta có thể sẽ mở rộng một vài classes bằng cách tạo ra một

số class dẫn xuất.

Chúng ta phải đảm bảo rằng, các class dẫn xuất mới này chỉ mở rộng mà không cần thay thế

Page 12: SOLID-notes

các chức năng (functionality) của các class cũ. Nếu không, các class mới có thể tạo ra hiệu

ứng không mong muốn khi chúng được sử dụng trong module hiện có của chương trình.

Theo nguyên tắc LSP nói rằng, nếu một module chương trình đang sử dụng một Base

class, thì reference tới Base class có thể được thay thế với một Class dẫn xuất mà không

ảnh hưởng đến chức năng của module chương trình.

o Mục đích:

Loại dẫn xuất phải có khả năng thay thế hoàn toàn cho các loại Base (Derived types must

be completely substitutable for their base types).

o Thí dụ:

Dưới đây là 1 thí dụ điển hình cho nguyên tắc LSP bị vi phạm.

Trong ví dụ, 2 class được sử dụng: Rectangle và Square.

Giả sử rằng đối tượng Rectangle được sử dụng ở đâu đó trong ứng dụng.

Chúng ta mở rộng ứng dụng và thêm class Square. Class Square được trả về bởi Factory pattern

dựa trên 1 số điều kiện và chúng ta không biết chính xác loại đối tượng nào sẽ được trả về. Nhưng

chúng ta biết nó là một Rectangle. Chúng ta nhận được đối tượng rectangle, thiết lập width tới 5

và height tới 10 và có được diện tích hình này. Đối với 1 rectangle với width 5 và height 10, diện

tích phải là 50. Thay vì kết quả là 100

Page 13: SOLID-notes
Page 14: SOLID-notes

o Tóm lại:

Nguyên tắc này chỉ là một phần mở rộng của Nguyên tắc Open Close và nó có nghĩa là chúng

ta phải chắc chắn rằng các class thừa kế mới đang mở rộng các class cơ sở mà không thay đổi

hành vi của họ.

3. Nguồn tham khảo

http://www.oodesign.com/

http://www.tutorialspoint.com/design_pattern/