SRP tuyên bố, không có gì chắc chắn, rằng một lớp chỉ nên có một lý do để thay đổi.
Giải mã lớp "báo cáo" trong câu hỏi, nó có ba phương thức:
printReport
getReportData
formatReport
Bỏ qua sự dư thừa Report
đang được sử dụng trong mọi phương thức, thật dễ dàng để biết lý do tại sao điều này vi phạm SRP:
Thuật ngữ "in" ngụ ý một số loại UI hoặc máy in thực tế. Do đó, lớp này chứa một số lượng UI hoặc logic trình bày. Một thay đổi đối với các yêu cầu UI sẽ đòi hỏi phải thay đổi Report
lớp.
Thuật ngữ "dữ liệu" ngụ ý một cấu trúc dữ liệu thuộc loại nào đó, nhưng không thực sự chỉ định cái gì (XML? JSON? CSV?). Bất kể, nếu "nội dung" của báo cáo từng thay đổi, thì phương pháp này cũng sẽ như vậy. Có khớp nối với cơ sở dữ liệu hoặc tên miền.
formatReport
nói chung chỉ là một cái tên khủng khiếp cho một phương thức, nhưng tôi cho rằng bằng cách nhìn vào nó một lần nữa nó có liên quan đến UI và có lẽ là một khía cạnh khác của UI hơn printReport
. Vì vậy, một lý do khác, không liên quan để thay đổi.
Vì vậy, một lớp này có thể được kết hợp với cơ sở dữ liệu, thiết bị màn hình / máy in và một số logic định dạng bên trong cho nhật ký hoặc đầu ra tệp hoặc không có gì. Bằng cách có tất cả ba hàm trong một lớp, bạn sẽ nhân số lượng phụ thuộc và tăng gấp ba xác suất rằng bất kỳ thay đổi phụ thuộc hoặc yêu cầu nào sẽ phá vỡ lớp này (hoặc một cái gì khác phụ thuộc vào nó).
Một phần của vấn đề ở đây là bạn đã chọn một ví dụ đặc biệt gai góc. Bạn nên có lẽ không có một lớp gọi là Report
, ngay cả khi nó chỉ thực hiện một điều , bởi vì ... những gì báo cáo? Không phải tất cả "báo cáo" các con thú hoàn toàn khác nhau, dựa trên dữ liệu khác nhau và các yêu cầu khác nhau? Và không phải là một báo cáo một cái gì đó đã được định dạng, cho màn hình hoặc để in?
Nhưng, nhìn qua điều đó và tạo nên một cái tên cụ thể giả định - hãy gọi nó IncomeStatement
(một báo cáo rất phổ biến) - một kiến trúc "SRPed" thích hợp sẽ có ba loại:
IncomeStatement
- tên miền và / hoặc lớp mô hình có chứa và / hoặc tính toán thông tin xuất hiện trên các báo cáo được định dạng.
IncomeStatementPrinter
, mà có lẽ sẽ thực hiện một số giao diện tiêu chuẩn như IPrintable<T>
. Có một phương thức chính Print(IncomeStatement)
và có thể một số phương thức hoặc thuộc tính khác để định cấu hình cài đặt dành riêng cho in.
IncomeStatementRenderer
, xử lý kết xuất màn hình và rất giống với lớp máy in.
Cuối cùng, bạn cũng có thể thêm các lớp đặc trưng hơn như IncomeStatementExporter
/ IExportable<TReport, TFormat>
.
Điều này được thực hiện dễ dàng hơn đáng kể trong các ngôn ngữ hiện đại với sự ra đời của generic và container IoC. Hầu hết mã ứng dụng của bạn không cần dựa vào IncomeStatementPrinter
lớp cụ thể , nó có thể sử dụng IPrintable<T>
và do đó hoạt động trên bất kỳ loại báo cáo có thể in nào, cung cấp cho bạn tất cả các lợi ích cảm nhận của Report
lớp cơ sở với một print
phương thức và không có vi phạm SRP thông thường nào . Việc thực hiện thực tế chỉ cần được khai báo một lần, trong đăng ký container IoC.
Một số người, khi đối mặt với thiết kế trên, đã trả lời với một cái gì đó như: "nhưng điều này trông giống như mã thủ tục và toàn bộ quan điểm của OOP là giúp chúng tôi thoát khỏi sự phân tách dữ liệu và hành vi!" Để tôi nói: sai .
Các IncomeStatement
là không chỉ là "dữ liệu", và sai lầm nói trên là nguyên nhân gây ra rất nhiều folks OOP cảm thấy họ đang làm điều gì đó sai bằng cách tạo ra một lớp "trong suốt" như vậy và sau đó bắt đầu gây nhiễu các loại chức năng không liên quan vào IncomeStatement
(tốt, mà và sự lười biếng nói chung). Lớp này có thể bắt đầu chỉ là dữ liệu, nhưng theo thời gian, được đảm bảo, nó sẽ kết thúc giống như một mô hình .
Ví dụ: báo cáo thu nhập thực tế có tổng doanh thu , tổng chi phí và dòng thu nhập ròng . Một hệ thống tài chính được thiết kế hợp lý rất có thể sẽ không lưu trữ những thứ này vì chúng không phải là dữ liệu giao dịch - thực tế, chúng thay đổi dựa trên việc bổ sung dữ liệu giao dịch mới. Tuy nhiên, việc tính toán các dòng này sẽ luôn giống hệt nhau, bất kể bạn đang in, kết xuất hoặc xuất báo cáo. Vì vậy, bạn IncomeStatement
lớp sẽ có một số lượng hợp lý của hành vi để nó trong hình thức getTotalRevenues()
, getTotalExpenses()
và getNetIncome()
phương pháp, và có lẽ một số người khác. Nó là một đối tượng kiểu OOP chính hãng với hành vi của chính nó, ngay cả khi nó không thực sự "làm" nhiều.
Nhưng format
và print
phương pháp, chúng không liên quan gì đến thông tin. Trên thực tế, không quá khó để bạn có thể thực hiện một số phương pháp này, ví dụ như một tuyên bố chi tiết về quản lý và một tuyên bố không chi tiết cho các cổ đông. Việc tách các hàm độc lập này thành các lớp khác nhau cho bạn khả năng chọn các triển khai khác nhau trong thời gian chạy mà không phải chịu gánh nặng của một print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
phương thức phù hợp với một kích thước . Kinh quá!
Hy vọng rằng bạn có thể thấy phương pháp tham số ồ ạt ở trên sai ở đâu và phương thức triển khai riêng biệt đi đúng; trong trường hợp một đối tượng, mỗi khi bạn thêm một nếp nhăn mới vào logic in, bạn phải thay đổi mô hình miền của mình ( Tim in finance muốn số trang, nhưng chỉ trên báo cáo nội bộ, bạn có thể thêm điều đó không? ) thay vào đó chỉ cần thêm một thuộc tính cấu hình cho một hoặc hai lớp vệ tinh.
Thực hiện SRP đúng cách là quản lý các phụ thuộc . Tóm lại, nếu một lớp đã làm một cái gì đó hữu ích và bạn đang xem xét thêm một phương thức khác sẽ giới thiệu một phụ thuộc mới (như UI, máy in, mạng, tệp, bất cứ thứ gì), thì không . Thay vào đó, hãy suy nghĩ về cách bạn có thể thêm chức năng này vào một lớp mới và cách bạn có thể làm cho lớp mới này phù hợp với kiến trúc tổng thể của bạn (khá dễ dàng khi bạn thiết kế xung quanh việc tiêm phụ thuộc). Đó là nguyên tắc / quy trình chung.
Lưu ý bên lề: Giống như Robert, tôi từ chối một cách kiên quyết khái niệm rằng một lớp tuân thủ SRP chỉ nên có một hoặc hai biến trạng thái. Một lớp bọc mỏng như vậy hiếm khi có thể được dự kiến sẽ làm bất cứ điều gì thực sự hữu ích. Vì vậy, đừng quá nhiệt tình với điều này.