实施SRP的实际方法是什么?


11

人们简单地检查班级是否违反单一责任原则的实用技术是什么?

我知道一个班级应该只有一个改变的理由,但是那句话在某种程度上缺乏真正实现这一改变的实用方法。

我发现的唯一方法是使用句子“……本身应该……”。其中第一个空格是类名称,第二个空格是方法(职责)名称。

但是,有时很难确定责任是否确实违反了SRP。

还有更多检查SRP的方法吗?

注意:

问题不在于SRP意味着什么,而是关于检查和实施SRP的实用方法或一系列步骤。

更新

报告类别

我添加了一个明显违反SRP的示例类。如果人们可以以此为例来说明他们如何遵循单一责任原则,那将是很好的。

这个例子是从这里开始的


这是一条有趣的规则,但是您仍然可以写:“一个人类可以自我渲染”。这可能被视为违反SRP,因为将GUI包含在包含业务规则和数据持久性的同一类中是不合适的。因此,我认为您需要添加体系结构域(层和层)的概念,并确保该语句仅对其中一个域有效(例如GUI,Data Access等)有效
NoChance 2012年

@EmmadKareem在Head First面向对象的分析和设计中提到了此规则,这正是我对此的想法。它有点缺乏实现它的实用方法。他们提到有时设计者不会意识到责任,并且他必须使用很多常识来判断该方法是否确实属于此类。
Songo 2012年

如果您真的想了解SRP,请阅读Bob Martin叔叔的著作。他的代码是我见过的最漂亮的代码,我相信他对SRP所做的任何评论不仅是合理的建议,而且还不只是挥手致意。
罗伯特·哈维

下选民请解释为什么要改善职位?!
松戈2012年

Answers:


7

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风格的对象,具有自己的行为,即使它似乎并没有真正“做”很多事情。

但是formatprint方法,它们与信息本身无关。实际上,您不太可能希望拥有这些方法的几种实现方式,例如,详细的管理层声明和不太详细的股东声明。将这些独立的函数分为不同的类,使您能够在运行时选择不同的实现,而无需一刀切的所有print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)方法。!

希望您可以看到上述大规模参数化方法在哪里出错,以及单独的实现在哪里正确;在单对象情况下,每次向打印逻辑添加新的皱纹时,都必须更改域模型(财务中的Tim想要页码,但只能在内部报表上添加吗?),而不是只需将配置属性添加到一两个卫星类中即可。

正确实施SRP与管理依赖关系有关。简而言之,如果一个类已经做了一些有用的事情,而您正在考虑添加另一种会引入新依赖关系的方法(例如UI,打印机,网络,文件等),则不要。考虑如何在类中添加此功能,以及如何使新类适合您的整体体系结构(在依赖项注入周围进行设计非常容易)。那是一般原理/过程。


旁注:像罗伯特一样,我显然拒绝接受SRP兼容类仅具有一个或两个状态变量的观点。如此薄的包装纸很少能起到真正有用的作用。因此,不要为此过度。


的确+1了。但是,我对这堂课感到困惑IncomeStatement。请问您提出的设计意味着IncomeStatement将有实例IncomeStatementPrinterIncomeStatementRenderer,这样,当我打电话print()IncomeStatement其将委托调用IncomeStatementPrinter呢?
Songo 2012年

@Songo:绝对不是!如果您遵循SOLID,则不应具有循环依赖性。显然,我的回答并没有说清楚,以至于该IncomeStatement没有一个print方法,或format方法,或不直接处理检查或操纵报告数据本身的任何其他方法。那就是那些其他类的目的。如果要打印一个,则需要对IPrintable<IncomeStatement>容器中注册的接口具有依赖性。
亚伦诺特,2012年

啊,我明白你的意思了。但是,如果我在类中注入实例,循环依赖关系PrinterIncomeStatement哪里?我想象的方式是当我打电话IncomeStatement.print()时会将它委托给IncomeStatementPrinter.print(this, format)。这种方法有什么问题?...另一个问题,您提到IncomeStatement如果我希望从数据库或XML文件中读取格式化的报告,则该信息应包含在格式化报告中显示的信息,而我应该提取加载数据的方法吗?进入一个单独的类,并将调用委派给IncomeStatement
松戈2012年

@Songo:您IncomeStatementPrinter取决于IncomeStatementIncomeStatement取决于IncomeStatementPrinter。那是周期性的依赖。而且这只是糟糕的设计;完全没有理由让您对or IncomeStatement有所了解,这是一个域模型,它与打印无关,并且委托毫无意义,因为任何其他类都可以创建或获取。没有充分的理由在域模型中具有任何打印概念。PrinterIncomeStatementPrinterIncomeStatementPrinter
亚伦诺特

至于如何IncomeStatement从数据库(或XML文件)加载-通常,这是由存储库和/或映射器而不是域来处理的,再次,您不会域中将其委托给它。如果其他的类需要读取这些模型之一,它将明确地要求该存储库。我猜除非您实现Active Record模式,否则我真的不是粉丝。
亚伦诺特,2012年

2

我检查SRP的方法是检查类的每种方法(职责)并提出以下问题:

“我是否需要更改实现此功能的方式?”

如果我找到需要以不同方式实现的功能(取决于某种配置或条件),则可以确定我需要一个额外的类来处理此职责。


1

这是《对象健美操》规则8的引文:

大多数类应该只负责处理一个状态变量,但是有些类将需要两个。向类中添加新的实例变量会立即降低该类的凝聚力。通常,在按照这些规则进行编程时,您会发现有两种类,它们维护单个实例变量的状态,以及协调两个单独变量的类。通常,不要将两种责任混在一起

鉴于这种(某种理想主义的)观点,您可以说任何仅包含一个或两个状态变量的类都不太可能违反SRP。您也可以说任何包含两个以上状态变量的类都可能违反SRP。


2
这种观点简直是无可救药的。甚至爱因斯坦著名但简单的方程式也需要两个变量。
罗伯特·哈维

OP的问题是“是否还有更多方法可以检查SRP?” -这是一种可能的指标。是的,这很简单,而且并非在所有情况下都适用,但这是检查是否违反SRP的一种可能方法。
MattDavey

1
我怀疑可变状态和不可变状态也是一个重要的考虑因素
jk。

规则8描述了创建具有成千上万个类的设计的完美过程,这使系统无可避免地变得复杂,难以理解和难以维护。但好的一面是您可以遵循SRP。
Dunk 2012年

@Dunk我不同意您的意见,但是那种讨论完全不是问题的话题。
MattDavey 2012年

1

一种可能的实现(用Java)。我对返回类型持自由态度,但总的来说,我认为它回答了这个问题。TBH我不认为Report类的接口不是那么糟糕,尽管可以使用更好的名称。为了简洁起见,我省略了警卫声明和主张。

编辑:还请注意该类是不可变的。因此,一旦创建,您将无法更改任何内容。您可以添加setFormatter()和setPrinter(),而不会遇到太多麻烦。恕我直言,关键是在实例化后不更改原始数据。

public class Report
{
    private ReportData data;
    private ReportDataDao dao;
    private ReportFormatter formatter;
    private ReportPrinter printer;


    /*
     *  Parameterized constructor for depndency injection, 
     *  there are better ways but this is explicit.
     */
    public Report(ReportDataDao dao, 
        ReportFormatter formatter, ReportPrinter printer)
    {
        super();
        this.dao = dao;
        this.formatter = formatter;
        this.printer = printer;
    }

    /*
     * Delegates to the injected printer.
     */
    public void printReport()
    {
        printer.print(formatReport());
    }


    /*
     * Lazy loading of data, delegates to the dao 
     * for the meat of the call.
     */
    public ReportData getReportData()
    {
        if (reportData == null)
        {
            reportData = dao.loadData();
        }
        return reportData;
    }

    /*
     * Delegate to the formatter for formatting 
     * (notice a pattern here).
     */
    public ReportData formatReport()
    {
        formatter.format(getReportData());
    }
}

感谢您的实施。我有两件事,if (reportData == null)我想你的意思是data相反。其次,我希望知道您是如何到达此实现的。就像为什么您决定将所有调用委派给其他对象一样。我一直想知道的另一件事是,打印自己的报告真的有责任吗?为什么不创建printer一个report在其构造函数中采用的单独类?
松戈2012年

是的,reportData = data,对此感到抱歉。委托允许对依赖项进行细粒度的控制。在运行时,您可以为每个组件提供替代实现。现在,您可以拥有HtmlPrinter,PdfPrinter,JsonPrinter等。这对于测试也很方便,因为您可以单独测试委托的组件,也可以将其集成到上面的对象中。您当然可以颠倒打印机和报表之间的关系,我只是想表明可以使用提供的类接口来提供解决方案。这是在旧系统上工作的习惯。:)
Heath Lilley

嗯...那么,如果您是从头开始构建系统,您会选择哪个选项?一Printer类是需要一个报告或Report接受一个打印机类?在不得不解析报告之前,我遇到了类似的问题,我与TL争论是否应该构建一个接受报告的解析器,或者该报告是否应在其中包含解析器并将parse()调用委托给它。
Songo 2012年

如果稍后需要,我将同时执行... printer.print(report)和report.print()。printer.print(report)方法的优点在于它是高度可重用的。它分离了责任,使您可以在需要时使用便捷的方法。也许您不需要让系统中的其他对象知道ReportPrinter,因此通过在类上使用print()方法,您将获得使抽象的报告打印逻辑与外界隔离的抽象程度。它仍然具有狭窄的变化向量,并且易于使用。
Heath Lilley 2012年

0

在您的示例中,尚不清楚是否违反了SRP。如果报表相对简单,也许它应该能够自己格式化和打印:

class Report {
  void format() {
     text = text.trim();
  }

  void print() {
     new Printer().write(text);
  }
}

这些方法是如此简单,以至于没有类ReportFormatterReportPrinter类。界面中唯一明显的问题是getReportData因为它违反了对非值对象的问别告诉。

另一方面,如果方法非常复杂,或者有许多格式化或打印a的方法,Report则有必要委派职责(也是可测试的):

class Report {
  void format(ReportFormatter formatter) {
     text = formatter.format(text);
  }

  void print(ReportPrinter printer) {
     printer.write(text);
  }
}

SRP是设计原则,而不是哲学概念,因此它基于您正在使用的实际代码。在语义上,您可以根据需要将一个班级划分或分组为多个职责。但是,作为实用原则,SRP应该可以帮助您找到需要修改的代码。您违反SRP的迹象是:

  • 类太大,以至于浪费时间滚动或寻找正确的方法。
  • 班级如此之小,数量众多,您浪费时间在它们之间来回切换或寻找正确的班级。
  • 当您必须进行更改时,它会影响许多类,因此很难跟踪。
  • 当您必须进行更改时,尚不清楚需要更改哪些类。

您可以通过改进名称,将相似的代码分组在一起,消除重复,使用分层设计以及根据需要拆分/组合类来通过重构来解决这些问题。学习SRP的最好方法是潜入代码库并减轻痛苦。


您能否检查我所附的示例,并据此详细说明您的答案。
Songo 2012年

更新。SRP取决于上下文,如果您发布了整个课程(在一个单独的问题中),则将更易于解释。
加勒特音乐厅

感谢更新。但是,有一个问题,打印自己的报告真的有责任吗?为什么不创建一个单独的打印机类在其构造函数中接收报告?
Songo 2012年

我只是说SRP取决于代码本身,您不应该教条地应用它。
加勒特音乐厅

是的,我明白你的意思。但是,如果您是从头开始构建系统,那么您将采取哪种选择?一Printer类是需要一个报告或Report接受一个打印机类?很多时候,在弄清代码是否会很复杂之前,我都会遇到这样的设计问题。
Songo 2012年

0

单一责任原则凝聚力概念紧密相关。为了具有高度凝聚力的类,您需要在类的实例变量及其方法之间具有相互依赖关系。也就是说,每个方法都应操纵尽可能多的实例变量。方法使用的变量越多,其类的凝聚力就越大;通常无法实现最大的内聚力。

另外,为了很好地应用SRP,您应该了解业务逻辑领域。知道每个抽象应该做什么。分层体系结构还与SRP相关,通过让每一层做特定的事情(数据源层应提供数据等)。

返回内聚,即使您的方法未使用所有变量,也应将它们耦合在一起:

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public Type1 method3() {
        //use var2 and var3
    }
}

您不应该像下面的代码那样,在一部分方法中使用一部分实例变量,而在另一部分方法中使用变量的另一部分(这里您应该有两个类用于变量的每个部分)。

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;
    private TypeA varA;
    private TypeB varB;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public TypeA methodA() {
        //use varA and varB
    }

    public TypeA methodB() {
        //use varA
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.