Khả năng áp dụng nguyên tắc trách nhiệm duy nhất


40

Gần đây tôi đến bởi một vấn đề kiến ​​trúc có vẻ tầm thường. Tôi đã có một kho lưu trữ đơn giản trong mã của mình được gọi như thế này (mã nằm trong C #):

var user = /* create user somehow */;
_userRepository.Add(user);
/* do some other stuff*/
_userRepository.SaveChanges();

SaveChanges là một trình bao bọc đơn giản cam kết thay đổi cơ sở dữ liệu:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
}

Sau đó, sau một thời gian, tôi cần triển khai logic mới sẽ gửi thông báo email mỗi khi người dùng được tạo trong hệ thống. Vì có nhiều cuộc gọi đến _userRepository.Add()SaveChangestrên toàn hệ thống, tôi quyết định cập nhật SaveChangesnhư thế này:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
    foreach (var newUser in dataContext.GetAddedUsers())
    {
       _eventService.RaiseEvent(new UserCreatedEvent(newUser ))
    }
}

Bằng cách này, mã bên ngoài có thể đăng ký UserCreatedEvent và xử lý logic nghiệp vụ cần thiết sẽ gửi thông báo.

Nhưng nó đã được chỉ ra cho tôi rằng việc sửa đổi SaveChangesvi phạm nguyên tắc Trách nhiệm duy nhất của tôi và điều đó SaveChangeschỉ nên lưu và không kích hoạt bất kỳ sự kiện nào.

Đây có phải là một điểm hợp lệ? Dường như với tôi rằng việc nâng cao một sự kiện ở đây về cơ bản giống như ghi nhật ký: chỉ cần thêm một số chức năng phụ vào chức năng. Và SRP không cấm bạn sử dụng các sự kiện ghi nhật ký hoặc bắn trong các chức năng của mình, nó chỉ nói rằng logic đó nên được gói gọn trong các lớp khác và việc kho lưu trữ gọi các lớp khác là ổn.


22
Câu trả lời của bạn là: "OK, vậy bạn sẽ viết nó như thế nào để nó không vi phạm SRP nhưng vẫn cho phép một điểm sửa đổi duy nhất?"
Robert Harvey

43
Quan sát của tôi là việc nâng cao một sự kiện không thêm trách nhiệm. Trong thực tế, hoàn toàn ngược lại: nó ủy thác trách nhiệm ở một nơi khác.
Robert Harvey

Tôi nghĩ rằng đồng nghiệp của bạn là đúng, nhưng câu hỏi của bạn là hợp lệ và hữu ích, vì vậy được nâng cấp!
Andres F.

16
Không có thứ gọi là định nghĩa dứt khoát về một Trách nhiệm duy nhất. Người chỉ ra rằng nó vi phạm SRP là chính xác khi sử dụng định nghĩa cá nhân của họ và bạn đúng khi sử dụng định nghĩa của mình. Tôi nghĩ rằng thiết kế của bạn hoàn toàn ổn với sự cảnh báo rằng sự kiện này không phải là một lần mà theo đó các chức năng tương tự khác được thực hiện theo những cách khác nhau. Tính nhất quán là rất xa, rất xa, rất quan trọng để chú ý hơn một số hướng dẫn mơ hồ như SRP mang đến cực kỳ kết thúc với hàng tấn các lớp rất dễ hiểu mà không ai biết cách tạo ra công việc trong một hệ thống.
Dunk

Câu trả lời:


14

Có, đó có thể là một yêu cầu hợp lệ để có một kho lưu trữ các sự kiện nhất định trên một số hành động nhất định như Addhoặc SaveChanges- và tôi sẽ không đặt câu hỏi này (như một số câu trả lời khác) chỉ vì ví dụ cụ thể của bạn về việc thêm người dùng và gửi email có thể trông bit giả. Sau đây, chúng tôi giả sử yêu cầu này là hoàn toàn hợp lý trong bối cảnh hệ thống của bạn.

Vì vậy, , mã hóa cơ chế sự kiện cũng như ghi nhật ký cũng như lưu vào một phương thức vi phạm SRP . Đối với nhiều trường hợp, đây có thể là một vi phạm có thể chấp nhận được, đặc biệt là khi không ai muốn phân phối trách nhiệm bảo trì "lưu thay đổi" và "nâng cao sự kiện" cho các nhóm / người bảo trì khác nhau. Nhưng giả sử một ngày nào đó ai đó muốn thực hiện chính xác điều này, liệu nó có thể được giải quyết một cách đơn giản, có thể bằng cách đặt mã của những mối quan tâm đó vào các thư viện lớp khác nhau không?

Giải pháp cho vấn đề này là để cho kho lưu trữ ban đầu của bạn chịu trách nhiệm thực hiện các thay đổi đối với cơ sở dữ liệu, không có gì khác và tạo một kho lưu trữ proxy có cùng giao diện chung, sử dụng lại kho lưu trữ ban đầu và thêm các cơ chế sự kiện bổ sung vào các phương thức.

// In EventFiringUserRepo:
public void SaveChanges()
{
  _basicRepo.SaveChanges();
   FireEventsForNewlyAddedUsers();
}

private void FireEventsForNewlyAddedUsers()
{
  foreach (var newUser in _basicRepo.DataContext.GetAddedUsers())
  {
     _eventService.RaiseEvent(new UserCreatedEvent(newUser))
  }
}

Bạn có thể gọi lớp proxy là a NotifyingRepositoryhoặc ObservableRepositorynếu bạn thích, dọc theo dòng câu trả lời được bình chọn cao của @ Peter's (điều này thực sự không cho biết cách giải quyết vi phạm SRP, chỉ nói vi phạm là ổn).

Cả lớp kho lưu trữ mới và cũ nên xuất phát từ một giao diện chung, như được hiển thị trong mô tả mẫu Proxy cổ điển .

Sau đó, trong mã gốc của bạn, khởi tạo _userRepositorybởi một đối tượng của EventFiringUserRepolớp mới . Bằng cách đó, bạn giữ cho kho lưu trữ ban đầu tách biệt với cơ chế sự kiện. Nếu được yêu cầu, bạn có thể có kho lưu trữ sự kiện và kho lưu trữ ban đầu cạnh nhau và để người gọi quyết định xem họ sử dụng cái trước hay cái sau.

Để giải quyết một mối quan tâm được đề cập trong các ý kiến: không phải điều đó dẫn đến các proxy trên đầu các proxy trên đầu các proxy, v.v. Trên thực tế, việc thêm cơ chế sự kiện sẽ tạo ra một nền tảng để thêm các yêu cầu khác của loại "gửi email" bằng cách đăng ký vào các sự kiện, do đó, hãy tuân thủ SRP với các yêu cầu đó, mà không cần bất kỳ proxy nào. Nhưng một điều phải được thêm vào một lần ở đây là cơ chế sự kiện.

Nếu loại tách biệt này thực sự có giá trị trong bối cảnh hệ thống của bạn là thứ bạn và người đánh giá phải tự quyết định. Tôi có lẽ sẽ không tách rời việc ghi nhật ký khỏi mã gốc, bằng cách sử dụng một proxy khác không phải bằng cách thêm một trình ghi nhật ký vào sự kiện người nghe, mặc dù điều đó là có thể.


3
Ngoài câu trả lời này. Có những lựa chọn thay thế cho proxy, như AOP .
Laiv

1
Tôi nghĩ rằng bạn đã bỏ lỡ vấn đề, không phải việc gây ra sự kiện sẽ phá vỡ SRP mà việc nâng cao sự kiện chỉ dành cho người dùng "Mới" đòi hỏi repo phải có trách nhiệm trong việc biết điều gì tạo nên người dùng "Mới" thay vì "Mới được thêm vào cho tôi "người dùng
Ewan

@Ewan: vui lòng đọc lại câu hỏi. Đó là về một vị trí trong mã thực hiện một số hành động nhất định cần được kết hợp với các hành động khác ngoài khả năng đáp ứng của đối tượng đó. Và việc đặt hành động và sự kiện gây quỹ ở một nơi đã bị một người đánh giá ngang hàng nghi ngờ là phá vỡ SRP. Ví dụ về "cứu người dùng mới" chỉ nhằm mục đích trình diễn, hãy gọi ví dụ giả định nếu bạn muốn, nhưng đó không phải là vấn đề của IMHO.
Doc Brown

2
Đây là câu trả lời tốt nhất / đúng IMO. Nó không chỉ duy trì SRP mà còn duy trì nguyên tắc Mở / Đóng. Và nghĩ về tất cả các bài kiểm tra tự động thay đổi trong lớp có thể phá vỡ. Sửa đổi các thử nghiệm hiện có khi chức năng mới được thêm vào là một mùi lớn.
Keith Payne

1
Giải pháp này hoạt động như thế nào nếu có một giao dịch đang diễn ra? Khi điều đó đang diễn ra, SaveChanges()không thực sự tạo ra bản ghi cơ sở dữ liệu và cuối cùng nó có thể bị khôi phục. Có vẻ như bạn phải ghi đè AcceptAllChangeshoặc đăng ký sự kiện TransactionCompleted.
John Wu

29

Gửi một thông báo rằng kho dữ liệu liên tục thay đổi có vẻ như là một điều hợp lý để làm khi lưu.

Tất nhiên, bạn không nên coi Thêm là trường hợp đặc biệt - bạn cũng phải thực hiện các sự kiện để Sửa đổi và Xóa. Đó là cách xử lý đặc biệt của trường hợp "Thêm" có mùi, buộc người đọc phải giải thích lý do tại sao nó có mùi và cuối cùng dẫn đến một số độc giả của mã để kết luận rằng nó phải vi phạm SRP.

Kho lưu trữ "thông báo" có thể được truy vấn, thay đổi và kích hoạt các sự kiện khi thay đổi, là một đối tượng hoàn toàn bình thường. Bạn có thể mong đợi để tìm thấy nhiều biến thể của chúng trong hầu hết các dự án có kích thước vừa phải.


Nhưng một kho lưu trữ "thông báo" có thực sự là những gì bạn cần không? Bạn đã đề cập đến C #: Nhiều người sẽ đồng ý rằng sử dụng System.Collections.ObjectModel.ObservableCollection<>thay vì System.Collections.Generic.List<>khi tất cả những gì bạn cần là tất cả các loại xấu và sai, nhưng ít người sẽ ngay lập tức chỉ ra SRP.

Những gì bạn đang làm bây giờ là trao đổi UserList _userRepositoryvới một ObservableUserCollection _userRepository. Nếu đó là cách hành động tốt nhất hay không phụ thuộc vào ứng dụng. Nhưng trong khi nó chắc chắn làm cho _userRepositorytrọng lượng nhẹ hơn đáng kể, theo ý kiến ​​khiêm tốn của tôi, nó không vi phạm SRP.


Vấn đề với việc sử dụng ObservableCollectioncho trường hợp này là, nó kích hoạt sự kiện tương đương không phải ở cuộc gọi đến SaveChanges, mà tại cuộc gọi đến Add, điều này sẽ dẫn đến một hành vi rất khác so với sự kiện được hiển thị trong ví dụ. Xem câu trả lời của tôi làm thế nào để giữ cho repo ban đầu nhẹ, và vẫn bám sát SRP bằng cách giữ nguyên các ngữ nghĩa.
Doc Brown

@DocBrown Tôi đã gọi các lớp đã biết ObservableCollection<>List<>để so sánh và bối cảnh. Tôi không có ý khuyên bạn nên sử dụng các lớp thực tế cho việc triển khai bên trong hoặc giao diện bên ngoài.
Peter

Ok, nhưng ngay cả khi OP sẽ thêm các sự kiện vào "Sửa đổi" và "Xóa" (mà tôi nghĩ rằng OP bỏ qua để giữ cho câu hỏi ngắn gọn, vì đơn giản), tôi nghĩ rằng một nhà đánh giá có thể dễ dàng đưa ra kết luận về vi phạm SRP. Nó có thể là một chấp nhận được, nhưng không ai có thể giải quyết nếu được yêu cầu.
Doc Brown

16

Có, đó là vi phạm nguyên tắc trách nhiệm duy nhất và một điểm hợp lệ.

Một thiết kế tốt hơn sẽ có một quy trình riêng lấy 'người dùng mới' từ kho lưu trữ và gửi email. Theo dõi những người dùng đã được gửi email, thất bại, gửi lại, v.v., v.v.

Bằng cách này, bạn có thể xử lý lỗi, sự cố và tương tự cũng như tránh kho lưu trữ của bạn nắm bắt mọi yêu cầu có ý tưởng rằng các sự kiện xảy ra "khi một cái gì đó được cam kết với cơ sở dữ liệu".

Kho lưu trữ không biết rằng người dùng bạn thêm là người dùng mới. Trách nhiệm của nó chỉ đơn giản là lưu trữ người dùng.

Có lẽ nó đáng để mở rộng trên các ý kiến ​​dưới đây.

Kho lưu trữ không biết rằng người dùng bạn thêm là người dùng mới - đúng vậy, nó có một phương thức gọi là Thêm. Ngữ nghĩa của nó ngụ ý rằng tất cả người dùng được thêm là người dùng mới. Kết hợp tất cả các đối số được truyền cho Thêm trước khi gọi Lưu - và bạn có được tất cả người dùng mới

Sai. Bạn đang kết hợp "Đã thêm vào Kho lưu trữ" và "Mới".

"Đã thêm vào Kho lưu trữ" có nghĩa là những gì nó nói. Tôi có thể thêm và xóa và thêm lại người dùng vào các kho khác nhau.

"Mới" là trạng thái của người dùng được xác định bởi các quy tắc kinh doanh.

Hiện tại quy tắc kinh doanh có thể là "Mới == vừa được thêm vào kho lưu trữ", nhưng điều đó không có nghĩa là không có trách nhiệm riêng biệt để biết và áp dụng quy tắc đó.

Bạn phải cẩn thận để tránh kiểu suy nghĩ tập trung vào cơ sở dữ liệu này. Bạn sẽ có các quy trình xử lý cạnh có thêm người dùng không mới vào kho lưu trữ và khi bạn gửi email cho họ, tất cả doanh nghiệp sẽ nói "Tất nhiên đó không phải là người dùng 'mới'! Quy tắc thực tế là X"

Câu trả lời này là IMHO khá thiếu điểm: repo chính xác là một vị trí trung tâm trong mã biết khi nào người dùng mới được thêm vào

Sai. Vì những lý do trên, cộng với đó không phải là một vị trí trung tâm trừ khi bạn thực sự bao gồm mã gửi email trong lớp thay vì chỉ đưa ra một sự kiện.

Bạn sẽ có các ứng dụng sử dụng lớp kho lưu trữ, nhưng không có mã để gửi email. Khi bạn thêm người dùng trong các ứng dụng đó, email sẽ không được gửi.


11
Kho lưu trữ không biết rằng người dùng bạn thêm là người dùng mới - đúng vậy, nó có một phương thức được gọi Add. Ngữ nghĩa của nó ngụ ý rằng tất cả người dùng được thêm là người dùng mới. Kết hợp tất cả các đối số được chuyển đến Addtrước khi gọi Save- và bạn có được tất cả người dùng mới.
Andre Borges

Tôi thích đề nghị này. Tuy nhiên, chủ nghĩa thực dụng chiếm ưu thế hơn độ tinh khiết. Tùy thuộc vào hoàn cảnh, việc thêm một lớp kiến ​​trúc hoàn toàn mới vào một ứng dụng hiện có có thể khó biện minh nếu tất cả những gì bạn cần làm chỉ là gửi một email khi người dùng được thêm vào.
Alexander

Nhưng sự kiện không nói người dùng thêm vào. Nó nói người dùng tạo ra. Nếu chúng tôi xem xét việc đặt tên đúng và chúng tôi đồng ý với sự khác biệt về ngữ nghĩa giữa thêm và tạo, thì sự kiện trong đoạn trích bị đặt tên sai hoặc bị đặt sai. Tôi không nghĩ rằng người đánh giá có bất cứ điều gì chống lại các kho lưu trữ. Có lẽ nó quan tâm đến loại sự kiện và tác dụng phụ của nó.
Laiv

7
@Andre Mới đối với repo, nhưng không nhất thiết là "mới" trong ý nghĩa kinh doanh. sự kết hợp của hai ý tưởng này đã che giấu trách nhiệm thêm từ cái nhìn đầu tiên. Tôi có thể nhập một tấn người dùng cũ vào kho lưu trữ mới của mình hoặc xóa và thêm lại người dùng, v.v. Sẽ có các quy tắc kinh doanh về "người dùng mới" vượt quá "đã được thêm vào dB"
Ewan

1
Người điều hành Lưu ý: Câu trả lời của bạn không phải là một cuộc phỏng vấn báo chí. Nếu bạn có các chỉnh sửa, hãy kết hợp chúng một cách tự nhiên vào câu trả lời của bạn mà không tạo ra toàn bộ hiệu ứng "tin nóng". Chúng tôi không phải là một diễn đàn thảo luận.
Robert Harvey

7

Đây có phải là một điểm hợp lệ?

Đúng vậy, mặc dù nó phụ thuộc rất nhiều vào cấu trúc mã của bạn. Tôi không có bối cảnh đầy đủ vì vậy tôi sẽ cố gắng nói chung.

Dường như với tôi rằng việc nâng cao một sự kiện ở đây về cơ bản giống như ghi nhật ký: chỉ cần thêm một số chức năng phụ vào chức năng.

Nó hoàn toàn không. Ghi nhật ký không phải là một phần của quy trình kinh doanh, nó có thể bị vô hiệu hóa, nó không gây ra tác dụng phụ (kinh doanh) và không ảnh hưởng đến trạng thái và sức khỏe của ứng dụng của bạn theo bất kỳ cách nào, ngay cả khi bạn vì một số lý do không thể đăng nhập Bất cứ điều gì nữa. Bây giờ so sánh với logic bạn đã thêm.

Và SRP không cấm bạn sử dụng các sự kiện ghi nhật ký hoặc bắn trong các chức năng của mình, nó chỉ nói rằng logic đó nên được gói gọn trong các lớp khác và việc kho lưu trữ gọi các lớp khác này là ổn.

SRP hoạt động song song với ISP (S và I trong RẮN). Bạn kết thúc với nhiều lớp và phương thức làm những việc rất cụ thể và không có gì khác. Họ rất tập trung, rất dễ dàng để cập nhật hoặc thay thế, và nói chung dễ dàng (er) để kiểm tra. Tất nhiên trong thực tế, bạn cũng sẽ có một vài lớp lớn hơn liên quan đến việc điều phối: họ sẽ có một số phụ thuộc và họ sẽ không tập trung vào các hành động nguyên tử, mà là các hành động kinh doanh, có thể cần nhiều bước. Miễn là bối cảnh kinh doanh rõ ràng, chúng cũng có thể được gọi là trách nhiệm đơn lẻ, nhưng như bạn đã nói một cách chính xác, khi mã phát triển, bạn có thể muốn trừu tượng một số nó thành các lớp / giao diện mới.

Bây giờ trở lại ví dụ cụ thể của bạn. Nếu bạn hoàn toàn phải gửi thông báo bất cứ khi nào người dùng được tạo và thậm chí có thể thực hiện các hành động chuyên biệt khác, thì bạn có thể tạo một dịch vụ riêng đóng gói yêu cầu này, như UserCreationServicemột phương thức, Add(user)xử lý cả phương thức lưu trữ (cuộc gọi vào kho lưu trữ của bạn) và thông báo dưới dạng một hành động kinh doanh. Hoặc làm điều đó trong đoạn trích ban đầu của bạn, sau_userRepository.SaveChanges();


2
Ghi nhật ký không phải là một phần của quy trình kinh doanh - làm thế nào điều này có liên quan trong bối cảnh của SRP? Nếu mục đích của sự kiện của tôi là gửi dữ liệu người dùng mới tới Google Analytics - thì việc vô hiệu hóa nó sẽ có tác dụng kinh doanh tương tự như vô hiệu hóa ghi nhật ký: không quan trọng, nhưng khá khó chịu. Quy tắc của ngón tay cái để thêm / không thêm logic mới vào hàm là gì? "Sẽ vô hiệu hóa nó gây ra tác dụng phụ kinh doanh lớn?"
Andre Borges

2
If the purpose of my event would be to send new user data to Google Analytics - then disabling it would have the same business effect as disabling logging: not critical, but pretty upsetting . Điều gì sẽ xảy ra nếu bạn đang bắn các sự kiện sớm gây ra "tin tức" giả mạo. Điều gì xảy ra nếu các phân tích đang tính đến "người dùng" cuối cùng không được tạo do lỗi với giao dịch DB? Điều gì xảy ra nếu công ty đưa ra quyết định dựa trên cơ sở sai, được hỗ trợ bởi dữ liệu không chính xác? Bạn quá tập trung vào khía cạnh kỹ thuật của vấn đề. "Đôi khi bạn không thể nhìn thấy gỗ cho cây '"
Laiv

@Laiv, bạn đang đưa ra một điểm hợp lệ, nhưng đây không phải là điểm của câu hỏi của tôi, hoặc câu trả lời này. Câu hỏi đặt ra là liệu đây có phải là một giải pháp hợp lệ trong bối cảnh SRP hay không, vì vậy hãy giả sử không có lỗi giao dịch DB.
Andre Borges

Về cơ bản bạn đang yêu cầu tôi nói với bạn những gì bạn muốn nghe. Tôi chỉ cung cấp cho bạn phạm vi. Phạm vi rộng hơn để bạn quyết định liệu SRP có quan trọng hay không vì SRP là vô dụng nếu không có bối cảnh thích hợp. IMO theo cách bạn đang tiếp cận mối quan tâm là không chính xác vì bạn chỉ tập trung vào giải pháp kỹ thuật. Bạn nên cung cấp đủ liên quan đến toàn bộ bối cảnh. Và vâng, DB có thể thất bại. Có một cơ hội cho nó xảy ra và bạn không nên bỏ qua điều đó, bởi vì như bạn biết, mọi thứ xảy ra và những điều này có thể thay đổi suy nghĩ của bạn về những nghi ngờ về SRP hoặc các thực tiễn tốt khác.
Laiv

1
Điều đó nói rằng, hãy nhớ rằng các nguyên tắc không phải là quy tắc được viết bằng đá. Chúng được thẩm thấu (thích nghi). Như bạn có thể thấy, chúng được mở để giải thích. Người đánh giá của bạn có một giải thích và bạn có một giải thích khác. Hãy thử xem những gì bạn nhìn thấy, giải quyết những nghi ngờ và lo lắng của anh ấy / cô ấy hoặc để anh ấy / cô ấy giải quyết vấn đề của bạn. Bạn sẽ không tìm thấy câu trả lời "đúng" ở đây. Câu trả lời đúng là tùy thuộc vào bạn và người đánh giá của bạn để tìm, trước tiên hãy hỏi các yêu cầu (chức năng và không chức năng) của dự án.
Laiv

4

Về mặt lý thuyết, SRP là về con người , như chú Bob giải thích trong bài viết của ông Nguyên tắc trách nhiệm duy nhất . Cảm ơn Robert Harvey đã cung cấp nó trong bình luận của bạn.

Câu hỏi đúng là:

"Các bên liên quan" nào đã thêm yêu cầu "gửi email"?

Nếu các bên liên quan đó cũng chịu trách nhiệm về sự kiên trì dữ liệu (không thể nhưng có thể) thì điều này không vi phạm SRP. Nếu không, nó làm.


2
Thú vị - Tôi chưa bao giờ nghe về cách giải thích này của SRP. Bạn có bất kỳ gợi ý để biết thêm thông tin / tài liệu về giải thích này?
sleske

2
@sleske: Từ chính chú Bob : "Và điều này đi đến mấu chốt của Nguyên tắc Trách nhiệm duy nhất. 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 chắc chắn 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 duy nhất, hay đúng hơn, một nhóm người kết hợ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. "
Robert Harvey

Cảm ơn Robert. IMO, Cái tên "Nguyên tắc trách nhiệm duy nhất" thật tồi tệ, vì nghe có vẻ đơn giản, nhưng quá ít người theo dõi ý nghĩa dự định của "trách nhiệm" là gì. Kiểu như OOP đã biến đổi từ nhiều khái niệm ban đầu của nó, và bây giờ là một thuật ngữ khá vô nghĩa.
dùng949300

1
Vâng. Đó là những gì đã xảy ra với thuật ngữ REST. Ngay cả Roy Fielding nói rằng mọi người đang sử dụng nó sai.
Robert Harvey

Mặc dù trích dẫn có liên quan, tôi nghĩ rằng câu trả lời này bỏ lỡ rằng yêu cầu "gửi email" không phải là một trong những yêu cầu trực tiếp mà câu hỏi vi phạm SRP hướng tới. Tuy nhiên, bằng cách nói "Mà" các bên liên quan "đã thêm yêu cầu" tăng sự kiện " , câu trả lời này sẽ trở nên liên quan nhiều hơn đến câu hỏi thực tế. Tôi đã sửa đổi câu trả lời của mình một chút để làm cho điều này rõ ràng hơn.
Doc Brown

2

Mặc dù về mặt kỹ thuật không có gì sai với các kho lưu trữ thông báo các sự kiện, tôi sẽ đề nghị xem xét nó từ quan điểm chức năng, nơi sự tiện lợi của nó làm tăng một số lo ngại.

Tạo người dùng, quyết định người dùng mới là gì và sự kiên trì của họ là 3 điều khác nhau .

Tiền đề của tôi

Hãy xem xét tiền đề trước đó trước khi quyết định xem kho lưu trữ có phải là nơi thích hợp để thông báo cho các sự kiện kinh doanh hay không (bất kể SRP). Lưu ý rằng tôi đã nói sự kiện kinh doanh vì với tôi UserCreatedcó ý nghĩa khác với UserStoredhoặc UserAdded 1 . Tôi cũng sẽ xem xét từng sự kiện được giải quyết cho các đối tượng khác nhau.

Một mặt, tạo người dùng là một quy tắc dành riêng cho doanh nghiệp có thể có hoặc không liên quan đến sự kiên trì. Nó có thể liên quan đến nhiều hoạt động kinh doanh hơn, liên quan đến nhiều hoạt động cơ sở dữ liệu / mạng hơn. Hoạt động lớp kiên trì là không biết. Lớp kiên trì không có đủ ngữ cảnh để quyết định xem trường hợp sử dụng đã kết thúc thành công hay chưa.

Mặt khác, nó không nhất thiết đúng mà _dataContext.SaveChanges();vẫn tồn tại thành công cho người dùng. Nó sẽ phụ thuộc vào nhịp giao dịch của cơ sở dữ liệu. Ví dụ, điều đó có thể đúng đối với các cơ sở dữ liệu như MongoDB, các giao dịch là nguyên tử, nhưng không thể, đối với RDBMS truyền thống thực hiện các giao dịch ACID khi có thể có nhiều giao dịch hơn và chưa được cam kết.

Đây có phải là một điểm hợp lệ?

Nó có thể là. Tuy nhiên, tôi dám nói rằng đó không chỉ là vấn đề của SRP (nói về mặt kỹ thuật), mà còn là vấn đề thuận tiện (nói theo chức năng).

  • Có thuận tiện để bắn các sự kiện kinh doanh từ các thành phần không biết về hoạt động kinh doanh đang diễn ra không?
  • Họ có đại diện đúng nơi nhiều như thời điểm thích hợp để làm điều đó không?
  • Tôi có nên cho phép các thành phần này phối hợp logic kinh doanh của mình thông qua các thông báo như thế này không?
  • Tôi có thể làm mất hiệu lực các tác dụng phụ gây ra bởi các sự kiện sớm? 2

Dường như với tôi rằng việc gây ra một sự kiện ở đây về cơ bản giống như đăng nhập

Tuyệt đối không. Ghi nhật ký có nghĩa là không có tác dụng phụ, tuy nhiên, như bạn đề xuất, sự kiện UserCreatednày có thể khiến các hoạt động kinh doanh khác xảy ra. Giống như thông báo. 3

nó chỉ nói rằng logic như vậy nên được gói gọn trong các lớp khác và việc một kho lưu trữ gọi các lớp khác này là ổn

Không nhất thiết phải đúng. SRP không phải là mối quan tâm cụ thể của lớp. Nó hoạt động ở các mức độ trừu tượng khác nhau, như các lớp, thư viện và hệ thống! Đó là về sự gắn kết, về việc cùng nhau giữ những gì thay đổi vì những lý do tương tự do bàn tay của các bên liên quan . Nếu việc tạo người dùng (ca sử dụng ) thay đổi thì có khả năng thời điểm và lý do cho sự kiện xảy ra cũng thay đổi.


1: Đặt tên đầy đủ cũng có vấn đề.

2: Giả sử chúng tôi đã gửi UserCreatedsau _dataContext.SaveChanges();, nhưng toàn bộ giao dịch cơ sở dữ liệu bị lỗi sau đó do sự cố kết nối hoặc vi phạm ràng buộc. Hãy cẩn thận với việc phát sóng sớm các sự kiện, vì các tác dụng phụ của nó có thể khó hoàn tác (nếu điều đó thậm chí có thể xảy ra).

3: Các quy trình thông báo không được xử lý đầy đủ có thể khiến bạn kích hoạt các thông báo không thể hoàn tác / sup>


1
+1 Điểm rất tốt về khoảng thời gian giao dịch. Nó có thể là sớm để khẳng định người dùng đã được tạo, bởi vì rollback có thể xảy ra; và không giống như nhật ký, có khả năng một số phần khác của ứng dụng thực hiện điều gì đó với sự kiện này.
Andres F.

2
Chính xác. Sự kiện biểu thị sự nhất định. Một cái gì đó đã xảy ra nhưng nó đã kết thúc.
Laiv

1
@Laiv: Ngoại trừ khi họ không. Microsoft có tất cả các loại sự kiện có tiền tố Beforehoặc Previewkhông đảm bảo chắc chắn về sự chắc chắn.
Robert Harvey

1
@ jpmc26: Không có giải pháp thay thế, đề xuất của bạn không hữu ích.
Robert Harvey

1
@ jpmc26: Vì vậy, câu trả lời của bạn là "thay đổi thành một hệ sinh thái phát triển hoàn toàn khác với một bộ công cụ và đặc điểm hiệu suất hoàn toàn khác". Gọi tôi ngược lại, nhưng tôi sẽ tưởng tượng điều này là không khả thi đối với phần lớn các nỗ lực phát triển.
Robert Harvey

1

Không, điều này không vi phạm SRP.

Nhiều người dường như nghĩ Nguyên tắc Trách nhiệm duy nhất có nghĩa là một chức năng chỉ nên thực hiện "một việc", và sau đó bị cuốn vào cuộc thảo luận về những gì cấu thành "một điều".

Nhưng đó không phải là những gì nguyên tắc có nghĩa. Đó là về mối quan tâm cấp doanh nghiệp. Một lớp không nên thực hiện nhiều mối quan tâm hoặc yêu cầu có thể thay đổi độc lập ở cấp độ kinh doanh. Hãy nói rằng một lớp vừa lưu trữ người dùng và gửi tin nhắn chào mừng được mã hóa cứng qua email. Nhiều mối quan tâm độc lập có thể khiến các yêu cầu của một lớp như vậy thay đổi. Nhà thiết kế có thể yêu cầu html / biểu định kiểu của thư để thay đổi. Chuyên gia truyền thông có thể yêu cầu từ ngữ của thư để thay đổi. Và chuyên gia UX có thể quyết định thư thực sự nên được gửi tại một điểm khác trong luồng lên máy bay. Vì vậy, lớp có thể thay đổi nhiều yêu cầu từ các nguồn độc lập. Điều này vi phạm SRP.

Nhưng việc bắn một sự kiện không vi phạm SRP sau đó, vì sự kiện chỉ phụ thuộc vào việc cứu người dùng chứ không phụ thuộc vào bất kỳ mối quan tâm nào khác. Các sự kiện thực sự là một cách thực sự tốt để duy trì SRP, vì bạn có thể có email được kích hoạt bởi lưu mà không bị lưu trữ bị ảnh hưởng bởi - hoặc thậm chí biết về - thư.


1

Đừng lo lắng về nguyên tắc trách nhiệm duy nhất. Điều này sẽ không giúp bạn đưa ra quyết định tốt ở đây vì bạn có thể chủ quan chọn một khái niệm cụ thể là "trách nhiệm". Bạn có thể nói trách nhiệm của lớp là quản lý tính bền vững của dữ liệu đối với cơ sở dữ liệu hoặc bạn có thể nói trách nhiệm của lớp là thực hiện tất cả các công việc liên quan đến việc tạo người dùng. Đây chỉ là các cấp độ khác nhau trong hành vi của ứng dụng và cả hai đều là biểu hiện khái niệm hợp lệ của một "trách nhiệm duy nhất". Vì vậy, nguyên tắc này là không có ích cho việc giải quyết vấn đề của bạn.

Nguyên tắc hữu ích nhất để áp dụng trong trường hợp này là nguyên tắc ít gây bất ngờ nhất . Vì vậy, hãy đặt câu hỏi: có ngạc nhiên khi một kho lưu trữ với vai trò chính là lưu trữ dữ liệu vào cơ sở dữ liệu cũng gửi e-mail không?

Vâng, nó rất nhiều là đáng ngạc nhiên. Đây là hai hệ thống bên ngoài hoàn toàn riêng biệt và tên SaveChangesnày cũng không ngụ ý gửi thông báo. Việc bạn ủy thác điều này cho một sự kiện làm cho hành vi trở nên đáng ngạc nhiên hơn, vì ai đó đang đọc mã có thể không còn dễ dàng nhìn thấy những hành vi bổ sung nào được gọi. Vô cảm gây hại cho khả năng đọc. Đôi khi, các lợi ích đáng giá cho chi phí dễ đọc, nhưng không phải khi bạn tự động gọi thêm một hệ thống bên ngoài có tác dụng quan sát được cho người dùng cuối. (Logging thể được loại trừ ở đây kể từ khi tác dụng của nó là về cơ bản hồ sơ lưu giữ cho mục đích gỡ lỗi. End người dùng không tiêu thụ nhật ký, vì vậy không có hại trong luôn đăng nhập.) Thậm chí tệ hơn, điều này làm giảm tính linh hoạt trong thời gian gửi e-mail, khiến cho không thể xen kẽ các hoạt động khác giữa lưu và thông báo.

Nếu mã của bạn thường cần gửi thông báo khi người dùng được tạo thành công, bạn có thể tạo một phương thức như vậy:

public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
    repo.Add(user);
    repo.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

Nhưng việc này có làm tăng giá trị hay không phụ thuộc vào chi tiết cụ thể của ứng dụng của bạn.


Tôi thực sự không khuyến khích sự tồn tại của SaveChangesphương pháp này. Phương pháp này có thể sẽ cam kết một giao dịch cơ sở dữ liệu, nhưng các kho lưu trữ khác có thể đã sửa đổi cơ sở dữ liệu trong cùng một giao dịch . Thực tế là nó cam kết tất cả chúng là một lần nữa đáng ngạc nhiên, vì SaveChangesđặc biệt gắn liền với trường hợp này của kho lưu trữ người dùng.

Mẫu đơn giản nhất để quản lý giao dịch cơ sở dữ liệu là một usingkhối bên ngoài :

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    context.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

Điều này cho phép lập trình viên kiểm soát rõ ràng khi thay đổi tất cả các kho lưu trữ, buộc mã phải ghi lại một cách rõ ràng chuỗi các sự kiện phải xảy ra trước khi cam kết, đảm bảo khôi phục được đưa ra do lỗi (giả sử rằng có sự DataContext.Disposecố quay ngược lại) và tránh bị ẩn kết nối giữa các lớp nhà nước.

Tôi cũng không muốn gửi e-mail trực tiếp trong yêu cầu. Sẽ mạnh mẽ hơn khi ghi lại nhu cầu thông báo trong hàng đợi. Điều này sẽ cho phép xử lý thất bại tốt hơn. Đặc biệt, nếu xảy ra lỗi khi gửi e-mail, nó có thể được thử lại sau mà không làm gián đoạn việc lưu người dùng và tránh trường hợp người dùng được tạo nhưng lỗi được trang web trả về.

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    _emailNotificationQueue.AddUserCreateNotification(user);
    _emailNotificationQueue.Commit();
    context.SaveChanges();
}

Trước tiên, tốt nhất là cam kết hàng đợi thông báo vì người tiêu dùng của hàng đợi có thể xác minh rằng người dùng tồn tại trước khi gửi e-mail, trong trường hợp context.SaveChanges()cuộc gọi thất bại. (Nếu không, bạn sẽ cần một chiến lược cam kết hai giai đoạn đầy đủ để tránh heurrbugs.)


Điểm mấu chốt là phải thực tế. Thực tế suy nghĩ thông qua các hậu quả (cả về rủi ro và lợi ích) của việc viết mã theo một cách cụ thể. Tôi thấy rằng "nguyên tắc trách nhiệm duy nhất" không thường xuyên giúp tôi làm điều đó, trong khi "nguyên tắc ít bất ngờ nhất" thường giúp tôi đi vào đầu một nhà phát triển khác (có thể nói) và suy nghĩ về những gì có thể xảy ra.


4
thật đáng ngạc nhiên khi một kho lưu trữ với vai trò chính là lưu dữ liệu vào cơ sở dữ liệu cũng gửi e-mail - tôi nghĩ rằng bạn đã bỏ lỡ điểm của câu hỏi của tôi. Kho lưu trữ của tôi không gửi email. Nó chỉ đưa ra một sự kiện và cách xử lý sự kiện này - là trách nhiệm của mã bên ngoài.
Andre Borges

4
Về cơ bản, bạn đang đưa ra lập luận "không sử dụng các sự kiện."
Robert Harvey

3
[nhún] Sự kiện là trung tâm của hầu hết các khung UI. Loại bỏ các sự kiện và các khung đó hoàn toàn không hoạt động.
Robert Harvey

2
@ jpmc26: Nó được gọi là ASP.NET Webforms. Nó thật tệ
Robert Harvey

2
My repository is not sending emails. It just raises an eventnhân quả. Các kho lưu trữ đang kích hoạt quá trình thông báo.
Laiv

0

Hiện tại SaveChangeshai điều: nó lưu các thay đổi nhật ký mà nó làm như vậy. Bây giờ bạn muốn thêm một thứ nữa vào nó: gửi thông báo email.

Bạn đã có ý tưởng thông minh để thêm một sự kiện vào nó, nhưng điều này đã bị chỉ trích vì vi phạm Nguyên tắc Trách nhiệm duy nhất (SRP), mà không nhận thấy rằng nó đã bị vi phạm.

Để có được giải pháp SRP thuần túy, trước tiên hãy kích hoạt sự kiện, sau đó gọi tất cả các hook cho sự kiện đó, hiện có ba: lưu, ghi nhật ký và cuối cùng là gửi email.

Hoặc bạn kích hoạt sự kiện trước hoặc bạn phải thêm vào SaveChanges. Giải pháp của bạn là lai giữa hai. Nó không giải quyết vi phạm hiện có trong khi nó khuyến khích ngăn chặn nó gia tăng vượt quá ba điều. Tái cấu trúc mã hiện có để tuân thủ SRP có thể đòi hỏi nhiều công việc hơn là rất cần thiết. Tùy thuộc vào dự án của bạn, họ muốn dùng SRP bao xa.


-1

Mã đã vi phạm SRP - cùng một lớp chịu trách nhiệm giao tiếp với bối cảnh dữ liệu và ghi nhật ký.

Bạn chỉ cần nâng cấp nó để có 3 trách nhiệm.

Một cách để loại bỏ mọi thứ trở lại 1 trách nhiệm sẽ là trừu tượng hóa _userRepository; làm cho nó trở thành một đài truyền hình chỉ huy

Nó có một bộ lệnh, cộng với một bộ người nghe. Nó nhận được lệnh và phát chúng cho người nghe. Có thể những người nghe được ra lệnh, và thậm chí họ có thể nói rằng lệnh thất bại (lần lượt được phát cho những người nghe đã được thông báo).

Bây giờ, hầu hết các lệnh có thể chỉ có 1 người nghe (bối cảnh dữ liệu). SaveChanges, trước các thay đổi của bạn, có 2 - bối cảnh dữ liệu và sau đó là bộ ghi.

Thay đổi của bạn sau đó thêm một trình lắng nghe khác để lưu các thay đổi, đó là nâng cao các sự kiện do người dùng mới tạo trong dịch vụ sự kiện.

Có một vài lợi ích cho việc này. Bây giờ bạn có thể xóa, nâng cấp hoặc sao chép mã đăng nhập mà không cần phần còn lại của mã. Bạn có thể thêm nhiều kích hoạt tại các thay đổi lưu cho nhiều thứ cần hơn.

Tất cả điều này được quyết định khi _userRepositoryđược tạo và nối dây (hoặc, có thể, các tính năng bổ sung đó được thêm / xóa ngay lập tức; có thể thêm / tăng cường ghi nhật ký trong khi ứng dụng chạy có thể được sử dụng).

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.