Làm thế nào một lớp có thể có nhiều phương thức mà không vi phạm nguyên tắc trách nhiệm duy nhất


64

Nguyên tắc trách nhiệm duy nhất được xác định trên wikipedia

Nguyên tắc trách nhiệm duy nhất là nguyên tắc lập trình máy tính nói rằng mọi mô-đun, lớp hoặc chức năng phải có trách nhiệm đối với một phần chức năng do phần mềm cung cấp và trách nhiệm đó phải được gói gọn trong lớp

Nếu một lớp chỉ nên có một trách nhiệm duy nhất, làm thế nào nó có thể có nhiều hơn 1 phương thức? Mỗi phương thức sẽ không có trách nhiệm khác nhau, điều đó có nghĩa là lớp sẽ có nhiều hơn 1 trách nhiệm.

Mỗi ví dụ tôi đã thấy thể hiện nguyên tắc trách nhiệm duy nhất sử dụng một lớp ví dụ chỉ có một phương thức. Có thể giúp xem một ví dụ hoặc có một lời giải thích về một lớp với nhiều phương thức vẫn có thể được coi là có một trách nhiệm.


11
Tại sao một downvote? Có vẻ như một câu hỏi lý tưởng cho SE.SE; người nghiên cứu chủ đề, và nỗ lực làm cho câu hỏi rất rõ ràng. Nó xứng đáng được nâng cấp thay thế.
Arseni Mourzenko

19
Downvote có lẽ là do đây là một câu hỏi đã được hỏi và trả lời nhiều lần rồi, ví dụ, xem phần mềmengineering.stackexchange.com/questions/ 345018/ . Theo tôi, nó không thêm các khía cạnh mới đáng kể.
Hans-Martin Mosner


9
Điều này chỉ đơn giản là reductio ad absurdum. Nếu mỗi lớp theo nghĩa đen chỉ được phép một phương thức, thì theo nghĩa đen, không có cách nào mà bất kỳ chương trình nào có thể làm được nhiều hơn một điều.
Darrel Hoffman

6
@DarrelHoffman Điều đó không đúng. Nếu mỗi lớp là một functor chỉ có phương thức "call ()", thì về cơ bản bạn chỉ mô phỏng lập trình thủ tục đơn giản với lập trình hướng đối tượng. Bạn vẫn có thể làm bất cứ điều gì bạn có thể làm khác, vì phương thức "call ()" của một lớp có thể gọi nhiều phương thức "call ()" của các lớp khác.
Vaelus

Câu trả lời:


29

Trách nhiệm duy nhất có thể không phải là điều mà một chức năng duy nhất có thể thực hiện.

 class Location { 
     public int getX() { 
         return x;
     } 
     public int getY() { 
         return y; 
     } 
 }

Lớp này có thể phá vỡ nguyên tắc trách nhiệm duy nhất. Không phải vì nó có hai chức năng, nhưng nếu mã cho getX()getY()phải đáp ứng các bên liên quan khác nhau có thể yêu cầu thay đổi. Nếu Phó Chủ tịch, ông X gửi một bản ghi nhớ rằng tất cả các số sẽ được biểu thị dưới dạng số dấu phẩy động và Giám đốc Kế toán, bà Y khẳng định rằng tất cả các số mà bộ phận của bà đánh giá sẽ vẫn là số nguyên cho dù ông X nghĩ gì tốt hơn thì lớp này có tốt hơn một ý tưởng duy nhất về trách nhiệm của ai vì mọi thứ sắp trở nên khó hiểu.

Nếu SRP đã được theo dõi thì sẽ rõ ràng nếu lớp Location đóng góp vào những thứ mà ông X và nhóm của ông được tiếp xúc. Làm rõ những gì lớp chịu trách nhiệm và bạn biết chỉ thị nào tác động đến lớp này. Nếu cả hai đều tác động đến lớp này thì nó được thiết kế kém để giảm thiểu tác động của thay đổi. "Một lớp chỉ nên có một lý do để thay đổi" không có nghĩa là toàn bộ lớp chỉ có thể làm một việc nhỏ. Điều đó có nghĩa là tôi không nên nhìn vào lớp học và nói rằng cả ông X và bà Y đều có hứng thú với lớp học này.

Khác với những thứ như thế. Không, nhiều phương pháp đều ổn. Chỉ cần đặt tên cho nó để làm rõ phương thức nào thuộc về lớp và phương thức nào không.

SRP của chú Bob thiên về Luật của Conway hơn là Luật của Quăn . Chú Bob ủng hộ việc áp dụng Luật xoăn (làm một việc) cho các chức năng chứ không phải các lớp học. SRP cảnh báo chống lại lý do trộn để thay đổi cùng nhau. Luật của Conway cho biết hệ thống sẽ tuân theo cách thông tin của một tổ chức. Điều đó dẫn đến việc theo dõi SRP vì bạn không quan tâm đến những gì bạn chưa bao giờ nghe về.

"Một mô-đun phải chịu trách nhiệm với một, và chỉ một, diễn viên"

Robert C Martin - Kiến trúc sạch

Mọi người cứ muốn SRP về mọi lý do để giới hạn phạm vi. Có nhiều lý do để giới hạn phạm vi hơn SRP. Tôi tiếp tục giới hạn phạm vi bằng cách khẳng định lớp học là một sự trừu tượng có thể lấy một cái tên đảm bảo nhìn vào bên trong sẽ không làm bạn ngạc nhiên .

Bạn có thể áp dụng Luật xoăn cho các lớp học. Bạn đang ở ngoài những gì chú Bob nói nhưng bạn có thể làm được. Bạn sai ở đâu là khi bạn bắt đầu nghĩ rằng điều đó có nghĩa là một chức năng. Điều đó giống như nghĩ rằng một gia đình chỉ nên có một đứa con. Có nhiều hơn một đứa trẻ không ngăn cản nó trở thành một gia đình.

Nếu bạn áp dụng luật của xoăn cho một lớp, mọi thứ trong lớp sẽ là về một ý tưởng thống nhất duy nhất. Ý tưởng đó có thể rộng. Ý tưởng có thể là sự kiên trì. Nếu một số chức năng tiện ích ghi nhật ký nằm trong đó, thì chúng rõ ràng không đúng chỗ. Không thành vấn đề nếu ông X là người duy nhất quan tâm đến mã này.

Nguyên tắc cổ điển để áp dụng ở đây được gọi là Tách mối quan tâm . Nếu bạn tách tất cả các mối quan tâm của mình, có thể lập luận rằng những gì còn lại ở bất kỳ nơi nào là một mối quan tâm. Đó là những gì chúng tôi gọi là ý tưởng này trước khi bộ phim City Slickers năm 1991 giới thiệu cho chúng tôi nhân vật Quăn.

Điều này là tốt Chỉ là những gì chú Bob gọi là trách nhiệm không phải là vấn đề đáng lo ngại. Trách nhiệm với anh ấy không phải là thứ bạn tập trung vào. Đó là thứ có thể buộc bạn phải thay đổi. Bạn có thể tập trung vào một mối quan tâm và vẫn tạo mã chịu trách nhiệm cho các nhóm người khác nhau với các chương trình nghị sự khác nhau.

Có lẽ bạn không quan tâm đến điều đó. Khỏe. Nghĩ rằng việc nắm giữ để "làm một việc" sẽ giải quyết tất cả các tai ương trong thiết kế của bạn cho thấy sự thiếu trí tưởng tượng về "một điều" cuối cùng có thể là gì. Một lý do khác để giới hạn phạm vi là tổ chức. Bạn có thể lồng nhiều "một thứ" bên trong "một thứ" khác cho đến khi bạn có một ngăn kéo rác đầy đủ mọi thứ. Tôi đã nói về điều đó trước đây

Tất nhiên, lý do OOP cổ điển để giới hạn phạm vi là lớp có các trường riêng trong đó và sau đó sử dụng getters để chia sẻ dữ liệu đó, chúng tôi đặt mọi phương thức cần dữ liệu đó trong lớp nơi chúng có thể sử dụng dữ liệu ở chế độ riêng tư. Nhiều người thấy điều này quá hạn chế để sử dụng như một bộ giới hạn phạm vi bởi vì không phải mọi phương thức thuộc về nhau đều sử dụng chính xác các trường giống nhau. Tôi muốn đảm bảo rằng bất kỳ ý tưởng nào mang dữ liệu lại với nhau đều là cùng một ý tưởng mang các phương thức lại với nhau.

Cách chức năng để xem xét điều này là a.f(x)a.g(x)chỉ đơn giản là f a (x) và g a (x). Không phải hai chức năng mà là sự liên tục của các cặp chức năng khác nhau. Các athậm chí không cần phải có dữ liệu trong đó. Nó có thể chỉ đơn giản là cách bạn biết cái nào fvà cách gtriển khai bạn sẽ sử dụng. Các chức năng thay đổi cùng thuộc về nhau. Đó là đa hình cũ tốt.

SRP chỉ là một trong nhiều lý do để giới hạn phạm vi. Đó là một thứ tốt. Nhưng không phải chỉ có một.


25
Tôi nghĩ rằng câu trả lời này gây nhầm lẫn cho ai đó đang cố gắng tìm ra SRP. Cuộc chiến giữa ông Chủ tịch và bà Giám đốc không được giải quyết thông qua các phương tiện kỹ thuật và sử dụng nó để biện minh cho một quyết định kỹ thuật là vô nghĩa. Luật của Conway trong hành động.
whatsisname

8
@whatsisname Ngược lại. SRP rõ ràng có nghĩa là áp dụng cho các bên liên quan. Nó không có gì để làm với thiết kế kỹ thuật. Bạn có thể không đồng ý với cách tiếp cận đó, nhưng đó là cách SRP ban đầu được xác định bởi chú Bob, và ông phải nhắc lại nhiều lần vì một số lý do, mọi người dường như không thể hiểu được khái niệm đơn giản này nó thực sự hữu ích là một câu hỏi hoàn toàn trực giao).
Luaan

Luật xoăn, như được mô tả bởi Tim Ottinger, nhấn mạnh rằng một biến nên luôn có nghĩa là một điều. Đối với tôi, SRP mạnh hơn thế một chút; một lớp về mặt khái niệm có thể đại diện cho "một điều", nhưng vi phạm SRP nếu hai trình điều khiển thay đổi bên ngoài xử lý một số khía cạnh của "một điều" đó theo những cách khác nhau hoặc quan tâm đến hai khía cạnh khác nhau. Vấn đề là một trong những mô hình; bạn đã chọn mô hình hóa một cái gì đó dưới dạng một lớp duy nhất, nhưng có một cái gì đó về miền khiến cho lựa chọn đó trở nên có vấn đề (mọi thứ bắt đầu cản trở bạn khi codebase phát triển).
Filip Milovanović

2
@ FilipMilovanović Sự giống nhau mà tôi thấy giữa Luật Conway và SRP, theo cách chú Bob giải thích SRP trong cuốn sách Kiến trúc sạch của ông xuất phát từ giả định rằng tổ chức có biểu đồ org theo chu kỳ sạch. Đây là một ý tưởng cũ. Ngay cả Kinh thánh cũng có một câu trích dẫn ở đây: "Không ai có thể phục vụ hai chủ nhân".
candied_orange

1
@TKK tôi liên quan đến nó (không đánh đồng nó) với luật của Conways không phải luật của xoăn. Tôi bác bỏ ý kiến ​​cho rằng SRP là luật của xoăn chủ yếu là do chú Bob đã tự nói như vậy trong cuốn sách Kiến trúc sạch của mình.
candied_orange

48

Chìa khóa ở đây là phạm vi , hoặc, nếu bạn thích, độ chi tiết . Một phần chức năng được đại diện bởi một lớp có thể được phân tách thành các phần chức năng, mỗi phần là một phương thức.

Đây là một ví dụ. Hãy tưởng tượng bạn cần tạo một CSV từ một chuỗi. Nếu bạn muốn tuân thủ RFC 4180, sẽ mất khá nhiều thời gian để thực hiện thuật toán và xử lý tất cả các trường hợp cạnh.

Làm theo một phương thức duy nhất sẽ dẫn đến một mã không thể đọc được và đặc biệt, phương thức này sẽ thực hiện một số việc cùng một lúc. Do đó, bạn sẽ chia nó thành nhiều phương thức; chẳng hạn, một trong số chúng có thể chịu trách nhiệm tạo tiêu đề, tức là dòng đầu tiên của CSV, trong khi phương thức khác sẽ chuyển đổi giá trị của bất kỳ loại nào thành biểu diễn chuỗi phù hợp với định dạng CSV, trong khi một phương thức khác sẽ xác định nếu giá trị cần được đính kèm thành dấu ngoặc kép.

Những phương pháp đó có trách nhiệm riêng của họ. Phương thức kiểm tra xem có cần thêm dấu ngoặc kép hay không có phương thức riêng và phương thức tạo tiêu đề có một. Đây là SRP được áp dụng cho các phương thức .

Bây giờ, tất cả các phương thức đó đều có một mục tiêu chung, đó là thực hiện một chuỗi và tạo CSV. Đây là trách nhiệm duy nhất của lớp .


Pablo H nhận xét:

Ví dụ hay, nhưng tôi cảm thấy nó vẫn không trả lời được tại sao SRP cho phép một lớp có nhiều hơn một phương thức công khai.

Thật. Ví dụ CSV tôi đưa ra có lý tưởng là một phương thức công khai và tất cả các phương thức khác là riêng tư. Một ví dụ tốt hơn sẽ là một hàng đợi, được thực hiện bởi một Queuelớp. Về cơ bản, lớp này sẽ chứa hai phương thức: push(còn được gọi enqueue) và pop(cũng được gọi dequeue).

  • Trách nhiệm của Queue.pushviệc thêm một đối tượng vào đuôi của hàng đợi.

  • Trách nhiệm của Queue.poplà loại bỏ một đối tượng khỏi đầu hàng đợi và xử lý trường hợp hàng đợi trống.

  • Trách nhiệm của Queuelớp là cung cấp logic hàng đợi.


1
Ví dụ hay, nhưng tôi cảm thấy nó vẫn không trả lời được tại sao SRP cho phép một lớp có nhiều hơn một phương thức công khai .
Pablo H

1
@PabloH: công bằng. Tôi đã thêm một ví dụ khác trong đó một lớp có hai phương thức.
Arseni Mourzenko

30

Một chức năng là một chức năng.

Một trách nhiệm là một trách nhiệm.

Một thợ máy có trách nhiệm sửa chữa ô tô, sẽ liên quan đến chẩn đoán, một số nhiệm vụ bảo trì đơn giản, một số công việc sửa chữa thực tế, một số nhiệm vụ cho người khác, v.v.

Một lớp container (danh sách, mảng, từ điển, bản đồ, v.v.) có trách nhiệm lưu trữ các đối tượng, bao gồm lưu trữ chúng, cho phép chèn, cung cấp quyền truy cập, một số loại đặt hàng, v.v.

Một trách nhiệm không có nghĩa là có rất ít mã / chức năng, nó có nghĩa là bất kỳ chức năng nào có "thuộc về nhau" thuộc cùng một trách nhiệm.


2
Đồng tình. @Aulis Ronkainen - để buộc trong hai câu trả lời. Và đối với các trách nhiệm lồng nhau, sử dụng sự tương tự cơ học của bạn, một nhà để xe có trách nhiệm bảo trì các phương tiện. các cơ chế khác nhau trong nhà để xe có trách nhiệm cho các bộ phận khác nhau của xe, nhưng mỗi cơ chế này hoạt động cùng nhau trong sự gắn kết
wolfsshield

2
@wolfsshield, đồng ý. Cơ khí chỉ làm một việc là vô ích, nhưng thợ máy có một trách nhiệm duy nhất thì không (ít nhất là nhất thiết). Mặc dù sự tương tự trong cuộc sống thực không phải lúc nào cũng tốt nhất để mô tả các khái niệm OOP trừu tượng, điều quan trọng là phải phân biệt các khác biệt này. Tôi tin rằng không hiểu sự khác biệt là những gì tạo ra sự nhầm lẫn ở nơi đầu tiên.
Aulis Ronkainen

3
@AulisRonkainen Mặc dù trông, có mùi và cảm giác giống như một sự tương tự, tôi đã có ý định sử dụng cơ chế để làm nổi bật ý nghĩa cụ thể của thuật ngữ Trách nhiệm trong SRP. Tôi hoàn toàn đồng ý với câu trả lời của bạn.
Peter

20

Trách nhiệm duy nhất không nhất thiết có nghĩa là nó chỉ làm một việc.

Lấy ví dụ một lớp dịch vụ người dùng:

class UserService {
    public User Get(int id) { /* ... */ }
    public User[] List() { /* ... */ }

    public bool Create(User u) { /* ... */ }
    public bool Exists(int id) { /* ... */ }
    public bool Update(User u) { /* ... */ }
}

Lớp này có nhiều phương thức nhưng trách nhiệm của nó là rõ ràng. Nó cung cấp quyền truy cập vào hồ sơ người dùng trong kho lưu trữ dữ liệu. Phụ thuộc duy nhất của nó là mô hình Người dùng và lưu trữ dữ liệu. Nó được ghép lỏng lẻo và có tính gắn kết cao, đó thực sự là những gì SRP đang cố gắng khiến bạn phải suy nghĩ.

Không nên nhầm lẫn SRP với "Nguyên tắc phân tách giao diện" (xem RẮN ). Nguyên tắc phân tách giao diện (ISP) nói rằng các giao diện nhỏ hơn, nhẹ hơn thích hợp hơn các giao diện tổng quát hơn. Go sử dụng rất nhiều ISP trong thư viện tiêu chuẩn của nó:

// Interface to read bytes from a stream
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface to write bytes to a stream
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface to convert an object into JSON
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

SRP và ISP chắc chắn có liên quan, nhưng cái này không ngụ ý cái kia. ISP ở cấp độ giao diện và SRP ở cấp độ lớp. Nếu một lớp thực hiện một số giao diện đơn giản, nó có thể không còn chỉ có một trách nhiệm.

Cảm ơn Luaan vì đã chỉ ra sự khác biệt giữa ISP và SRP.


3
Trên thực tế, bạn đang mô tả nguyên tắc phân tách giao diện ("I" trong RẮN). SRP là một con thú khác.
Luaan

Như một bên, bạn đang sử dụng quy ước mã hóa nào ở đây? Tôi mong chờ các đối tượng UserServiceUserđược UpperCamelCase, nhưng phương pháp Create , ExistsUpdatetôi đã có thể làm lowerCamelCase.
KlaymenDK

1
@KlaymenDK Bạn nói đúng, chữ hoa chỉ là thói quen sử dụng Go (chữ hoa = xuất / công khai, chữ thường = riêng tư)
Jesse

@Luaan Cảm ơn bạn đã chỉ ra điều đó, tôi sẽ làm rõ câu trả lời của tôi
Jesse

1
@KlaymenDK Rất nhiều ngôn ngữ sử dụng PascalCase cho các phương thức cũng như các lớp. C # chẳng hạn.
Brilliantastick

15

Có một đầu bếp trong một nhà hàng. Trách nhiệm duy nhất của anh là nấu ăn. Tuy nhiên, anh ta có thể nấu bít tết, khoai tây, bông cải xanh và hàng trăm thứ khác. Bạn sẽ thuê một đầu bếp cho mỗi món ăn trong thực đơn của bạn? Hoặc một đầu bếp cho mỗi thành phần của mỗi món ăn? Hoặc một đầu bếp có thể đáp ứng trách nhiệm duy nhất của mình: Nấu ăn?

Nếu bạn yêu cầu đầu bếp đó cũng làm bảng lương, đó là khi bạn vi phạm SRP.


4

Ví dụ: lưu trữ trạng thái đột biến.

Giả sử bạn có lớp đơn giản nhất từ ​​trước đến nay, công việc duy nhất của họ là lưu trữ một int.

public class State {
    private int i;


    public State(int i) { this.i = i; }
}

Nếu bạn bị giới hạn chỉ có 1 phương thức, bạn có thể có một setState()hoặc một getState(), trừ khi bạn phá vỡ đóng gói và icông khai.

  • Một setter là vô dụng nếu không có getter (bạn không bao giờ có thể đọc thông tin)
  • Một getter là vô dụng nếu không có setter (bạn không bao giờ có thể thay đổi thông tin).

Vì vậy, rõ ràng, trách nhiệm duy nhất này đòi hỏi phải có ít nhất 2 phương thức trên lớp này. QED.


4

Bạn đang hiểu sai nguyên tắc trách nhiệm duy nhất.

Trách nhiệm duy nhất không bằng một phương pháp duy nhất. Chúng có nghĩa là những thứ khác nhau. Trong phát triển phần mềm, chúng tôi nói về sự gắn kết . Các hàm (phương thức) có độ gắn kết cao "thuộc" với nhau và có thể được tính là thực hiện một trách nhiệm duy nhất.

Nhà phát triển phải thiết kế hệ thống sao cho nguyên tắc trách nhiệm duy nhất được thực hiện. Người ta có thể xem đây là một kỹ thuật trừu tượng và do đó đôi khi là một vấn đề quan điểm. Việc thực hiện nguyên tắc trách nhiệm duy nhất làm cho mã chủ yếu dễ kiểm tra hơn và dễ hiểu hơn về kiến ​​trúc và thiết kế của nó.


2

Việc này thường hữu ích (trong bất kỳ ngôn ngữ nào, nhưng đặc biệt là ngôn ngữ OO) để xem xét mọi thứ và sắp xếp chúng theo quan điểm của dữ liệu thay vì các chức năng.

Do đó, hãy xem xét trách nhiệm của một lớp là duy trì tính toàn vẹn và cung cấp trợ giúp để sử dụng chính xác dữ liệu mà nó sở hữu. Rõ ràng điều này dễ thực hiện hơn nếu tất cả các mã nằm trong một lớp, thay vì trải rộng trên một số lớp. Thêm hai điểm được thực hiện đáng tin cậy hơn và mã được duy trì dễ dàng hơn, với một Point add(Point p)phương thức trong Pointlớp hơn là có ở nơi khác.

Và đặc biệt, lớp không được tiết lộ bất cứ điều gì có thể dẫn đến dữ liệu không nhất quán hoặc không chính xác. Ví dụ: nếu Pointphải nằm trong mặt phẳng (0,0) đến (127.127), thì hàm tạo và mọi phương thức sửa đổi hoặc tạo mới Pointcó trách nhiệm kiểm tra các giá trị mà chúng đưa ra và từ chối mọi thay đổi sẽ vi phạm điều này yêu cầu. (Thường thì một cái gì đó giống như Pointlà bất biến, và đảm bảo rằng không có cách sửa đổi nào Pointsau khi nó được xây dựng cũng sẽ là khả năng đáp ứng của lớp)

Lưu ý rằng layering ở đây là hoàn toàn chấp nhận được. Bạn có thể có một Pointlớp để xử lý các điểm riêng lẻ và một Polygonlớp để xử lý một tập hợp Points; những điều này vẫn có trách nhiệm riêng vì Polygoncác đại biểu chịu mọi trách nhiệm đối phó với bất kỳ điều gì chỉ liên quan đến một Point(chẳng hạn như đảm bảo một điểm có cả giá trị xygiá trị) cho Pointlớp.

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.