Việc thêm một kiểu trả về vào một phương thức cập nhật có vi phạm Nguyên tắc Trách nhiệm Đơn lẻ không?


37

Tôi có một phương pháp cập nhật dữ liệu nhân viên trong cơ sở dữ liệu. Các Employeelớp học là không thay đổi, vì vậy "cập nhật" phương tiện đối tượng thực sự để tạo một đối tượng mới.

Tôi muốn Updatephương thức trả về một thể hiện mới Employeevới dữ liệu được cập nhật, nhưng từ giờ tôi có thể nói trách nhiệm của phương thức là cập nhật dữ liệu nhân viên lấy từ cơ sở dữ liệu một Employeeđối tượng mới , nó có vi phạm Nguyên tắc Trách nhiệm duy nhất không?

Bản ghi DB được cập nhật. Sau đó, một đối tượng mới được khởi tạo để thể hiện bản ghi này.


5
Một mã giả nhỏ có thể đi một chặng đường dài ở đây: Thực sự có một bản ghi DB mới được tạo ra hay bản ghi DB được cập nhật, nhưng trong mã "máy khách", một đối tượng mới được tạo vì lớp được mô hình hóa là bất biến?
Martin Ba

Ngoài những gì Martin Bra đã hỏi, tại sao bạn lại trả về một nhân viên khi bạn đang cập nhật cơ sở dữ liệu? Các giá trị của cá thể Nhân viên không thay đổi trong phương thức Cập nhật, vậy tại sao lại trả về nó (người gọi đã có quyền truy cập vào thể hiện ...). Hoặc phương thức cập nhật cũng lấy các giá trị (có khả năng khác nhau) từ cơ sở dữ liệu?
Thorsal

1
@Thorsal: nếu các thực thể dữ liệu của bạn là bất biến, trả về một thể hiện với dữ liệu được cập nhật là khá nhiều SOP, vì nếu không, bạn phải tự thực hiện việc khởi tạo dữ liệu đã sửa đổi.
mikołak

21
Compare-and-swaptest-and-setlà các hoạt động cơ bản trong lý thuyết lập trình đa luồng. Cả hai đều là phương thức cập nhật với kiểu trả về và không thể hoạt động khác. Điều này có phá vỡ các khái niệm như phân tách truy vấn lệnh hay Nguyên tắc trách nhiệm đơn không? Vâng, và đó là toàn bộ vấn đề . SRP không phải là một điều tốt trên toàn cầu, và trên thực tế có thể gây hại tích cực.
MSalters

3
@MSalters: Chính xác. Phân tách lệnh / truy vấn cho biết có thể đưa ra các truy vấn có thể được nhận dạng là idempotent và đưa ra các lệnh mà không phải chờ trả lời, nhưng đọc-sửa đổi-ghi nguyên tử phải được công nhận là một loại hoạt động thứ ba.
supercat

Câu trả lời:


16

Như với bất kỳ quy tắc nào, tôi nghĩ điều quan trọng ở đây là xem xét mục đích của quy tắc, tinh thần và không bị sa lầy trong việc phân tích chính xác cách quy tắc được diễn đạt trong một số sách giáo khoa và cách áp dụng điều đó cho trường hợp này. Chúng ta không cần phải tiếp cận điều này như luật sư. Mục đích của các quy tắc là giúp chúng tôi viết các chương trình tốt hơn. Nó không giống như mục đích của việc viết chương trình là để duy trì các quy tắc.

Mục đích của quy tắc một trách nhiệm là làm cho các chương trình dễ hiểu và duy trì hơn bằng cách làm cho mỗi chức năng thực hiện một điều độc lập, mạch lạc.

Ví dụ, tôi đã từng viết một hàm mà tôi gọi là "checkOrderStatus", xác định xem một đơn đặt hàng đang chờ xử lý, vận chuyển, đặt hàng lại, bất cứ điều gì và trả lại mã cho biết. Sau đó, một lập trình viên khác đã xuất hiện và sửa đổi chức năng này để cập nhật số lượng trong tay khi đơn hàng được chuyển đi. Điều này vi phạm nghiêm trọng nguyên tắc trách nhiệm duy nhất. Một lập trình viên khác đọc mã đó sau đó sẽ thấy tên hàm, xem cách sử dụng giá trị trả về và có thể không bao giờ nghi ngờ rằng nó đã cập nhật cơ sở dữ liệu. Một người nào đó cần có trạng thái đơn hàng mà không cập nhật số lượng trong tay sẽ ở trong một vị trí khó xử: anh ta có nên viết một chức năng mới sao chép phần trạng thái đơn hàng không? Thêm một cờ để cho nó biết có thực hiện cập nhật db không? V.v.

Mặt khác, tôi sẽ không tiết lộ những gì cấu thành "hai điều". Gần đây tôi đã viết một chức năng gửi thông tin khách hàng từ hệ thống của chúng tôi đến hệ thống của khách hàng. Hàm đó thực hiện một số định dạng lại dữ liệu để đáp ứng yêu cầu của họ. Ví dụ: chúng tôi có một số trường có thể là null trên cơ sở dữ liệu của chúng tôi, nhưng chúng không cho phép null nên chúng tôi phải điền vào một số văn bản giả, "không được chỉ định" hoặc tôi quên các từ chính xác. Có thể cho rằng chức năng này đang thực hiện hai điều: định dạng lại dữ liệu VÀ gửi nó. Nhưng tôi rất cố tình đặt điều này trong một chức năng duy nhất thay vì "định dạng lại" và "gửi" bởi vì tôi không muốn, không bao giờ gửi mà không định dạng lại. Tôi không muốn ai đó viết một cuộc gọi mới và không nhận ra anh ta phải gọi lại định dạng và sau đó gửi.

Trong trường hợp của bạn, cập nhật cơ sở dữ liệu và trả lại hình ảnh của bản ghi được viết có vẻ như hai điều có thể đi cùng nhau một cách hợp lý và chắc chắn. Tôi không biết chi tiết về ứng dụng của bạn vì vậy tôi không thể nói dứt khoát nếu đây là một ý tưởng tốt hay không, nhưng nghe có vẻ hợp lý.

Nếu bạn đang tạo một đối tượng trong bộ nhớ chứa tất cả dữ liệu cho bản ghi, thực hiện các cuộc gọi cơ sở dữ liệu để ghi điều này và sau đó trả lại đối tượng, điều này rất có ý nghĩa. Bạn có đối tượng trong tay của bạn. Tại sao không chỉ đưa nó trở lại? Nếu bạn không trả lại đối tượng, làm thế nào người gọi có được nó? Anh ta có phải đọc cơ sở dữ liệu để lấy đối tượng bạn vừa viết không? Điều đó có vẻ khá không hiệu quả. Làm thế nào anh ta sẽ tìm thấy hồ sơ? Bạn có biết khóa chính? Nếu ai đó tuyên bố rằng "hợp pháp" cho chức năng ghi trả về khóa chính để bạn có thể đọc lại hồ sơ, tại sao không trả lại toàn bộ hồ sơ để bạn không phải làm vậy? Có gì khác biệt?

Mặt khác, nếu việc tạo đối tượng là một nhóm công việc khá khác biệt so với việc viết bản ghi cơ sở dữ liệu và một người gọi có thể muốn thực hiện ghi nhưng không tạo đối tượng, thì điều này có thể lãng phí. Nếu một người gọi có thể muốn đối tượng nhưng không thực hiện ghi, thì bạn phải cung cấp một cách khác để có được đối tượng, điều đó có nghĩa là viết mã dự phòng.

Nhưng tôi nghĩ kịch bản 1 có nhiều khả năng, vì vậy tôi có thể nói, có lẽ không có vấn đề gì.


Cảm ơn tất cả các câu trả lời, nhưng điều này thực sự giúp tôi nhiều nhất.
Sipo

Nếu bạn không muốn gửi mà không định dạng lại và không có cách nào để định dạng lại bên cạnh việc gửi dữ liệu sau đó, thì chúng là một thứ chứ không phải hai.
Joker_vD

public function sendDataInClientFormat() { formatDataForClient(); sendDataToClient(); } private function formatDataForClient() {...} private function sendDataToClient() {...}
CJ Dennis

@CJDennis Chắc chắn. Và trên thực tế, đó là cách tôi đã thực hiện: Một chức năng thực hiện định dạng, một chức năng để gửi thực tế và có hai chức năng khác mà tôi sẽ không nhận được ở đây. Sau đó, một hàm cấp cao nhất để gọi tất cả chúng theo trình tự thích hợp. Bạn có thể nói "định dạng và gửi" là một thao tác logic và do đó có thể được kết hợp đúng trong một chức năng. Nếu bạn khăng khăng rằng đó là hai, ok, vậy thì điều hợp lý cần làm là có một chức năng cấp cao nhất làm tất cả.
Jay

67

Khái niệm về SRP là dừng các mô-đun làm 2 việc khác nhau khi thực hiện chúng riêng lẻ để bảo trì tốt hơn và ít mì spaghetti kịp thời. Như SRP nói "một lý do để thay đổi".

Trong trường hợp của bạn, nếu bạn có một thói quen "cập nhật và trả về đối tượng được cập nhật", bạn vẫn thay đổi đối tượng một lần - cho nó 1 lý do để thay đổi. Rằng bạn trả lại đối tượng ngay không phải ở đây cũng không ở đó, bạn vẫn đang hoạt động trên đối tượng đó. Mã có trách nhiệm cho một, và chỉ một, điều.

SRP không thực sự là về việc cố gắng giảm tất cả các hoạt động trong một cuộc gọi, mà là để giảm những gì bạn đang hoạt động và cách bạn đang vận hành trên đó. Vì vậy, một bản cập nhật duy nhất trả về đối tượng được cập nhật là tốt.


7
Ngoài ra, đó không phải là "về việc cố gắng giảm tất cả các hoạt động trong một cuộc gọi", mà là giảm bao nhiêu trường hợp khác nhau mà bạn phải suy nghĩ khi sử dụng mục được đề cập.
jpmc26

46

Như mọi khi, đây là một câu hỏi về mức độ. SRP sẽ ngăn bạn viết một phương thức lấy bản ghi từ cơ sở dữ liệu bên ngoài, thực hiện chuyển đổi Fourier nhanh trên đó và cập nhật kết quả đăng ký thống kê toàn cầu. Tôi nghĩ rằng hầu hết mọi người sẽ đồng ý những điều này nên được thực hiện bằng các phương pháp khác nhau. Đưa ra một trách nhiệm duy nhất cho mỗi phương pháp đơn giản là cách tiết kiệm và đáng nhớ nhất để đưa ra quan điểm đó.

Ở đầu kia của quang phổ là các phương thức mang lại thông tin về trạng thái của một đối tượng. Các điển hình isActiveđã cung cấp thông tin này là trách nhiệm duy nhất của nó. Có lẽ mọi người đều đồng ý rằng điều này là ổn.

Bây giờ, một số người mở rộng nguyên tắc cho đến nay họ coi việc trả lại cờ thành công là một trách nhiệm khác với việc thực hiện hành động mà thành công đang được báo cáo. Theo cách giải thích cực kỳ nghiêm ngặt, điều này là đúng, nhưng vì phương án thay thế sẽ phải gọi một phương thức thứ hai để có được trạng thái thành công, làm phức tạp người gọi, nhiều lập trình viên hoàn toàn ổn với việc trả lại mã thành công từ phương thức có tác dụng phụ.

Trả lại đối tượng mới là một bước nữa trên con đường dẫn đến nhiều trách nhiệm. Yêu cầu người gọi thực hiện cuộc gọi thứ hai cho toàn bộ đối tượng là hợp lý hơn một chút so với yêu cầu cuộc gọi thứ hai chỉ để xem liệu cuộc gọi đầu tiên có thành công hay không. Tuy nhiên, nhiều lập trình viên sẽ xem xét trả lại kết quả của một bản cập nhật hoàn toàn tốt. Mặc dù điều này có thể được hiểu là hai trách nhiệm hơi khác nhau, nhưng nó chắc chắn không phải là một trong những lạm dụng nghiêm trọng đã truyền cảm hứng cho nguyên tắc bắt đầu.


15
Nếu bạn phải gọi một phương thức thứ hai để xem liệu phương thức thứ nhất có thành công hay không, bạn có nên gọi phương thức thứ ba để có kết quả của phương pháp thứ hai không? Sau đó, nếu bạn thực sự muốn kết quả, thì ...

3
Tôi có thể thêm rằng ứng dụng SRP quá nhiệt tình dẫn đến vô số các lớp nhỏ, bản thân nó là một gánh nặng. Gánh nặng lớn như thế nào phụ thuộc vào môi trường, trình biên dịch, công cụ IDE / trợ giúp của bạn, v.v.
Erik Alapää

8

Nó có vi phạm Nguyên tắc Trách nhiệm duy nhất không?

Không cần thiết. Nếu bất cứ điều gì, điều này vi phạm nguyên tắc phân tách truy vấn lệnh .

Trách nhiệm là

cập nhật dữ liệu nhân viên

với sự hiểu biết ngầm định rằng trách nhiệm này bao gồm tình trạng của hoạt động; ví dụ: nếu hoạt động thất bại, một ngoại lệ được đưa ra, nếu nó thành công, nhân viên cập nhật được trả về, v.v.

Một lần nữa, đây là tất cả các vấn đề về mức độ và đánh giá chủ quan.


Vì vậy, những gì về tách lệnh truy vấn?

Vâng, nguyên tắc đó tồn tại, nhưng trả về kết quả của một bản cập nhật trên thực tế là rất phổ biến.

(1) Java Set#add(E)thêm phần tử và trả về sự bao gồm trước đó của nó trong tập hợp.

if (visited.add(nodeToVisit)) {
    // perform operation once
}

Điều này hiệu quả hơn so với giải pháp thay thế CQS, có thể phải thực hiện hai lần tra cứu.

if (!visited.contains(nodeToVisit)) {
    visited.add(nodeToVisit);
    // perform operation once
}

(2) So sánh và trao đổi , tìm nạp và thêmkiểm tra và thiết lập là những nguyên thủy phổ biến cho phép lập trình đồng thời tồn tại. Các mẫu này xuất hiện thường xuyên, từ các hướng dẫn CPU cấp thấp đến các bộ sưu tập đồng thời cấp cao.


2

Trách nhiệm duy nhất là về một lớp không cần thay đổi vì nhiều lý do.

Ví dụ, một nhân viên có một danh sách các số điện thoại. Khi cách bạn xử lý số điện thoại thay đổi (bạn có thể thêm mã gọi quốc gia) hoàn toàn không thay đổi lớp nhân viên.

Tôi sẽ không có lớp nhân viên cần biết làm thế nào nó tự lưu vào cơ sở dữ liệu, vì sau đó nó sẽ thay đổi để thay đổi nhân viên và thay đổi cách lưu trữ dữ liệu.

Tương tự như vậy không nên có một phương thức Tính toán trong lớp nhân viên. Một tài sản tiền lương là ok, nhưng việc tính thuế vv nên được thực hiện ở một nơi khác.

Nhưng phương thức Cập nhật trả về những gì nó vừa cập nhật là tốt.


2

Wrt. trường hợp cụ thể:

Employee Update(Employee, name, password, etc) (thực sự sử dụng Builder vì tôi có rất nhiều tham số).

dường như các Updatephương pháp lấy một hiện Employeenhư tham số đầu tiên để xác định (?) Người lao động hiện có và một tập hợp các thông số để thay đổi về nhân viên này.

Tôi làm nghĩ rằng đây có thể được thực hiện rõ ràng hơn. Tôi thấy hai trường hợp:

(a) Employee thực sự có chứa một cơ sở dữ liệu / ID duy nhất mà theo đó nó luôn có thể được xác định trong cơ sở dữ liệu. (Nghĩa là, bạn không cần toàn bộ giá trị bản ghi được đặt để tìm thấy nó trong DB.

Trong trường hợp này, tôi thích một void Update(Employee obj)phương thức, chỉ cần tìm bản ghi hiện có bằng ID và sau đó cập nhật các trường từ đối tượng được truyền. Hoặc có thể là mộtvoid Update(ID, EmployeeBuilder values)

Một biến thể của điều này mà tôi thấy hữu ích là chỉ có một void Put(Employee obj)phương thức chèn hoặc cập nhật, tùy thuộc vào việc bản ghi (theo ID) có tồn tại hay không.

(b) Bản ghi đầy đủ hiện tại là cần thiết cho tra cứu DB, trong trường hợp đó, nó vẫn có thể có ý nghĩa hơn để có : void Update(Employee existing, Employee newData).

Theo như tôi có thể thấy ở đây, tôi thực sự sẽ nói rằng trách nhiệm xây dựng một đối tượng mới (hoặc các giá trị đối tượng phụ) để lưu trữ và thực sự lưu trữ nó là trực giao, vì vậy tôi sẽ tách chúng ra.

Các yêu cầu đồng thời được đề cập trong các câu trả lời khác (nguyên tử set-and-lấy / so sánh-trao đổi, v.v.) không phải là một vấn đề trong mã DB mà tôi đã làm việc cho đến nay. Khi nói chuyện với DB, tôi nghĩ rằng điều này nên được xử lý ở cấp độ giao dịch một cách bình thường, không phải ở cấp độ tuyên bố riêng lẻ. (Điều đó không có nghĩa là có thể không có một thiết kế trong đó "nguyên tử" Employee[existing data] Update(ID, Employee newData)không thể có ý nghĩa, nhưng với DB truy cập thì đó không phải là thứ tôi thường thấy.)


1

Cho đến nay mọi người ở đây đang nói về các lớp học. Nhưng hãy nghĩ về nó từ một quan điểm giao diện.

Nếu một phương thức giao diện khai báo một kiểu trả về và / hoặc một lời hứa ngầm về giá trị trả về, thì mọi thực hiện cần phải trả về đối tượng được cập nhật.

Vì vậy, bạn có thể hỏi:

  • Bạn có thể nghĩ về các triển khai có thể không muốn trả về một đối tượng mới không?
  • Bạn có thể nghĩ về các thành phần phụ thuộc vào giao diện (bằng cách chèn một thể hiện), không cần nhân viên cập nhật làm giá trị trả về không?

Cũng nghĩ về các đối tượng giả cho các bài kiểm tra đơn vị. Rõ ràng sẽ dễ dàng hơn để giả mà không có giá trị trả lại. Và các thành phần phụ thuộc dễ kiểm tra hơn, nếu các phụ thuộc được tiêm của chúng có giao diện đơn giản hơn.

Dựa trên những cân nhắc này, bạn có thể đưa ra quyết định.

Và nếu bạn hối tiếc về sau, bạn vẫn có thể giới thiệu giao diện thứ hai, với các bộ điều hợp giữa hai. Tất nhiên các giao diện bổ sung như vậy làm tăng độ phức tạp thành phần, vì vậy tất cả chỉ là sự đánh đổi.


0

Cách tiếp cận của bạn là tốt. Bất biến là một sự khẳng định mạnh mẽ. Điều duy nhất tôi sẽ hỏi là: Có nơi nào khác mà bạn xây dựng đối tượng không. Nếu đối tượng của bạn không bất biến, bạn sẽ phải trả lời các câu hỏi bổ sung vì "Trạng thái" được giới thiệu. Và sự thay đổi trạng thái của một đối tượng có thể xảy ra trên các lý do khác nhau. Sau đó, bạn nên biết trường hợp của bạn và chúng không nên dư thừa hoặc chia.

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.