Design Pattern in .NET - Nguyên tắc SOLID

07 Tháng Hai 2021

SOLID là tập hợp 5 nguyên tắc thiết kế trong lập trình hướng đối tượng nhắm tới mục tiêu giúp code bào trì dễ dàng hơn. Những nguyên tắc này được Robert C. Martin đưa ra vào những năm 2000 trên thực tế, chúng chỉ là lựa chọn của năm nguyên tắc thiết kế trong hàng tá nguyên tắc thiết kế được thể hiện trong sách và blog của Martin

Design Pattern in .NET - Nguyên tắc SOLID

5 nguyên tắc đó gồm có:

  • Single Responsibility Principle (SRP).
  • Open-Closed Principle (OCP)
  • Liskov Substitution Principle (LSP).
  • Interface Segregation Principle (ISP).
  • Dependency Inversion Principle (DIP)

Single Responsibility Principle

Giả sử rằng bạn có một đối tượng là journal dùng để ghi nhật ký. Lớp quản lý đối tượng này gồm các thuộc tính là Title và Number of entries. Đoạn code ví dụ cho việc khai báo lớp quản lý journal như sau:

Tiếp theo bạn có thể viết thêm các phương thức để thêm đối tượng vào entries. Bạn cũng có thể viết thêm phương thức để xóa entry khỏi danh sách theo đoạn code dưới đây:

Và bạn sẽ có kết quả về class journal có thể sử dụng như sau:

Thật hợp lý khi có phương thức này như một phần của lớp Journal vì thêm một entries là điều mà Journal thực sự cần phải có. Nhưng câu chuyện sẽ dừng lại ở đó?

Tiếp theo bạn tưởng tượng việc bạn muốn duy trì nhật ký bằng cách lưu nó vào tệp tin. Và giờ bạn phải thêm đoạn code này vào Journal class

Cách tiếp cận này cực kỳ có vấn đề. Trách nhiệm của Journal Class là lưu giữ các entries, không phải để ghi chúng vào tệp tin. Nếu bạn thêm chức năng liên tục vào Journal và lớp khác tương tự, bất kỳ thay đổi nào (giả sử bạn quyết định không ghi vào tập tin lưu trữ máy cá nhân mà cần đẩy lên cloud) bạn sẽ phải đồi lớp hiện tại và các lớp phụ thuộc khác.

Chúng ta nên tạm dừng ở đây và bạn có thể thấy: Một số kiến trúc dẫn đến bạn phải thực hiện rất nhiều thay đổi nhỏ trong nhiều lớp cho dù có liên quan hay không, điều này dẫn tới 1 dấu hiệu gì đó không đúng lắm. Bây giờ nó thực sự phụ thuộc vào tình huống: Nếu bạn đang đổi tên 1 symbol được sử dụng ở nhiều nơi, nhìn chung thì điều này vẫn ổn vì hiện nay ReSharper, Rider hoặc bất kỳ IDE nào bạn sử dụng sẽ cho phép bạn thực hiện tái cấu trúc và thay đổi hàng loạt ở nhiều nơi. Tuy nhiên khi bạn cần sửa lại hoàn toàn, đó có thể là 1 quá trình rất khó khăn.

Do đó, bạn có thể thấy việc lữu trữ là 1 thao tác riêng biệt, điều này thường được thể hiện tốt hơn trong một lớp riêng biệt. Trong trường hợp của ví dụ trên, chúng ta sẽ xây dựng 1 lớp riêng như sau:

Đây chính là ý nghĩa về Single Responsibility: Mỗi lớp chỉ có một trách nhiệm và do đó chỉ có một lý do để thay đổi. Journal class sẽ chỉ cần thay đổi nếu có điều gì đó cần được thực hiện liên quan đến việc lưu trữ các mục nhập trong bộ nhớ; ví dụ: bạn có thể muốn mỗi mục nhập được bắt đầu bằng dấu thời gian, vì vậy bạn sẽ thay đổi phương thức Add () để thực hiện chính xác điều đó. Mặt khác, nếu bạn muốn thay đổi cơ chế lưu trữ, điều này sẽ được thay đổi trong PersistenceManager.

Một ví dụ điển hình về anti-pattern1 vi phạm SRP được gọi là một God Object. God Object là một lớp lớn cố gắng xử lý càng nhiều chức năng nhất có thể, trở thành một khối quái dị. Nói một cách chính xác, là bạn cố xây dựng hệ thống với một lớp duy nhất, nhưng, tốt hơn là không, bạn sẽ kết thúc với một mớ hỗn độn không thể hiểu nổi. May mắn cho chúng ta, God Objects dễ dàng nhận ra bằng mắt thường hoặc tự động (chỉ cần đếm số method) các developer có trách nhiệm có thể được xác định nhanh chóng và trừng phạt thích đáng.

Open-Closed Principle

Giả sử chúng ta có một loạt sản phẩm (hoàn toàn giả định) trong một cơ sở dữ liệu. Mỗi sản phẩm có một màu sắc và kích thước và được xác định như sau:

Bây giờ, chúng ta muốn cung cấp các khả năng lọc nhất định cho một bộ sản phẩm nhất định. Chúng a tạo ra một lớp service ProductFilter. Để hỗ trợ lọc sản phẩm theo màu sắc, chúng ta thực hiện như sau:

Cách tiếp cận hiện tại của chúng ta để lọc các mục theo màu sắc đều tốt, tất nhiên nó có thể được đơn giản hóa rất nhiều với việc sử dụng LINQ. Vì vậy, code của chúng ta được đưa vào chạy trên môi trường production, nhưng thật không may, một thời gian sau, ông chủ yêu cầu chúng tathực hiện lọc theo kích thước. Vì vậy, chúng taquay trở lại ProductFilter.cs, thêm code bên dưới và biên dịch lại

Điều này có vẻ giống như sự trùng lặp hoàn toàn, phải không? Tại sao chúng ta không viết một phương thức chung sử dụng Generic (tức là <T>)? Chà, một lý do có thể là các hình thức lọc khác nhau có thể được thực hiện theo những cách khác nhau: Ví dụ: một số loại bản ghi có thể được lập chỉ mục và cần được tìm kiếm theo một cách cụ thể; một số kiểu dữ liệu có thể tìm kiếm được trên đơn vị xử lý Đồ họa (GPU) trong khi những loại khác thì không.

Hơn nữa, bạn có thể muốn hạn chế các tiêu chí mà người ta có thể lọc. Ví dụ: nếu bạn xem Amazon hoặc một cửa hàng trực tuyến tương tự, bạn chỉ được phép thực hiện lọc trên một bộ tiêu chí hữu hạn. Những tiêu chí đó có thể được Amazon thêm vào hoặc loại bỏ nếu họ nhận thấy rằng việc sắp xếp theo số lượng đánh giá ảnh hưởng đến lợi nhuận.

Được rồi, vậy là code của chúng tôi được lên môi trường chạy production, nhưng một lần nữa, ông chủ quay lại và nói với chúng ta rằng bây giờ cần phải tìm kiếm theo cả kích thước và màu sắc. Vậy chúng ta phải làm gì ngoài việc thêm các phương thức khác?

Điều chúng tôi cần từ kịch bản này, là thực thi Open-Closed Principle nói rằng một loại được mở để mở rộng, nhưng bị đóng cho sửa đổi. Nói cách khác, chúng ta muốn bộ lọc có thể mở rộng mà không cần phải sửa đổi nó (và biên dịch lại một cái gì đó đã hoạt động và có thể đã được chuyển đến khách hàng)

Làm thế nào chúng ta có thể đạt được nó? Trước hết, chúng ta tách (SRP!) Về mặt khái niệm quy trình lọc của chúng tathành hai phần: bộ lọc (một cấu trúc lấy tất cả các mục và chỉ trả về một số) và một đặc tả (một vị từ để áp dụng cho một phần tử dữ liệu).

Trong đoạn code trên T được hiểu là sử dụng Generic nó có thể là sản phẩm và nó cũng có thể là cái gì đó khác. Việc sử dụng Generic làm cho code chúng ta dễ dàng tái sử dụng hơn. Đoạn code trên đây chúng ta chỉ định nghĩa các interface và tiếp theo chúng ta sẽ thực hiện hàm Filter() lấy danh sách phẩm theo yêu cầu như sau:

Chúng ta tiếp tục triển khai interface cho ISpecification<T>

Dường như chúng ta đang viết khá nhiều code để làm cái gì đó có vẻ đơn giản, nhưng khoan bạn hãy nghĩ đến lợi ích. Ở đây phần khó chịu nhất là chỉ định tham số cho AndSpecification - hãy nhớ rằng không giống như thông số kỹ thuật về màu sắc và kích thuốc, bộ tổ hợp này không bị giới hạn về loại sản phẩm.

Hãy nhớ rằng, nhờ vào sức mạnh của C #, bạn có thể chỉ cần giới thiệu một toán tử & (quan trọng: lưu ý ký hiệu đơn và ở đây; && là sản phẩm phụ) cho hai đối tượng ISpecification <T>, do đó thực hiện quá trình lọc theo hai ( hoặc hơn) tiêu chí hơi đơn giản hơn. Vấn đề duy nhất là chúng ta cần phải thay đổi từ một interface sang một lớp abstract

Đương nhiên, bạn có thể nâng cao cách tiếp cận này bằng cách viết phương thức mở rộng trên tất cả các cặp thông số kỹ thuật có thể có:

Bây giờ, hãy tóm tắt lại OCP là gì và cách ví dụ cụ thể thực thi nó. Về cơ bản, OCP quan điểm rằng bạn không cần phải quay lại code mà bạn có đã được viết và thử nghiệm và thay đổi nó. Đó chính xác là những gì đang xảy ra đây! Chúng ta thực hiện ISpecification <T> và IFilter <T> và từ đó trở đi, tất cả việc chúng ta phải làm là triển khai một trong hai interface (mà không sửa đổi chính các interface) để triển khai cơ chế lọc mới. Đây là những gì có nghĩa là Open-Closed Principle (OCP)

Liskov Substitution Principle

 

 

 

Bình luận