Nguyên tắc RẮN và cấu trúc mã


150

Trong một cuộc phỏng vấn việc làm gần đây, tôi không thể trả lời một câu hỏi về RẮN - ngoài việc cung cấp ý nghĩa cơ bản của các nguyên tắc khác nhau. Nó thực sự làm phiền tôi. Tôi đã thực hiện một vài ngày đáng để đào bới xung quanh và vẫn chưa đưa ra một bản tóm tắt thỏa đáng.

Câu hỏi phỏng vấn là:

Nếu bạn đã xem xét một dự án .Net mà tôi đã nói với bạn tuân thủ nghiêm ngặt các nguyên tắc RẮN, bạn sẽ thấy gì về mặt cấu trúc của dự án và mã?

Tôi loanh quanh một chút, không thực sự trả lời câu hỏi, rồi ném bom ra ngoài.

Làm thế nào tôi có thể xử lý tốt hơn câu hỏi này?


3
Tôi đã tự hỏi những gì không rõ ràng trong trang wiki cho RẮN
BЈовић

Khối xây dựng trừu tượng mở rộng.
rwong

Bằng cách tuân theo các Nguyên tắc RẮN của Thiết kế hướng đối tượng, các lớp của bạn sẽ tự nhiên có xu hướng nhỏ, có nhiều yếu tố và dễ dàng được kiểm tra. Nguồn: docs.asp.net/en/latest/fundamentals/ từ
WhileTrueS ngủ

Câu trả lời:


188

S = Nguyên tắc trách nhiệm duy nhất

Vì vậy, tôi hy vọng sẽ thấy một cấu trúc thư mục / tệp được tổ chức tốt và phân cấp đối tượng. Mỗi lớp / phần chức năng nên được đặt tên là chức năng của nó rất rõ ràng và nó chỉ nên chứa logic để thực hiện nhiệm vụ đó.

Nếu bạn thấy các lớp người quản lý khổng lồ với hàng ngàn dòng mã, đó sẽ là dấu hiệu cho thấy trách nhiệm duy nhất không được tuân theo.

O = Nguyên tắc mở / đóng

Về cơ bản, đây là ý tưởng rằng chức năng mới nên được thêm vào thông qua các lớp mới có tác động tối thiểu đến / yêu cầu sửa đổi chức năng hiện có.

Tôi hy vọng sẽ thấy nhiều việc sử dụng kế thừa đối tượng, gõ phụ, giao diện và các lớp trừu tượng để tách biệt thiết kế của một chức năng khỏi triển khai thực tế, cho phép những người khác đi cùng và thực hiện các phiên bản khác cùng mà không ảnh hưởng đến nguyên.

Nguyên tắc thay thế L = Liskov

Điều này có liên quan đến khả năng coi các kiểu con là kiểu cha mẹ của chúng. Điều này xuất hiện trong C # nếu bạn đang thực hiện một hệ thống phân cấp đối tượng được kế thừa phù hợp.

Tôi hy vọng sẽ thấy mã xử lý các đối tượng phổ biến như kiểu cơ sở và phương thức gọi của chúng trên các lớp cơ sở / trừu tượng hơn là khởi tạo và làm việc trên chính các kiểu con.

I = Nguyên tắc phân chia giao diện

Điều này tương tự như SRP. Về cơ bản, bạn xác định các tập con nhỏ hơn các chức năng như giao diện và làm việc với những người để giữ cho hệ thống của bạn tách riêng (ví dụ như một FileManagercó thể có responsibilty duy nhất đối phó với File I / O, nhưng điều đó có thể thực hiện một IFileReaderIFileWritertrong đó có các định nghĩa phương pháp cụ thể cho việc đọc và viết các tập tin).

D = Nguyên tắc đảo ngược phụ thuộc.

Một lần nữa điều này liên quan đến việc giữ một hệ thống tách rời. Có lẽ bạn đang cảnh giác về việc sử dụng thư viện .NET Dependency Injection, đang được sử dụng trong giải pháp như Unityhoặc Ninjecthoặc hệ thống ServiceLocator như AutoFacServiceLocator.


36
Tôi đã thấy nhiều vi phạm LSP trong C #, mỗi khi ai đó quyết định loại phụ cụ thể của họ là chuyên biệt và do đó không cần phải thực hiện một phần của giao diện và chỉ cần ném một ngoại lệ vào phần đó thay vào đó ... Đây là một cách tiếp cận phổ biến để giải quyết vấn đề hiểu lầm về thiết kế và triển khai giao diện
Jimmy Hoffa

2
@JimmyHoffa Đó là một trong những lý do chính khiến tôi khăng khăng sử dụng Hợp đồng mã; trải qua quá trình suy nghĩ thiết kế các hợp đồng giúp rất nhiều người thoát khỏi thói quen xấu đó.
Andy

12
Tôi không thích "LSP đi ra khỏi hộp trong C #" và đánh đồng phương pháp thực hành tiêm chích phụ thuộc vào DIP.
Euphoric

3
+1 nhưng nghịch đảo phụ thuộc <> Tiêm phụ thuộc. Họ chơi tốt với nhau, nhưng nghịch đảo phụ thuộc không chỉ là tiêm phụ thuộc. Tham khảo: Dip trong tự nhiên
Marjan Venema

3
@Andy: những gì giúp ích cho các bài kiểm tra đơn vị được xác định trên các giao diện mà tất cả những người triển khai (bất kỳ lớp nào có thể / được khởi tạo) đều được kiểm tra.
Marjan Venema

17

Rất nhiều lớp học nhỏ và giao diện với sự phụ thuộc tiêm khắp nơi. Có lẽ trong một dự án lớn, bạn cũng sẽ sử dụng khung IoC để giúp bạn xây dựng và quản lý vòng đời của tất cả các đối tượng nhỏ đó. Xem https://stackoverflow.com/questions/21288/which-net-dependency-injection-frameworks-are-worth-looking-into

Lưu ý rằng một dự án .NET lớn mà NGHIÊM TÚC tuân theo các nguyên tắc RẮN không nhất thiết có nghĩa là một cơ sở mã tốt để làm việc với mọi người. Tùy thuộc vào người phỏng vấn, anh ấy / cô ấy có thể muốn bạn thể hiện rằng bạn hiểu RẮN có nghĩa là gì và / hoặc kiểm tra xem bạn tuân thủ các nguyên tắc thiết kế một cách giáo điều như thế nào.

Bạn thấy, để RẮN, bạn cần tuân theo:

S nguyên tắc trách nhiệm Ingle, vì vậy bạn sẽ có nhiều lớp học nhỏ mỗi trong số họ làm một điều duy nhất

O nguyên tắc bút kín, mà trong .NET thường thực hiện với dependency injection, mà cũng đòi hỏi tôi và D dưới đây ...

Nguyên tắc thay thế L iskov có lẽ không thể giải thích trong c # bằng một lớp lót. May mắn thay, có những câu hỏi khác giải quyết nó, ví dụ: https://stackoverflow.com/questions/4428725/can-you-explain-liskov-substlation-principl-with-a-good-c-sharp-example

Nguyên tắc phân chia I nterface hoạt động song song với nguyên tắc Mở-Đóng. Nếu theo nghĩa đen, điều đó có nghĩa là thích một số lượng lớn các giao diện rất nhỏ thay vì một vài giao diện "lớn"

Nguyên tắc đảo ngược nguyên tắc D các lớp cấp cao không nên phụ thuộc vào các lớp cấp thấp, cả hai nên phụ thuộc vào trừu tượng.


SRP không có nghĩa là "chỉ làm một việc."
Robert Harvey

13

Một số điều cơ bản mà tôi mong đợi sẽ thấy trong cơ sở mã của một cửa hàng đặc biệt RẮN trong công việc hàng ngày của họ:

  • Nhiều tệp mã nhỏ - với một lớp cho mỗi tệp là cách thực hành tốt nhất trong .NET và Nguyên tắc Trách nhiệm duy nhất khuyến khích các cấu trúc lớp mô-đun nhỏ, tôi hy vọng sẽ thấy rất nhiều tệp chứa một lớp nhỏ, tập trung.
  • Rất nhiều mẫu Bộ điều hợp và Kết hợp - Tôi mong đợi việc sử dụng nhiều mẫu Bộ điều hợp (một lớp triển khai một giao diện bằng cách "chuyển qua" chức năng của một giao diện khác) để hợp lý hóa việc cắm vào một phụ thuộc được phát triển cho một mục đích những nơi khác nhau mà chức năng của nó cũng cần thiết. Cập nhật đơn giản như thay thế bộ ghi chép bảng điều khiển bằng bộ ghi tệp sẽ vi phạm LSP / ISP / DIP nếu giao diện được cập nhật để lộ phương tiện chỉ định tên tệp sẽ sử dụng; thay vào đó, lớp logger tệp sẽ hiển thị các thành viên bổ sung, và sau đó một Adaptor sẽ làm cho trình ghi nhật ký tệp trông giống như một trình ghi nhật ký giao diện điều khiển bằng cách ẩn các công cụ mới, vì vậy chỉ có đối tượng chụp tất cả những điều này lại với nhau để biết sự khác biệt.

    Tương tự, khi một lớp cần thêm một phụ thuộc của giao diện tương tự như giao diện hiện có, để tránh thay đổi đối tượng (OCP), câu trả lời thông thường là triển khai mẫu Tổng hợp / Chiến lược (một lớp thực hiện giao diện phụ thuộc và tiêu thụ nhiều giao diện khác việc triển khai giao diện đó, với số lượng logic khác nhau cho phép lớp chuyển cuộc gọi đến một, một số hoặc tất cả các triển khai).

  • Rất nhiều giao diện và ABC - DIP nhất thiết phải có sự trừu tượng tồn tại và ISP khuyến khích chúng có phạm vi hẹp. Do đó, các giao diện và các lớp cơ sở trừu tượng là quy tắc và bạn sẽ cần rất nhiều trong số chúng để bao quát chức năng phụ thuộc được chia sẻ của cơ sở mã của bạn. Mặc dù RẮN nghiêm ngặt sẽ bắt buộc phải tiêm tất cả mọi thứ , nhưng rõ ràng bạn phải tạo ở đâu đó, và vì vậy nếu một hình thức GUI chỉ được tạo ra khi là con của một hình thức cha mẹ bằng cách thực hiện một số hành động đối với cha mẹ đã nói, tôi không có ý định làm mới hình thức con từ mã trực tiếp trong cha mẹ. Tôi chỉ thường làm cho mã đó là phương thức riêng của mình, vì vậy nếu hai hành động có cùng dạng đã từng mở cửa sổ, tôi chỉ cần gọi phương thức đó.
  • Nhiều dự án - Điểm của tất cả những điều này là giới hạn phạm vi thay đổi. Thay đổi bao gồm cần phải biên dịch lại (một bài tập tương đối nhỏ nữa, nhưng vẫn quan trọng trong nhiều hoạt động quan trọng của bộ xử lý và băng thông, như triển khai các bản cập nhật cho môi trường di động). Nếu một tệp trong một dự án phải được xây dựng lại, tất cả các tệp sẽ làm. Điều đó có nghĩa là nếu bạn đặt các giao diện trong cùng các thư viện với việc triển khai của chúng, bạn sẽ thiếu điểm; bạn sẽ phải biên dịch lại tất cả các cách sử dụng nếu bạn thay đổi cách triển khai giao diện vì bạn cũng sẽ tự biên dịch lại định nghĩa giao diện, yêu cầu các cách sử dụng phải trỏ đến một vị trí mới trong tệp nhị phân kết quả. Do đó, giữ cho các giao diện tách biệt với tập quán triển khai, trong khi phân tách thêm chúng theo khu vực sử dụng chung, là một thực tiễn tốt nhất điển hình.
  • Rất nhiều sự chú ý dành cho thuật ngữ "Gang of Four" - Các mẫu thiết kế được xác định trong cuốn sách Thiết kế mẫu năm 1994 nhấn mạnh đến thiết kế mã mô-đun, kích thước cắn mà SOLID tìm cách tạo ra. Chẳng hạn, Nguyên tắc đảo ngược phụ thuộc và Nguyên tắc mở / đóng, là trung tâm của hầu hết các mẫu được xác định trong cuốn sách đó. Do đó, tôi mong muốn một cửa hàng tuân thủ mạnh mẽ các nguyên tắc RẮN cũng nắm lấy thuật ngữ trong cuốn sách của Gang of Four và đặt tên các lớp theo chức năng của họ dọc theo các dòng đó, chẳng hạn như "AbcFactory", "XyzRepousing", "DefToXyzAd CHƯƠNG" "," A1Command ", v.v.
  • Kho lưu trữ chung - Để phù hợp với ISP, DIP và SRP như thường được hiểu, Kho lưu trữ gần như có mặt khắp nơi trong thiết kế RẮN, vì nó cho phép tiêu thụ mã để yêu cầu các lớp dữ liệu theo cách trừu tượng mà không cần kiến ​​thức cụ thể về cơ chế truy xuất / lưu giữ và nó đặt mã thực hiện điều này ở một nơi trái ngược với mẫu DAO (ví dụ, nếu bạn có một lớp dữ liệu Hóa đơn, bạn cũng có một InvoiceDAO tạo ra các đối tượng ngậm nước kiểu đó, v.v. tất cả các đối tượng / bảng dữ liệu trong cơ sở mã / lược đồ).
  • IoC Container - Tôi ngần ngại thêm cái này, vì tôi thực sự không sử dụng khung IoC để thực hiện phần lớn việc tiêm phụ thuộc của mình. Nó nhanh chóng trở thành mô hình chống đối tượng của Thiên Chúa khi ném mọi thứ vào thùng chứa, lắc nó lên và tuôn ra sự phụ thuộc ngậm nước theo chiều dọc mà bạn cần thông qua phương pháp nhà máy được tiêm. Âm thanh tuyệt vời, cho đến khi bạn nhận ra rằng cấu trúc trở nên khá nguyên khối, và dự án với thông tin đăng ký, nếu "thông thạo", bây giờ phải biết mọi thứ về mọi thứ trong giải pháp của bạn. Đó là rất nhiều lý do để thay đổi. Nếu nó không thông thạo (đăng ký bị ràng buộc muộn bằng cách sử dụng tệp cấu hình), thì một phần chính của chương trình của bạn dựa vào "chuỗi ma thuật", hoàn toàn khác có thể là sâu.

1
Tại sao các downvote?
KeithS

Tôi nghĩ rằng đây là một câu trả lời tốt. Thay vì giống với nhiều bài đăng trên blog về các thuật ngữ này, bạn đã liệt kê các ví dụ và giải thích để hiển thị cách sử dụng và giá trị của chúng
Crowie

10

Đánh lạc hướng họ với cuộc thảo luận của Jon Skeet về cách 'O' trong RẮN là "vô ích và kém hiểu biết" và khiến họ nói về "biến thể được bảo vệ" của Alistair Cockburn và "thiết kế để thừa kế, hoặc cấm đoán" của Josh Bloch.

Tóm tắt ngắn gọn về bài viết của Skeet (mặc dù tôi không khuyên bạn nên bỏ tên anh ta mà không đọc bài đăng trên blog gốc!):

  • Hầu hết mọi người không biết "nguyên tắc mở" và "đóng" trong "nguyên tắc đóng mở" nghĩa là gì, ngay cả khi họ nghĩ họ làm vậy.
  • Giải thích phổ biến bao gồm:
    • các mô-đun phải luôn được mở rộng thông qua kế thừa thực hiện, hoặc
    • mã nguồn của mô-đun gốc không bao giờ có thể thay đổi.
  • Ý định cơ bản của OCP và công thức ban đầu của Bertrand Meyer là ổn:
    • rằng các mô-đun nên có các giao diện được xác định rõ (không nhất thiết phải theo nghĩa kỹ thuật của 'giao diện') mà khách hàng của chúng có thể phụ thuộc, nhưng
    • có thể mở rộng những gì họ có thể làm mà không phá vỡ các giao diện đó.
  • Nhưng các từ "mở" và "đóng" chỉ gây nhầm lẫn vấn đề, ngay cả khi chúng làm cho một từ viết tắt dễ phát âm.

OP hỏi, "Làm thế nào tôi có thể xử lý tốt hơn câu hỏi này?" Là một kỹ sư cao cấp thực hiện một cuộc phỏng vấn, tôi sẽ vô cùng thích thú với một ứng viên có thể nói chuyện một cách thông minh về những ưu và nhược điểm của các phong cách thiết kế mã khác nhau so với người có thể liệt kê ra một danh sách các gạch đầu dòng.

Một câu trả lời hay khác sẽ là, "Chà, điều đó phụ thuộc vào mức độ họ hiểu nó. Nếu tất cả những gì họ biết là từ thông dụng RẮN, tôi sẽ lạm dụng quyền thừa kế, lạm dụng các khung tiêm phụ thuộc, một triệu giao diện nhỏ không có giao diện nào phản ánh từ vựng tên miền được sử dụng để giao tiếp với quản lý sản phẩm .... "


6

Có lẽ có một số cách mà điều này có thể được trả lời với lượng thời gian khác nhau. Tuy nhiên, tôi nghĩ rằng điều này phù hợp hơn với "Bạn có biết RẮN có nghĩa là gì không?" Vì vậy, trả lời câu hỏi này có lẽ chỉ cần đạt được điểm nhấn và giải thích nó theo một dự án.

Vì vậy, bạn mong đợi để xem như sau:

  • Các lớp có một Trách nhiệm duy nhất (ví dụ: lớp truy cập dữ liệu cho khách hàng sẽ chỉ nhận dữ liệu khách hàng từ cơ sở dữ liệu khách hàng).
  • Các lớp học dễ dàng được mở rộng mà không ảnh hưởng đến hành vi hiện có. Tôi không phải sửa đổi các thuộc tính hoặc các phương thức khác để thêm chức năng bổ sung.
  • Các lớp dẫn xuất có thể được thay thế cho các lớp cơ sở và các hàm sử dụng các lớp cơ sở đó không phải mở lớp cơ sở sang loại cụ thể hơn để xử lý chúng.
  • Giao diện nhỏ và dễ hiểu. Nếu một lớp sử dụng một giao diện, nó không cần phụ thuộc vào một số phương thức để hoàn thành một nhiệm vụ.
  • Mã được trừu tượng hóa đủ để việc thực hiện cấp cao không bị phụ thuộc cụ thể vào việc thực hiện cấp thấp cụ thể. Tôi có thể chuyển đổi việc thực hiện cấp thấp mà không ảnh hưởng đến mã cấp cao. Ví dụ, tôi có thể chuyển lớp truy cập dữ liệu SQL của mình sang lớp dựa trên dịch vụ Web mà không ảnh hưởng đến phần còn lại của ứng dụng.

4

Đây là một câu hỏi xuất sắc, mặc dù tôi nghĩ nó là một câu hỏi phỏng vấn khó.

Các nguyên tắc RẮN thực sự chi phối các lớp và giao diện và cách chúng liên quan với nhau.

Câu hỏi này thực sự là một câu hỏi có liên quan nhiều hơn đến các tệp và không nhất thiết là các lớp.

Một quan sát hoặc câu trả lời ngắn gọn mà tôi sẽ đưa ra là nhìn chung bạn sẽ thấy các tệp chỉ giữ một giao diện và thường thì quy ước là chúng bắt đầu bằng chữ I viết hoa. Ngoài ra, tôi sẽ đề cập rằng các tệp sẽ không có mã trùng lặp (đặc biệt là trong một mô-đun, ứng dụng hoặc thư viện) và mã đó sẽ được chia sẻ cẩn thận qua các ranh giới nhất định giữa các mô-đun, ứng dụng hoặc thư viện.

Robert Martin thảo luận về chủ đề này trong lĩnh vực C ++ trong Thiết kế các ứng dụng C ++ hướng đối tượng sử dụng phương pháp Booch (xem các phần về Sự gắn kết, Đóng cửa và Tái sử dụng) và trong Clean Code .


Các lập trình viên .NET IME thường tuân theo quy tắc "1 lớp trên mỗi tệp" và cũng phản ánh các cấu trúc thư mục / không gian tên; Visual Studio IDE khuyến khích cả hai thực tiễn và các plugin khác nhau như ReSharper có thể thực thi chúng. Vì vậy, tôi mong đợi sẽ thấy một cấu trúc dự án / tệp phản ánh cấu trúc lớp / giao diện.
KeithS
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.