SRP毫无疑问地指出,一个班级应该永远只有一个改变的理由。
解构问题中的“报告”类,它具有三种方法:
printReport
getReportData
formatReport
忽略Report
每种方法中都使用的冗余,很容易看出为什么这违反了SRP:
术语“打印”表示某种类型的UI或实际的打印机。因此,此类包含一定数量的UI或表示逻辑。更改UI要求将需要更改Report
类。
术语“数据”表示某种数据结构,但并未真正指定什么(XML?JSON?CSV?)。无论如何,如果报表的“内容”发生了变化,则此方法也会发生变化。耦合到数据库或域。
formatReport
一般而言,它只是一个方法的可怕名称,但我认为通过它的使用,它再次与UI有关,并且可能与UI有所不同printReport
。因此,另一个不相关的变化原因。
因此,这一类可能与数据库,屏幕/打印机设备以及一些用于日志或文件输出等的内部格式化逻辑结合在一起。通过将所有三个功能都放在一个类中,您可以使依赖项的数量成倍增加,并使任何依赖项或需求更改破坏该类(或依赖于此的其他事物)的可能性增加三倍。
这里的部分问题是您选择了一个特别棘手的示例。Report
即使只做一件事,您也可能不应该拥有一个叫做的类,因为…… 什么报告?并非所有的“报告”都是基于不同的数据和不同的需求而完全不同的野兽吗?而且,报表不是已经格式化的东西,无论是用于屏幕还是用于打印?
但是,回顾过去并组成一个假想的具体名称-我们称它IncomeStatement
(一个非常常见的报告)-适当的“ SRPed”架构将具有三种类型:
IncomeStatement
- 包含和/或计算在格式化报告上显示的信息的域和/或模型类。
IncomeStatementPrinter
,可能会实现一些标准接口,例如IPrintable<T>
。有一个关键方法,Print(IncomeStatement)
也许还有其他一些方法或属性可用于配置特定于打印的设置。
IncomeStatementRenderer
,它处理屏幕渲染,并且与打印机类非常相似。
您最终还可以添加更多特定于功能的类,例如IncomeStatementExporter
/ IExportable<TReport, TFormat>
。
通过引入泛型和IoC容器,这在现代语言中变得非常容易。您的大多数应用程序代码不需要依赖特定的IncomeStatementPrinter
类,它可以使用IPrintable<T>
并因此可以在任何类型的可打印报告上运行,这为您Report
提供了具有print
方法的基类的所有可感知的好处,并且没有常见的违反SRP的情况。实际的实现只需要在IoC容器注册中声明一次。
有些人在面对上述设计时会做出类似的回应:“但是,这看起来像是过程代码,而OOP的全部目的就是要使我们摆脱数据和行为的分离!” 我对此说:错。
的IncomeStatement
是不只是“数据”,和上述的错误是什么原因导致了很多OOP的乡亲觉得他们通过建立这样一个“透明”类做错了什么,然后开始干扰各种不相关的功能整合到了IncomeStatement
(当然,这和一般的懒惰)。此类可能只是从数据开始的,但是随着时间的推移,有保证的话,它将最终成为更多的模型。
例如,真实的损益表包含总收入,总费用和净收入线。经过适当设计的金融系统很可能不会存储这些数据,因为它们不是交易数据-实际上,它们会根据新交易数据的添加而发生变化。但是,无论您是打印,渲染还是导出报告,这些行的计算总会完全相同。所以,你的IncomeStatement
类将不得不在形式的行为给它一个公平的量getTotalRevenues()
,getTotalExpenses()
和getNetIncome()
方法,而且很可能其他几个人。这是一个真正的OOP风格的对象,具有自己的行为,即使它似乎并没有真正“做”很多事情。
但是format
和print
方法,它们与信息本身无关。实际上,您不太可能希望拥有这些方法的几种实现方式,例如,详细的管理层声明和不太详细的股东声明。将这些独立的函数分为不同的类,使您能够在运行时选择不同的实现,而无需一刀切的所有print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
方法。!
希望您可以看到上述大规模参数化方法在哪里出错,以及单独的实现在哪里正确;在单对象情况下,每次向打印逻辑添加新的皱纹时,都必须更改域模型(财务中的Tim想要页码,但只能在内部报表上添加吗?),而不是只需将配置属性添加到一两个卫星类中即可。
正确实施SRP与管理依赖关系有关。简而言之,如果一个类已经做了一些有用的事情,而您正在考虑添加另一种会引入新依赖关系的方法(例如UI,打印机,网络,文件等),则不要。考虑如何在新类中添加此功能,以及如何使新类适合您的整体体系结构(在依赖项注入周围进行设计非常容易)。那是一般原理/过程。
旁注:像罗伯特一样,我显然拒绝接受SRP兼容类仅具有一个或两个状态变量的观点。如此薄的包装纸很少能起到真正有用的作用。因此,不要为此过度。