Liệu một constructor xác nhận các đối số của nó có vi phạm SRP không?


66

Tôi đang cố gắng tuân thủ Nguyên tắc Trách nhiệm Đơn lẻ (SRP) càng nhiều càng tốt và đã quen với một mô hình nhất định (đối với SRP về các phương pháp) phụ thuộc nhiều vào các đại biểu. Tôi muốn biết cách tiếp cận này là âm thanh hoặc nếu có bất kỳ vấn đề nghiêm trọng nào với nó.

Ví dụ: để kiểm tra đầu vào cho hàm tạo, tôi có thể giới thiệu phương thức sau ( Streamđầu vào là ngẫu nhiên, có thể là bất cứ thứ gì)

private void CheckInput(Stream stream)
{
    if(stream == null)
    {
        throw new ArgumentNullException();
    }

    if(!stream.CanWrite)
    {
        throw new ArgumentException();
    }
}

Phương pháp này (được cho là) ​​làm nhiều hơn một điều

  • Kiểm tra đầu vào
  • Ném các ngoại lệ khác nhau

Do đó, để tuân thủ SRP, tôi đã thay đổi logic thành

private void CheckInput(Stream stream, 
                        params (Predicate<Stream> predicate, Action action)[] inputCheckers)
{
    foreach(var inputChecker in inputCheckers)
    {
        if(inputChecker.predicate(stream))
        {
            inputChecker.action();
        }
    }
}

Mà được cho là chỉ làm một việc (phải không?): Kiểm tra đầu vào. Để kiểm tra thực tế các đầu vào và đưa ra các ngoại lệ, tôi đã giới thiệu các phương pháp như

bool StreamIsNull(Stream s)
{
    return s == null;
}

bool StreamIsReadonly(Stream s)
{
    return !s.CanWrite;
}

void Throw<TException>() where TException : Exception, new()
{
    throw new TException();
}

và có thể gọi CheckInputnhư

CheckInput(stream,
    (this.StreamIsNull, this.Throw<ArgumentNullException>),
    (this.StreamIsReadonly, this.Throw<ArgumentException>))

Đây có phải là bất kỳ tốt hơn so với tùy chọn đầu tiên, hoặc tôi giới thiệu sự phức tạp không cần thiết? Có cách nào tôi vẫn có thể cải thiện mô hình này không, nếu nó khả thi?


26
Tôi có thể lập luận rằng CheckInputvẫn đang làm nhiều việc: Nó vừa lặp qua một mảng vừa gọi một hàm vị ngữ gọi một hàm hành động. Có phải đó không phải là vi phạm SRP?
Bart van Ingen Schenau

8
Vâng, đó là điểm tôi đã cố gắng để thực hiện.
Bart van Ingen Schenau

135
Điều quan trọng cần nhớ là đó là nguyên tắc trách nhiệm duy nhất ; không phải là nguyên tắc hành động duy nhất . Nó có một trách nhiệm: xác minh luồng được xác định và có thể ghi.
David Arno

40
Hãy nhớ rằng toàn bộ quan điểm của các nguyên tắc phần mềm này là làm cho mã dễ đọc và dễ bảo trì hơn. CheckInput ban đầu của bạn dễ đọc và bảo trì hơn nhiều so với phiên bản được cấu trúc lại của bạn. Trên thực tế, nếu tôi từng bắt gặp phương thức CheckInput cuối cùng của bạn trong một cơ sở mã, tôi sẽ loại bỏ tất cả và viết lại cho phù hợp với những gì ban đầu bạn có.
17 trên 26

17
Những "nguyên tắc" này thực tế là vô dụng vì bạn chỉ có thể định nghĩa "trách nhiệm duy nhất" theo bất kỳ cách nào bạn muốn đi trước với bất kỳ ý tưởng ban đầu nào của bạn. Nhưng nếu bạn cố gắng áp dụng chúng một cách cứng nhắc, tôi đoán bạn sẽ kết thúc với loại mã này, mà nói thẳng ra, thật khó hiểu.
Casey

Câu trả lời:


151

SRP có lẽ là nguyên tắc phần mềm bị hiểu lầm nhất.

Một ứng dụng phần mềm được xây dựng từ các mô-đun, được xây dựng từ các mô-đun, được xây dựng từ ...

Ở phía dưới, một hàm duy nhất như CheckInputsẽ chỉ chứa một chút logic, nhưng khi bạn đi lên, mỗi mô-đun kế tiếp sẽ gói gọn ngày càng nhiều logic và điều này là bình thường .

SRP không phải là về một hành động nguyên tử duy nhất . Đó là về một trách nhiệm duy nhất, ngay cả khi trách nhiệm đó đòi hỏi nhiều hành động ... và cuối cùng là về bảo trìkiểm tra :

  • nó thúc đẩy đóng gói (tránh các đối tượng của Chúa),
  • nó thúc đẩy phân tách các mối quan tâm (tránh thay đổi gợn sóng thông qua toàn bộ cơ sở mã),
  • nó giúp kiểm tra bằng cách thu hẹp phạm vi trách nhiệm.

Thực tế CheckInputđược thực hiện với hai kiểm tra và đưa ra hai ngoại lệ khác nhau là không liên quan đến một mức độ nào đó.

CheckInputcó trách nhiệm hẹp: đảm bảo rằng đầu vào tuân thủ các yêu cầu. Có, có nhiều yêu cầu, nhưng điều này không có nghĩa là có nhiều trách nhiệm. Vâng, bạn có thể chia séc, nhưng điều đó sẽ giúp như thế nào? Tại một số điểm, kiểm tra phải được liệt kê theo một cách nào đó.

Hãy so sánh:

Constructor(Stream stream) {
    CheckInput(stream);
    // ...
}

đấu với:

Constructor(Stream stream) {
    CheckInput(stream,
        (this.StreamIsNull, this.Throw<ArgumentNullException>),
        (this.StreamIsReadonly, this.Throw<ArgumentException>));
    // ...
}

Bây giờ, CheckInputlàm ít hơn ... nhưng người gọi nó làm nhiều hơn!

Bạn đã chuyển danh sách các yêu cầu từ CheckInput, nơi chúng được gói gọn, sang Constructornơi chúng được hiển thị.

Đó có phải là một sự thay đổi tốt? Nó phụ thuộc:

  • Nếu CheckInputchỉ được gọi ở đó: nó gây tranh cãi, một mặt nó làm cho các yêu cầu có thể nhìn thấy được, mặt khác nó làm mã hóa;
  • Nếu CheckInputđược gọi nhiều lần với cùng một yêu cầu , thì nó vi phạm DRY và bạn có vấn đề đóng gói.

Điều quan trọng là nhận ra rằng một trách nhiệm duy nhất có thể ngụ ý rất nhiều công việc. "Bộ não" của một chiếc xe tự lái có một trách nhiệm duy nhất:

Lái xe đến đích.

Đó là một trách nhiệm duy nhất, nhưng đòi hỏi phải phối hợp rất nhiều cảm biến và diễn viên, đưa ra nhiều quyết định và thậm chí có thể có những yêu cầu mâu thuẫn 1 ...

... tuy nhiên, tất cả được gói gọn. Vì vậy, khách hàng không quan tâm.

1 sự an toàn của hành khách, sự an toàn của người khác, tôn trọng các quy định, ...


2
Tôi nghĩ rằng cách bạn đang sử dụng từ "đóng gói" và các dẫn xuất của nó là khó hiểu. Ngoài ra, câu trả lời tuyệt vời!
Fabio nói Phục hồi lại

4
Tôi đồng ý với câu trả lời của bạn, nhưng lập luận về não xe tự lái thường cám dỗ mọi người phá vỡ SRP. Giống như bạn đã nói, đó là các mô-đun được tạo thành từ các mô-đun. Bạn có thể xác định mục đích của toàn bộ hệ thống đó, nhưng hệ thống đó nên tự chia nhỏ. Bạn có thể phá vỡ hầu hết mọi vấn đề.
Sava B.

13
@SavaB.: Chắc chắn, nhưng nguyên tắc vẫn giữ nguyên. Một mô-đun nên có một trách nhiệm duy nhất, mặc dù phạm vi lớn hơn các thành phần của nó.
Matthieu M.

3
@ user949300 Được rồi, thế còn "lái xe". Thực sự, "lái xe" là trách nhiệm và "an toàn" và "hợp pháp" là những yêu cầu về cách thức thực hiện trách nhiệm đó. Và chúng tôi thường liệt kê các yêu cầu khi nêu rõ trách nhiệm.
Brian McCutchon

1
"SRP có lẽ là nguyên tắc phần mềm bị hiểu lầm nhất." Bằng chứng là câu trả lời này :)
Michael

41

Trích dẫn chú Bob về SRP ( https://8thlight.com/blog/uncle-bob/2014/05/08/SingleReponsibilityPrincipl.html ):

Nguyên tắc trách nhiệm duy nhất (SRP) nói rằng mỗi mô-đun phần mềm nên có một và chỉ một lý do để thay đổi.

... Nguyên tắc này là về con người.

... Khi bạn viết một mô-đun phần mềm, bạn muốn đảm bảo rằng khi các thay đổi được yêu cầu, những thay đổi đó chỉ có thể bắt nguồn từ một người, hoặc đúng hơn, một nhóm người được ghép chặt chẽ duy nhất đại diện cho một chức năng kinh doanh được xác định hẹp.

... Đây là lý do chúng tôi không đưa SQL vào các tệp JSP. Đây là lý do chúng tôi không tạo HTML trong các mô-đun tính toán kết quả. Đây là lý do mà các quy tắc kinh doanh không nên biết lược đồ cơ sở dữ liệu. Đây là lý do chúng tôi tách mối quan tâm.

Ông giải thích rằng các mô-đun phần mềm phải giải quyết các lo lắng của các bên liên quan cụ thể. Do đó, trả lời câu hỏi của bạn:

Đây có phải là bất kỳ tốt hơn so với tùy chọn đầu tiên, hoặc tôi giới thiệu sự phức tạp không cần thiết? Có cách nào tôi vẫn có thể cải thiện mô hình này không, nếu nó khả thi?

IMO, bạn chỉ nhìn vào một phương thức, khi bạn nên xem ở cấp độ cao hơn (cấp độ trong trường hợp này). Có lẽ chúng ta nên xem những gì lớp của bạn hiện đang làm (và điều này đòi hỏi nhiều lời giải thích hơn về kịch bản của bạn). Hiện tại, lớp của bạn vẫn đang làm điều tương tự. Chẳng hạn, nếu ngày mai có một số yêu cầu thay đổi về một số xác nhận (ví dụ: "bây giờ luồng có thể là null"), thì bạn vẫn cần phải đến lớp này và thay đổi nội dung trong đó.


4
Câu trả lời hay nhất. Để giải thích về OP, nếu kiểm tra bảo vệ đến từ hai bên / bộ phận khác nhau, thì checkInputs()nên tách ra, nói thành checkMarketingInputs()checkRegulatoryInputs(). Nếu không thì kết hợp tất cả chúng thành một phương pháp là tốt.
dùng949300

36

Không, thay đổi này không được SRP thông báo.

Hãy tự hỏi tại sao không có kiểm tra trong trình kiểm tra của bạn cho "đối tượng được truyền vào là một luồng" . Câu trả lời là rõ ràng: ngôn ngữ ngăn người gọi biên dịch một chương trình truyền trong một luồng không.

Hệ thống loại C # không đủ để đáp ứng nhu cầu của bạn; kiểm tra của bạn đang thực hiện thực thi các bất biến không thể được thể hiện trong hệ thống loại ngày hôm nay . Nếu có một cách để nói rằng phương thức này có một luồng có thể ghi không thể xóa được, thì bạn đã viết nó, nhưng không có, vì vậy bạn đã làm điều tốt nhất tiếp theo: bạn đã thực thi hạn chế loại trong thời gian chạy. Hy vọng rằng bạn cũng đã ghi lại nó, để các nhà phát triển sử dụng phương pháp của bạn không phải vi phạm, thất bại trong các trường hợp thử nghiệm và sau đó khắc phục sự cố.

Đưa các loại vào một phương thức không vi phạm Nguyên tắc Trách nhiệm duy nhất; không phải là phương pháp thực thi các điều kiện tiên quyết của nó hoặc khẳng định các điều kiện hậu của nó.


1
Ngoài ra, để đối tượng được tạo ở trạng thái hợp lệ là trách nhiệm mà một nhà xây dựng về cơ bản luôn có. Nếu nó, như bạn đã đề cập, yêu cầu kiểm tra bổ sung mà bộ thực thi và / hoặc trình biên dịch không thể cung cấp, thì thực sự không có cách nào xung quanh nó.
SBI

23

Không phải tất cả các trách nhiệm được tạo ra như nhau.

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

Đây là hai ngăn kéo. Cả hai đều có một trách nhiệm. Họ từng có tên cho bạn biết những gì thuộc về họ. Một là ngăn kéo đựng đồ bằng bạc. Cái khác là ngăn kéo rác.

Vậy sự khác biệt là gì? Các ngăn kéo bằng bạc làm rõ những gì không thuộc về nó. Các ngăn kéo rác tuy nhiên chấp nhận bất cứ điều gì sẽ phù hợp. Lấy thìa ra khỏi ngăn kéo đựng đồ bằng bạc có vẻ rất sai. Tuy nhiên, tôi rất khó để nghĩ về bất cứ điều gì sẽ bị bỏ lỡ nếu loại bỏ khỏi ngăn kéo rác. Sự thật là bạn có thể yêu cầu bất cứ điều gì có một trách nhiệm duy nhất nhưng bạn nghĩ có trách nhiệm duy nhất tập trung hơn?

Một đối tượng có một trách nhiệm duy nhất không có nghĩa là chỉ có một điều có thể xảy ra ở đây. Trách nhiệm có thể làm tổ. Nhưng những trách nhiệm lồng nhau đó có ý nghĩa, chúng không nên làm bạn ngạc nhiên khi bạn tìm thấy chúng ở đây và bạn nên bỏ lỡ chúng nếu chúng không còn nữa.

Vì vậy, khi bạn cung cấp

CheckInput(Stream stream);

Tôi không thấy mình lo ngại rằng cả việc kiểm tra đầu vào và ném ngoại lệ. Tôi sẽ quan tâm nếu nó vừa kiểm tra đầu vào vừa tiết kiệm đầu vào. Đó là một bất ngờ khó chịu. Một cái tôi sẽ không bỏ lỡ nếu nó biến mất.


21

Khi bạn buộc mình trong các nút thắt và viết mã lạ để tuân thủ Nguyên tắc Phần mềm Quan trọng, thông thường bạn đã hiểu sai nguyên tắc (mặc dù đôi khi nguyên tắc này sai). Như câu trả lời xuất sắc của Matthieu chỉ ra, toàn bộ ý nghĩa của SRP phụ thuộc vào định nghĩa của "trách nhiệm".

Các lập trình viên có kinh nghiệm nhìn thấy các nguyên tắc này và liên hệ chúng với các ký ức về mã mà chúng tôi đã làm hỏng; lập trình viên ít kinh nghiệm nhìn thấy chúng và có thể không có gì liên quan đến chúng cả. Đó là một sự trừu tượng trôi nổi trong không gian, tất cả đều cười và không có mèo. Vì vậy, họ đoán, và nó thường đi xấu. Trước khi bạn phát triển ý nghĩa ngựa lập trình, sự khác biệt giữa mã quá phức tạp và mã thông thường hoàn toàn không rõ ràng.

Đây không phải là một điều răn tôn giáo mà bạn phải tuân theo bất kể hậu quả cá nhân. Đó là một quy tắc ngón tay cái có nghĩa là chính thức hóa một yếu tố của ý nghĩa ngựa lập trình và giúp bạn giữ mã của mình đơn giản và rõ ràng nhất có thể. Nếu nó có tác dụng ngược lại, bạn có quyền tìm kiếm một số đầu vào bên ngoài.

Trong lập trình, bạn không thể sai nhiều hơn là cố gắng suy ra ý nghĩa của một định danh từ các nguyên tắc đầu tiên bằng cách chỉ nhìn chằm chằm vào nó, và điều đó sẽ giúp các định danh viết về lập trình cũng giống như các định danh trong mã thực tế.


14

Vai trò kiểm tra

Đầu tiên, hãy để tôi đưa ra điều hiển nhiên ngoài kia, CheckInput đang làm một việc, ngay cả khi nó đang kiểm tra các khía cạnh khác nhau. Cuối cùng, nó kiểm tra đầu vào . Người ta có thể lập luận rằng đó không phải là một điều nếu bạn đang xử lý các phương thức được gọi DoSomething, nhưng tôi nghĩ sẽ an toàn khi cho rằng việc kiểm tra đầu vào không quá mơ hồ.

Việc thêm mẫu này cho các vị từ có thể hữu ích nếu bạn không muốn logic kiểm tra đầu vào được đặt vào lớp của bạn, nhưng mẫu này có vẻ khá dài dòng cho những gì bạn đang cố gắng đạt được. Có thể trực tiếp hơn nhiều khi chỉ cần truyền một giao diện IStreamValidatorvới một phương thức duy nhất isValid(Stream)nếu đó là những gì bạn muốn có được. Bất kỳ lớp thực hiện nào IStreamValidatorcũng có thể sử dụng các vị từ như StreamIsNullhoặc StreamIsReadonlynếu họ muốn, nhưng quay trở lại điểm trung tâm, đó là một thay đổi khá vô lý để thực hiện các lợi ích của việc duy trì nguyên tắc trách nhiệm duy nhất.

Kiểm tra sự tỉnh táo

Theo ý kiến ​​của tôi, tất cả chúng ta đều cho phép "kiểm tra độ tỉnh táo" để đảm bảo rằng ít nhất bạn đang xử lý một luồng không phải là không có giá trị và có thể ghi được, và kiểm tra cơ bản này không bằng cách nào đó làm cho lớp của bạn trở thành trình xác nhận luồng. Nhắc bạn, kiểm tra tinh vi hơn sẽ là tốt nhất để lại bên ngoài lớp học của bạn, nhưng đó là nơi dòng được rút ra. Khi bạn cần bắt đầu thay đổi trạng thái luồng của mình bằng cách đọc từ luồng đó hoặc dành tài nguyên cho xác thực, bạn đã bắt đầu thực hiện xác thực chính thức luồng của mình và đây là điều nên được đưa vào lớp của chính nó.

Phần kết luận

Tôi nghĩ rằng nếu bạn đang áp dụng một mô hình để tổ chức tốt hơn một khía cạnh của lớp học của bạn, thì nó xứng đáng để ở trong lớp của chính nó. Vì một mô hình không phù hợp, bạn cũng nên đặt câu hỏi liệu nó có thực sự thuộc về lớp của chính nó hay không. Tôi nghĩ rằng trừ khi bạn tin rằng việc xác thực luồng có thể sẽ bị thay đổi trong tương lai và đặc biệt nếu bạn tin rằng xác thực này thậm chí có thể có tính chất động, thì mô hình bạn mô tả là một ý tưởng tốt, ngay cả khi nó có thể ban đầu tầm thường. Nếu không, không cần phải tùy tiện làm cho chương trình của bạn phức tạp hơn. Hãy gọi một thuổng là một thuổng. Xác nhận là một chuyện, nhưng kiểm tra đầu vào null không phải là xác nhận, và do đó tôi nghĩ bạn có thể an toàn để giữ nó trong lớp của mình mà không vi phạm nguyên tắc trách nhiệm duy nhất.


4

Nguyên tắc rõ ràng không nêu một đoạn mã nên "chỉ làm một việc duy nhất".

"Trách nhiệm" trong SRP nên được hiểu ở cấp độ yêu cầu. Trách nhiệm của mã là đáp ứng yêu cầu kinh doanh. SRP bị vi phạm nếu một đối tượng thỏa mãn nhiều hơn một yêu cầu kinh doanh độc lập . Bằng cách độc lập, điều đó có nghĩa là một yêu cầu có thể thay đổi trong khi yêu cầu khác được giữ nguyên.

Có thể hình dung rằng một yêu cầu kinh doanh mới được đưa ra có nghĩa là đối tượng cụ thể này không nên kiểm tra để có thể đọc được, trong khi yêu cầu kinh doanh khác vẫn yêu cầu đối tượng kiểm tra để có thể đọc được? Không, bởi vì các yêu cầu kinh doanh không chỉ định chi tiết thực hiện ở cấp đó.

Một ví dụ thực tế về vi phạm SRP sẽ là mã như thế này:

var message = "Your package will arrive before " + DateTime.Now.AddDays(14);

Mã này rất đơn giản, nhưng vẫn có thể hình dung rằng văn bản sẽ thay đổi độc lập với ngày giao hàng dự kiến, vì chúng được quyết định bởi các bộ phận khác nhau của doanh nghiệp.


Một lớp khác nhau cho hầu hết mọi yêu cầu nghe có vẻ như một cơn ác mộng không lành.
tên của

@whatsisname: Có lẽ SRP không dành cho bạn. Không có nguyên tắc thiết kế áp dụng cho tất cả các loại và kích cỡ của dự án. (Nhưng lưu ý chúng tôi chỉ nói về độc lập yêu cầu (tức là có thể thay đổi một cách độc lập), không chỉ bất kỳ yêu cầu kể từ đó nó sẽ chỉ phụ thuộc vào cách hạt mịn họ được quy định.)
JacquesB

Tôi nghĩ rằng SRP đòi hỏi một yếu tố của phán đoán tình huống rất khó để mô tả trong một cụm từ lôi cuốn.
whatsisname

@whatsisname: Tôi hoàn toàn đồng ý.
JacquesB

+1 cho SRP bị vi phạm nếu một đối tượng thỏa mãn nhiều hơn một yêu cầu kinh doanh độc lập. Bằng cách độc lập, điều đó có nghĩa là một yêu cầu có thể thay đổi trong khi yêu cầu khác được giữ nguyên
Juzer Ali 7/2/18

3

Tôi thích điểm từ câu trả lời của @ EricLippert :

Hãy tự hỏi tại sao không có kiểm tra trong trình kiểm tra của bạn cho đối tượng được truyền vào là một luồng . Câu trả lời là rõ ràng: ngôn ngữ ngăn người gọi biên dịch một chương trình truyền trong một luồng không.

Hệ thống loại C # không đủ để đáp ứng nhu cầu của bạn; kiểm tra của bạn đang thực hiện thực thi các bất biến không thể được thể hiện trong hệ thống loại ngày hôm nay . Nếu có một cách để nói rằng phương thức này có một luồng có thể ghi không thể xóa được, thì bạn đã viết nó, nhưng không có, vì vậy bạn đã làm điều tốt nhất tiếp theo: bạn đã thực thi hạn chế loại trong thời gian chạy. Hy vọng rằng bạn cũng đã ghi lại nó, để các nhà phát triển sử dụng phương pháp của bạn không phải vi phạm, thất bại trong các trường hợp thử nghiệm và sau đó khắc phục sự cố.

EricLippert nói rằng đây là một vấn đề đối với hệ thống loại. Và vì bạn muốn sử dụng nguyên tắc trách nhiệm đơn (SRP), nên về cơ bản bạn cần hệ thống loại chịu trách nhiệm cho công việc này.

Thật sự có thể sắp xếp làm điều này trong C #. Chúng ta có thể bắt được chữ theo nullthời gian biên dịch, sau đó bắt được chữ không theo nghĩa đen nullvào thời gian chạy. Điều đó không tốt bằng kiểm tra toàn thời gian biên dịch, nhưng đó là một cải tiến nghiêm ngặt so với việc không bao giờ bắt kịp thời gian biên dịch.

Vì vậy, bạn biết làm thế nào C # có Nullable<T>? Hãy đảo ngược điều đó và thực hiện NonNullable<T>:

public struct NonNullable<T> where T : class
{
    public T Value { get; private set; }
    public NonNullable(T value)
    {
        if (value == null) { throw new NullArgumentException(); }
        this.Value = value;
    }
    //  Ease-of-use:
    public static implicit operator T(NonNullable<T> value) { return value.Value; }
    public static implicit operator NonNullable<T>(T value) { return new NonNullable<T>(value); }

    //  Hack-ish overloads that prevent null-literals from being implicitly converted into NonNullable<T>'s.
    public static implicit operator NonNullable<T>(Tuple<T> value) { return new NonNullable<T>(value.Item1); }
    public static implicit operator NonNullable<T>(Tuple<T, T> value) { return new NonNullable<T>(value.Item1); }
}

Bây giờ, thay vì viết

public void Foo(Stream stream)
{
  if (stream == null) { throw new NullArgumentException(); }

  // ...method code...
}

, chỉ viết:

public void Foo(NonNullable<Stream> stream)
{
  // ...method code...
}

Sau đó, có ba trường hợp sử dụng:

  1. Cuộc gọi của người dùng Foo()không có giá trị Stream:

    Stream stream = new Stream();
    Foo(stream);
    

    Đây là trường hợp sử dụng mong muốn và nó hoạt động với hoặc không có NonNullable<>.

  2. Người dùng gọi Foo()với null Stream:

    Stream stream = null;
    Foo(stream);
    

    Đây là một lỗi gọi. Ở đây NonNullable<>giúp thông báo cho người dùng rằng họ không nên làm điều này, nhưng nó không thực sự ngăn họ lại. Dù bằng cách nào, điều này dẫn đến một thời gian chạy NullArgumentException.

  3. Người dùng gọi Foo()với null:

    Foo(null);

    nullsẽ không hoàn toàn chuyển đổi thành a NonNullable<>, vì vậy người dùng sẽ gặp lỗi trong IDE trước thời gian chạy. Đây là ủy quyền kiểm tra null cho hệ thống loại, giống như SRP sẽ khuyên.

Bạn cũng có thể mở rộng phương pháp này để khẳng định những điều khác về lập luận của mình. Ví dụ: vì bạn muốn một luồng có thể ghi, bạn có thể xác định một luồng struct WriteableStream<T> where T:Streamkiểm tra cho cả nullstream.CanWritetrong hàm tạo. Đây vẫn sẽ là một loại kiểm tra thời gian chạy, nhưng:

  1. Nó trang trí các loại với WriteableStreamvòng loại, báo hiệu sự cần thiết cho người gọi.

  2. Nó thực hiện kiểm tra ở một nơi duy nhất trong mã, vì vậy bạn không phải lặp lại kiểm tra và throw InvalidArgumentExceptionmỗi lần.

  3. Nó phù hợp hơn với SRP bằng cách đẩy các nhiệm vụ kiểm tra loại sang hệ thống loại (được mở rộng bởi các nhà trang trí chung).


3

Cách tiếp cận của bạn hiện đang làm thủ tục. Bạn đang phá vỡ Streamđối tượng và xác nhận nó từ bên ngoài. Đừng làm điều đó - nó phá vỡ đóng gói. Hãy để Streamchịu trách nhiệm cho xác nhận của riêng mình. Chúng tôi không thể tìm cách áp dụng SRP cho đến khi chúng tôi có một số lớp để áp dụng nó.

Đây là một Streamhành động chỉ thực hiện một hành động nếu nó vượt qua xác nhận:

class Stream
{
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }

        System.out.println("My action");
    }
}

Nhưng bây giờ chúng tôi đang vi phạm SRP! "Một lớp chỉ nên có một lý do để thay đổi." Chúng tôi đã có sự kết hợp của 1) xác thực và 2) logic thực tế. Chúng tôi có hai lý do có thể cần phải thay đổi.

Chúng ta có thể giải quyết điều này với việc xác nhận trang trí . Đầu tiên, chúng ta cần chuyển đổi chúng ta Streamsang một giao diện và thực hiện nó như một lớp cụ thể.

interface Stream
{
    void someAction();
}

class DefaultStream implements Stream
{
    @Override
    public void someAction()
    {
        System.out.println("My action");
    }
}

Bây giờ chúng ta có thể viết một trình trang trí bao bọc a Stream, thực hiện xác nhận và trì hoãn theo quy định Streamcho logic thực tế của hành động.

class WritableStream implements Stream
{
    private final Stream stream;

    public WritableStream(final Stream stream)
    {
        this.stream = stream;
    }

    @Override
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }
        stream.someAction();
    }
}

Bây giờ chúng ta có thể sáng tác những cách này theo bất kỳ cách nào chúng ta muốn:

final Stream myStream = new WritableStream(
    new DefaultStream()
);

Muốn xác nhận bổ sung? Thêm một trang trí khác.


1

Công việc của một lớp là cung cấp một dịch vụ đáp ứng hợp đồng . Một lớp luôn có một hợp đồng: một tập hợp các yêu cầu để sử dụng nó, và hứa rằng nó đưa ra về trạng thái của nó và các đầu ra với điều kiện là các yêu cầu được đáp ứng. Hợp đồng này có thể rõ ràng, thông qua tài liệu và / hoặc xác nhận, hoặc ẩn, nhưng nó luôn tồn tại.

Một phần trong hợp đồng của lớp bạn là người gọi cung cấp cho hàm tạo một số đối số không được rỗng. Thực hiện hợp đồng trách nhiệm của lớp, vì vậy để kiểm tra xem người gọi đã đáp ứng một phần của hợp đồng có thể dễ dàng được coi là thuộc phạm vi trách nhiệm của lớp hay không.

Ý tưởng rằng một lớp học thực hiện hợp đồng là do Bertrand Meyer , người thiết kế ngôn ngữ lập trình Eiffel và ý tưởng thiết kế theo hợp đồng . Ngôn ngữ Eiffel làm cho đặc điểm kỹ thuật và kiểm tra phần hợp đồng của ngôn ngữ.


0

Như đã được chỉ ra trong các câu trả lời khác, SRP thường bị hiểu lầm. Đây không phải là về việc có mã nguyên tử mà chỉ thực hiện một chức năng. Đó là về việc đảm bảo rằng các đối tượng và phương thức của bạn chỉ làm một việc và một điều chỉ được thực hiện ở một nơi.

Hãy xem xét một ví dụ nghèo nàn trong mã giả.

class Math
    private int a;
    private int b;
    def constructor(int x, int y) 
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Trong ví dụ khá vô lý của chúng tôi, "trách nhiệm" của hàm tạo Math # là làm cho đối tượng toán học có thể sử dụng được. Nó làm như vậy bằng cách vệ sinh đầu vào, sau đó bằng cách đảm bảo các giá trị không -1.

Đây là SRP hợp lệ vì hàm tạo chỉ làm một việc. Nó đang chuẩn bị đối tượng Math. Tuy nhiên, nó không phải là rất duy trì. Nó vi phạm KHÔ.

Vì vậy, hãy vượt qua nó

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        cleanX(x)
        cleanY(y)
    end
    def cleanX(int x)
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
   end
   def cleanY(int y)
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Trong lần này, chúng tôi đã hiểu hơn một chút về DRY, nhưng chúng tôi vẫn có cách để đi với DRY. SRP mặt khác có vẻ hơi tắt. Bây giờ chúng ta có hai chức năng với cùng một công việc. Cả CleanX và CleanY khử trùng đầu vào.

Hãy thử đi

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i != null)
          return i
        else if(i < 0)
          return abs(i)
        else if (i == -1)
          throw "Some Silly Error"
        else
          return 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Bây giờ cuối cùng đã tốt hơn về DRY, và SRP dường như đã đồng ý. Chúng tôi chỉ có một nơi làm công việc "vệ sinh".

Về lý thuyết, mã này dễ bảo trì hơn và tốt hơn khi chúng ta sửa lỗi và thắt chặt mã, chúng ta chỉ cần thực hiện nó ở một nơi.

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i == null)
          return 0
        else if (i == -1)
          throw "Some Silly Error"
        else
          return abs(i)
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Trong hầu hết các trường hợp trong thế giới thực, các đối tượng sẽ phức tạp hơn và SRP sẽ được áp dụng trên một loạt các đối tượng. Ví dụ tuổi có thể thuộc về Cha, Mẹ, Con, Con gái, vì vậy thay vì có 4 lớp tính tuổi từ ngày sinh, bạn có một lớp Người thực hiện điều đó và 4 lớp kế thừa từ đó. Nhưng tôi hy vọng ví dụ này sẽ giúp giải thích. SRP không phải là về các hành động nguyên tử, mà là về một "công việc" đang được thực hiện.


-3

Nói về SRP, chú Bob không thích séc rỗng được rắc khắp nơi. Nói chung, với tư cách là một nhóm, nên tránh sử dụng các tham số null cho các hàm tạo bất cứ khi nào có thể. Khi bạn xuất bản mã bên ngoài nhóm của bạn, mọi thứ có thể thay đổi.

Việc thực thi tính không rỗng của các tham số của hàm tạo mà không đảm bảo tính cố kết của lớp trong câu hỏi dẫn đến sự phình to trong mã gọi, đặc biệt là các kiểm tra.

Nếu bạn thực sự muốn thực thi các hợp đồng như vậy, hãy cân nhắc sử dụng Debug.Asserthoặc một cái gì đó tương tự để giảm sự lộn xộn:

public AClassThatDefinitelyNeedsAWritableStream(Stream stream)
{
   Assert.That(stream.CanWrite, "Put crucial information here, and not inane bloat.");

   // Go on normal operation.
}
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.