Làm rõ nguyên tắc trách nhiệm duy nhất


64

Nguyên tắc trách nhiệm duy nhất nói rằng một lớp nên làm một và chỉ một việc. Một số trường hợp được cắt khá rõ ràng. Tuy nhiên, những người khác thì khó khăn vì những gì trông giống như "một thứ" khi được xem ở một mức độ trừu tượng nhất định có thể là nhiều thứ khi được xem ở mức thấp hơn. Tôi cũng sợ rằng nếu Nguyên tắc Trách nhiệm Đơn lẻ được tôn vinh ở các cấp thấp hơn, mã ravioli phân tách quá mức, trong đó nhiều dòng được dành để tạo ra các lớp nhỏ cho mọi thứ và xử lý thông tin xung quanh hơn là thực sự giải quyết vấn đề trong tay, có thể dẫn đến.

Làm thế nào bạn sẽ mô tả "một điều" có nghĩa là gì? Một số dấu hiệu cụ thể mà một lớp thực sự làm nhiều hơn "một điều" là gì?


6
+1 cho hơn "mã ravioli" hàng đầu. Ngay từ đầu trong sự nghiệp, tôi là một trong những người đã đưa nó đi quá xa. Không chỉ với các lớp, mà với mô đun hóa phương thức quá. Mã của tôi bị ảnh hưởng bởi hàng tấn phương pháp nhỏ đã làm một cái gì đó đơn giản, chỉ với mục đích phá vỡ một vấn đề thành các phần nhỏ có thể vừa trên màn hình mà không cần cuộn. Rõ ràng, điều này thường đi quá xa.
Bàn Bobby

Câu trả lời:


50

Tôi thực sự thích cách Robert C. Martin (chú Bob) khôi phục Nguyên tắc Trách nhiệm duy nhất (được liên kết với PDF) :

Không bao giờ nên có nhiều hơn một lý do để một lớp thay đổi

Nó khác biệt một cách tinh tế so với định nghĩa "chỉ nên làm một điều" truyền thống và tôi thích điều này bởi vì nó buộc bạn phải thay đổi cách bạn nghĩ về lớp học của mình. Thay vì suy nghĩ về "điều này có làm một việc không?", Thay vào đó bạn nghĩ về những gì có thể thay đổi và những thay đổi đó ảnh hưởng đến lớp học của bạn như thế nào. Vì vậy, ví dụ, nếu cơ sở dữ liệu thay đổi, lớp của bạn cần phải thay đổi? Còn nếu thiết bị đầu ra thay đổi (ví dụ như màn hình, hoặc thiết bị di động hoặc máy in) thì sao? Nếu lớp của bạn cần thay đổi vì những thay đổi từ nhiều hướng khác, thì đó là dấu hiệu cho thấy lớp của bạn có quá nhiều trách nhiệm.

Trong bài viết được liên kết, chú Bob kết luận:

SRP là một trong những nguyên tắc đơn giản nhất và là một trong những nguyên tắc khó nhất để có được quyền. Các trách nhiệm liên kết là điều mà chúng tôi làm một cách tự nhiên. Tìm kiếm và tách biệt các trách nhiệm đó với nhau là phần lớn những gì thiết kế phần mềm thực sự hướng tới.


2
Tôi thích cách anh ấy nói quá, có vẻ dễ kiểm tra hơn khi liên quan đến thay đổi hơn là "trách nhiệm" trừu tượng.
Matthieu M.

Đó thực sự là một cách tuyệt vời để đặt nó. Tôi thích điều đó. Lưu ý, tôi thường có xu hướng nghĩ về SRP khi áp dụng mạnh mẽ hơn nhiều vào các phương pháp. Đôi khi một lớp chỉ phải thực hiện hai điều (có thể là lớp đó là cầu nối hai miền), nhưng một phương thức hầu như không bao giờ được thực hiện nhiều hơn những gì bạn có thể mô tả ngắn gọn bằng chữ ký loại của nó.
CodexArcanum

1
Chỉ cho thấy điều này với Grad của tôi - đọc tuyệt vời và một lời nhắc nhở tốt cho bản thân mình.
Martijn Verburg

1
Ok, điều này rất có ý nghĩa khi bạn kết hợp nó với ý tưởng rằng mã không bị áp đảo chỉ nên lập kế hoạch cho sự thay đổi có thể xảy ra trong tương lai gần, không phải cho mọi thay đổi có thể. Sau đó, tôi sẽ nói lại điều này một chút vì "chỉ nên có một lý do để một lớp thay đổi có khả năng xảy ra trong tương lai gần". Điều này khuyến khích sự đơn giản trong các phần của thiết kế không có khả năng thay đổi và tách rời ở các phần có khả năng thay đổi.
dsimcha

18

Tôi tiếp tục tự hỏi SRP đang cố gắng giải quyết vấn đề gì? Khi nào SRP giúp tôi? Đây là những gì tôi nghĩ ra:

Bạn nên cấu trúc lại trách nhiệm / chức năng của một lớp khi:

1) Bạn đã sao chép chức năng (DRY)

2) Bạn thấy rằng mã của bạn cần một mức độ trừu tượng khác để giúp bạn hiểu về nó (KISS)

3) Bạn thấy rằng các phần chức năng được các chuyên gia tên miền của bạn hiểu là tách biệt với một thành phần khác (Ngôn ngữ Ubiquitous)

Bạn KHÔNG NÊN tái cấu trúc trách nhiệm ra khỏi một lớp khi:

1) Không có bất kỳ chức năng trùng lặp.

2) Các chức năng không có ý nghĩa bên ngoài bối cảnh của lớp học của bạn. Nói cách khác, lớp của bạn cung cấp một bối cảnh để dễ hiểu chức năng hơn.

3) Các chuyên gia tên miền của bạn không có khái niệm về trách nhiệm đó.

Tôi nhận thấy rằng nếu SRP được áp dụng rộng rãi, chúng ta trao đổi một loại phức tạp (cố gắng tạo ra đầu hoặc đuôi của một lớp với quá nhiều thứ đang diễn ra bên trong) với một loại khác (cố gắng giữ tất cả các cộng tác viên / cấp độ của trừu tượng hóa thẳng để tìm ra những gì các lớp này thực sự làm).

Khi nghi ngờ, hãy để nó ra! Bạn luôn có thể cấu trúc lại sau này, khi có một trường hợp rõ ràng cho nó.

Bạn nghĩ sao?


Mặc dù các hướng dẫn này có thể có hoặc không hữu ích, nhưng nó thực sự không liên quan gì đến SRP như được định nghĩa trong RẮN, phải không?
sara

Cảm ơn bạn. Tôi không thể tin được một số sự điên rồ liên quan đến cái gọi là các nguyên tắc RẮN, nơi chúng tạo ra mã rất đơn giản phức tạp hơn gấp trăm lần mà không có lý do chính đáng . Những điểm bạn đưa ra mô tả lý do thực tế để thực hiện SRP. Tôi nghĩ rằng các nguyên tắc bạn đưa ra ở trên nên trở thành từ viết tắt của riêng họ và ném "RẮN" ra, nó có hại hơn là có lợi. "Các phi hành gia kiến ​​trúc" thực sự, như bạn đã chỉ ra trong chủ đề dẫn tôi đến đây.
Nicholas Petersen

4

Tôi không biết nếu có một thang đo khách quan cho nó nhưng những gì sẽ cho đi điều này sẽ là các phương pháp - không quá nhiều trong số chúng mà là sự đa dạng của chức năng của chúng. Tôi đồng ý bạn có thể phân tách quá xa nhưng tôi sẽ không tuân theo bất kỳ quy tắc cứng và nhanh nào về nó.


4

Câu trả lời nằm trong định nghĩa

Những gì bạn xác định trách nhiệm là, cuối cùng cung cấp cho bạn ranh giới.

Thí dụ:

Tôi có một thành phần có trách nhiệm hiển thị hóa đơn -> trong trường hợp này, nếu tôi bắt đầu thêm bất cứ thứ gì nữa thì tôi sẽ phá vỡ nguyên tắc.

Mặt khác, nếu tôi nói trách nhiệm xử lý hóa đơn -> thêm nhiều chức năng nhỏ hơn (ví dụ như in Hóa đơn , cập nhật Hóa đơn ) thì tất cả đều nằm trong ranh giới đó.

Tuy nhiên, nếu mô-đun đó bắt đầu xử lý bất kỳ chức năng nào bên ngoài hóa đơn , thì nó sẽ nằm ngoài ranh giới đó.


Tôi đoán vấn đề chính ở đây là từ "xử lý". Nó quá chung chung để xác định một trách nhiệm. Tôi nghĩ sẽ tốt hơn nếu có một thành phần chịu trách nhiệm về hóa đơn in và một thành phần khác để cập nhật hóa đơn thay vì xử lý một thành phần duy nhất {in, cập nhật và - tại sao không? - hiển thị, bất cứ điều gì} hóa đơn .
Machado

1
OP về cơ bản là hỏi "Làm thế nào để bạn xác định trách nhiệm?" Vì vậy, khi bạn nói trách nhiệm là bất cứ điều gì bạn xác định nó có vẻ như nó chỉ lặp lại câu hỏi.
Despertar

2

Tôi luôn xem nó ở hai cấp độ:

  • Tôi chắc chắn rằng phương pháp của tôi chỉ làm một việc và làm tốt
  • Tôi thấy một lớp là một nhóm logic (OO) hợp lý của các phương thức đó thể hiện tốt một điều

Vì vậy, một cái gì đó giống như một đối tượng miền được gọi là Dog:

Doglà lớp học của tôi nhưng Chó có thể làm nhiều việc! Tôi có thể có các phương thức như walk(), run()bite(DotNetDeveloper spawnOfBill)(xin lỗi không thể cưỡng lại; p).

Nếu Dogtrở nên khó sử dụng thì tôi nghĩ về cách các nhóm phương thức đó có thể được mô hình hóa cùng nhau trong một lớp khác, chẳng hạn như một Movementlớp, có thể chứa các phương thức của tôi walk()run().

Không có quy tắc cứng và nhanh, thiết kế OO của bạn sẽ phát triển theo thời gian. Tôi cố gắng tìm kiếm một giao diện / API công khai rõ ràng cũng như các phương pháp đơn giản thực hiện tốt một việc và một điều.


Bite thực sự nên lấy một thể hiện của Object và một DotNetDeveloper nên là một lớp con của Person (thông thường, dù sao đi nữa!)
Alan Pearce

@Alan - Có - đã sửa lỗi đó cho bạn :-)
Martijn Verburg

1

Tôi nhìn vào nó nhiều hơn dọc theo dòng của một lớp chỉ nên đại diện cho một điều. Để chiếm đoạt @ dụ Karianna, tôi có tôi Doglớp, trong đó có phương pháp walk(), run()bark(). Tôi sẽ không để thêm phương pháp meaow(), squeak(), slither()hoặc fly()vì những người không phải những điều mà con chó làm. Chúng là những thứ mà các động vật khác làm, và những động vật khác sẽ có lớp riêng để đại diện cho chúng.

(BTW, nếu con chó của bạn không bay, sau đó bạn nên có lẽ dừng lại ném anh ta ra khỏi cửa sổ).


+1 cho "nếu con chó của bạn bay, thì có lẽ bạn nên ngừng ném nó ra khỏi cửa sổ". :)
Bàn Bobby

Ngoài câu hỏi về những gì lớp nên đại diện, một thể hiện đại diện cho cái gì? Nếu một người coi SeesFoodlà một đặc điểm của DogEyes, Barknhư một thứ gì đó được thực hiện bởi a DogVoice, và Eatnhư một thứ được thực hiện bởi a DogMouth, thì logic như thế if (dog.SeesFood) dog.Eat(); else dog.Bark();sẽ trở thành if (eyes.SeesFood) mouth.Eat(); else voice.Bark();, mất bất kỳ cảm giác nhận dạng nào mà mắt, miệng và giọng nói đều được kết nối với một quyền lợi duy nhất.
supercat

@supercat đó là một điểm công bằng, mặc dù bối cảnh rất quan trọng. Nếu mã bạn đề cập là trong Doglớp, thì nó có thể Dogliên quan. Nếu không, thì có lẽ bạn sẽ kết thúc với một cái gì đó myDog.Eyes.SeesFoodchứ không phải chỉ eyes.SeesFood. Một khả năng khác là Doghiển thị ISeegiao diện đòi hỏi thuộc Dog.Eyestính và SeesFoodphương thức.
JohnL

@JohnL: Nếu các cơ chế thực tế nhìn thấy được xử lý bởi đôi mắt của một con chó, trong về cơ bản giống như của một con mèo hay một con ngựa vằn, sau đó nó có thể làm cho tinh thần để có cơ chế xử lý bởi một Eyelớp, nhưng một con chó nên "nhìn thấy" sử dụng đôi mắt của nó, thay vì chỉ có đôi mắt có thể nhìn thấy. Một con chó không phải là mắt, nhưng nó cũng không đơn giản là một vật giữ mắt. Đó là một "thứ có thể [ít nhất là cố gắng] nhìn thấy", và nên được mô tả qua giao diện là như vậy. Ngay cả một con chó mù có thể được hỏi nếu nó nhìn thấy thức ăn; Nó sẽ không hữu ích lắm, vì con chó sẽ luôn nói "không", nhưng không có hại gì khi hỏi.
supercat

Sau đó, bạn sẽ sử dụng giao diện ISee như tôi mô tả trong nhận xét của mình.
JohnL

1

Một lớp nên làm một việc khi được xem ở mức độ trừu tượng của chính nó. Nó chắc chắn sẽ làm nhiều việc ở mức độ trừu tượng hơn. Đây là cách các lớp hoạt động để làm cho các chương trình dễ bảo trì hơn: chúng ẩn các chi tiết triển khai nếu bạn không cần kiểm tra chúng chặt chẽ.

Tôi sử dụng tên lớp như một bài kiểm tra cho việc này. Nếu tôi không thể đặt cho một lớp một tên mô tả khá ngắn hoặc nếu tên đó có một từ như "Và" trong đó, có lẽ tôi đã vi phạm Nguyên tắc Trách nhiệm duy nhất.

Theo kinh nghiệm của tôi, việc giữ nguyên tắc này ở cấp thấp sẽ dễ dàng hơn, nơi mọi thứ cụ thể hơn.


0

Đó là về việc có một rô-bốt độc đáo .

Mỗi lớp nên được nối lại bằng một tên vai trò. Trên thực tế, một vai trò là một (bộ) động từ được liên kết với một bối cảnh.

Ví dụ :

Tệp cung cấp quyền truy cập của tệp. Trình quản lý tệp quản lý các đối tượng tệp.

Tài nguyên giữ dữ liệu cho một tài nguyên từ Tệp. ResourceManager giữ và cung cấp tất cả Tài nguyên.

Ở đây bạn có thể thấy rằng một số động từ như "Manage" ngụ ý một tập hợp các động từ khác. Động từ một mình được coi là chức năng tốt hơn so với các lớp, hầu hết thời gian. Nếu động từ ngụ ý quá nhiều hành động có bối cảnh chung của riêng chúng, thì nó phải là một lớp trong chính nó.

Vì vậy, ý tưởng chỉ là để cho bạn có một ý tưởng đơn giản về lớp học bằng cách xác định một vai trò duy nhất, đó có thể là liên kết của một số vai trò phụ (được thực hiện bởi các đối tượng thành viên hoặc các đối tượng khác).

Tôi thường xây dựng các lớp Manager có nhiều lớp khác nhau trong đó. Giống như một Nhà máy, một Cơ quan đăng ký, v.v ... Xem một lớp Người quản lý như một loại trưởng nhóm, một người chỉ huy dàn nhạc hướng dẫn các dân tộc khác làm việc cùng nhau để đạt được một ý tưởng cấp cao. Anh ta có một vai trò, nhưng ngụ ý làm việc với các vai trò độc đáo khác bên trong. Bạn cũng có thể thấy nó giống như cách một công ty được tổ chức: một CEO không phải là một người làm việc hiệu quả ở mức năng suất thuần túy, nhưng nếu anh ta không ở đó, thì không có gì có thể làm việc chính xác với nhau. Đó là vai trò của anh ấy.

Khi bạn thiết kế, xác định vai trò duy nhất. Và đối với mỗi vai trò, một lần nữa hãy xem liệu nó có thể bị cắt trong một số vai trò khác không. Bằng cách đó, nếu bạn cần mô phỏng thay đổi cách Trình quản lý của bạn xây dựng các đối tượng, chỉ cần thay đổi Nhà máy và yên tâm.


-1

SRP không chỉ là về việc phân chia các lớp, mà còn về chức năng ủy nhiệm.

Trong ví dụ Dog được sử dụng ở trên, không sử dụng SRP làm lý do để có 3 lớp riêng biệt như DogBarker, DogWalker, v.v (độ gắn kết thấp). Thay vào đó, hãy nhìn vào việc thực hiện các phương thức của một lớp và quyết định xem chúng có "biết quá nhiều" không. Bạn vẫn có thể có dog.walk (), nhưng có lẽ phương thức walk () nên ủy thác cho một lớp khác các chi tiết về cách đi bộ được thực hiện.

Thực tế, chúng tôi đang cho phép lớp Dog có một lý do để thay đổi: bởi vì Chó thay đổi. Tất nhiên, kết hợp điều này với các nguyên tắc RẮN khác, bạn sẽ mở rộng Dog cho chức năng mới thay vì thay đổi Dog (mở / đóng). Và bạn sẽ tiêm các phụ thuộc của bạn như IMove và IEat. Và tất nhiên bạn sẽ thực hiện các giao diện riêng biệt này (phân tách giao diện). Chó sẽ chỉ thay đổi nếu chúng tôi tìm thấy một lỗi hoặc nếu Chó thay đổi căn bản (Liskov Sub, không mở rộng và sau đó xóa hành vi).

Hiệu quả ròng của RẮN là chúng ta có thể viết mã mới thường xuyên hơn chúng ta phải sửa đổi mã hiện có và đó là một chiến thắng lớn.


-1

Tất cả phụ thuộc vào định nghĩa trách nhiệm và cách định nghĩa đó sẽ ảnh hưởng đến việc bảo trì mã của bạn. Tất cả mọi thứ tập trung vào một điều và đó là cách thiết kế của bạn sẽ giúp bạn trong việc duy trì mã của bạn.

Và Giống như ai đó đã nói "Đi bộ trên nước và thiết kế phần mềm từ các thông số kỹ thuật cụ thể là dễ dàng, miễn là cả hai đều bị đóng băng".

Vì vậy, nếu chúng ta xác định trách nhiệm theo cách cụ thể hơn, thì chúng ta không cần phải thay đổi nó.

Đôi khi trách nhiệm sẽ rõ ràng rõ ràng, nhưng đôi khi nó có thể tinh tế và chúng ta cần quyết định một cách thận trọng.

Giả sử, chúng ta thêm một trách nhiệm khác vào lớp Dog có tên là CatchThief (). Bây giờ nó có thể dẫn đến một trách nhiệm khác. Ngày mai, nếu cách Dog bắt kẻ trộm phải được thay đổi bởi Police Dept, thì lớp Dog sẽ phải được thay đổi. Sẽ tốt hơn nếu tạo một lớp con khác trong trường hợp này và đặt tên là ThiefCathcerDog. Nhưng ở một góc nhìn khác, nếu chúng tôi chắc chắn rằng nó sẽ không thay đổi trong bất kỳ trường hợp nào, hoặc cách thức bắtThief đã phụ thuộc vào một số tham số bên ngoài, thì hoàn toàn ổn khi có trách nhiệm này. Nếu trách nhiệm không quá kỳ lạ thì chúng ta phải quyết định một cách thận trọng dựa trên trường hợp sử dụng.


-1

"Một lý do để thay đổi" phụ thuộc vào người đang sử dụng hệ thống. Hãy chắc chắn rằng bạn có các trường hợp sử dụng cho từng tác nhân và lập danh sách các thay đổi có khả năng nhất, cho mỗi trường hợp thay đổi có thể xảy ra, đảm bảo chỉ có một lớp sẽ bị ảnh hưởng bởi thay đổi đó. Nếu bạn đang thêm một trường hợp sử dụng hoàn toàn mới, hãy đảm bảo bạn chỉ cần mở rộng một lớp để làm như vậy.


1
Một lý do để thay đổi không liên quan gì đến số lượng ca sử dụng, diễn viên hoặc khả năng thay đổi. Bạn không nên lập một danh sách các thay đổi có khả năng. Đúng là một thay đổi chỉ nên tác động đến một lớp. Có thể mở rộng một lớp để thích ứng với sự thay đổi đó là tốt, nhưng đó là nguyên tắc đóng mở, không phải SRP.
candied_orange

Tại sao chúng ta không nên lập một danh sách các thay đổi có khả năng nhất? Thiết kế đầu cơ?
kiwicomb123

bởi vì nó sẽ không giúp ích gì, sẽ không hoàn thành và có nhiều cách hiệu quả hơn để đối phó với sự thay đổi hơn là cố gắng dự đoán nó. Chỉ mong đợi nó. Quyết định cô lập và tác động sẽ là tối thiểu.
candied_orange

Được rồi, tôi hiểu, nó vi phạm nguyên tắc "bạn không cần nó".
kiwicomb123

Trên thực tế yagni có thể được đẩy đến xa là tốt. Nó thực sự có nghĩa là để ngăn bạn thực hiện các trường hợp sử dụng đầu cơ. Không giữ bạn khỏi cách ly một trường hợp sử dụng được thực hiện.
candied_orange
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.