单一责任原则-如何避免代码碎片化?


56

我正在一个团队中,团队负责人是SOLID开发原则的坚决拥护者。但是,他缺乏将复杂软件发布出去的大量经验。

在某种情况下,他将SRP应用于本来已经很复杂的代码库中,而现在该代码库变得非常分散,难以理解和调试。

现在,我们不仅遇到代码碎片问题,而且遇到封装问题,因为某个类中可能是私有或受保护的方法被判断为代表“更改原因”,并已提取到公共或内部类和接口中,与应用程序的封装目标不符。

我们有一些类构造函数,它们接收20个以上的接口参数,因此我们的Io​​C注册和解析本身已成为一个庞然大物。

我想知道我们是否可以使用任何“远离SRP的重构”方法来帮助解决其中一些问题。我已经读到如果创建多个空的粗粒度类来“包装”多个紧密相关的类以提供对它们功能总和的单点访问(即模仿一个较小的类),则它不会违反SOLID过度采用SRP的类实施)。

除此之外,我想不出一种解决方案,该解决方案将使我们能够务实地继续我们的开发工作,同时又使每个人都满意。

有什么建议么 ?


18
那只是我的意见,但我认为还有另外一条规则,很容易在各种缩写词“常识性原则”中被遗忘。当“解决方案”产生了更多它真正要解决的问题时,那就出了问题。我的看法是,如果问题很复杂,但是包含在一个类中,该类可以解决问题的复杂性,并且仍然相对容易调试-我将其搁置一旁。通常,您的“包装”想法对我来说似乎是正确的,但我会将答案留给知识渊博的人。
PatrykĆwiek'12

6
至于“理由改变”-无需过早推测所有原因。等到您实际上必须更改它之后,再看看将来可以做些什么来使这种更改变得更容易。

62
具有20个构造函数参数的类对我来说听起来不是SRP!
MattDavey 2012年

1
您输入“ ... IoC注册和解决方案...”;这听起来好像您(或您的团队负责人)认为“ IoC”和“依赖注入”(DI)是同一件事,但事实并非如此。DI是实现IoC的一种手段,但当然不是唯一的一种。您应该仔细分析为什么要进行IoC。如果是因为要编写单元测试,则还可以尝试使用服务定位器模式或仅使用接口类(ISomething)。恕我直言,与依赖注入相比,这些方法要容易处理得多,并且代码更具可读性。

2
这里给出的任何答案都是真空的;我们将不得不查看代码以给出特定的响应。构造函数中有20个参数?好吧,您可能缺少一个对象……或者它们可能都是有效的;或者它们可能属于配置文件,或者它们可能属于DI类,或者...这些症状听起来似乎很可疑,但是像CS中的大多数情况一样,“取决于” ...
Steven A. Lowe

Answers:


84

如果您的类在构造函数中有20个参数,这听起来并不像您的团队完全知道SRP是什么。如果您有一个只做一件事的类,那么它如何有20个依赖项?这就像进行一次钓鱼之旅,并带上一根钓鱼竿,钓具盒,supplies缝用品,保龄球,双节棍,喷火器等。...如果您需要所有这些去钓鱼,那么您不仅会钓鱼。

就是说,SRP与目前的大多数原则一样,可能会被过度应用。如果您创建一个用于增加整数的新类,那么是的,这可能是一个责任,但是请继续。这是荒谬的。我们倾向于忘记诸如SOLID原理之类的东西是有目的的。SOLID是达到目的的一种手段,而不是目的本身。最终是可维护性。如果您要通过“单一责任原则”来了解这一点,则表明对SOLID的热情使团队对SOLID的目标视而不见。

所以,我想我的意思是……SRP不是您的问题。这可能是对SRP的误解,或者是对其的难以置信的精细应用。尝试让您的团队将主要内容保留为主要内容。最主要的是可维护性。

编辑

让人们以鼓励易用性的方式设计模块。将每个类都视为一个微型API。首先考虑“我想如何使用此类”,然后实现它。不要只是想“这堂课需要做什么”。如果您不对可用性进行过多考虑,SRP确实有很大的趋势使类变得更难使用。

编辑2

如果您正在寻找有关重构的技巧,则可以开始执行建议的操作-创建粗粒度的类来包装其他几个类。确保较粗粒度的类仍坚持SRP,但级别更高。然后,您有两种选择:

  1. 如果在系统的其他地方不再使用细粒度的类,则可以逐渐将其实现拉入粗粒度的类并删除它们。
  2. 单独保留更细粒度的类。也许它们设计得很好,而您只需要包装即可使它们更易于使用。我怀疑您的大多数项目都是这种情况。

当您完成重构后(但在提交到存储库之前),请检查您的工作并询问自己,重构是否实际上是对可维护性和易用性的改进。


2
让人们思考设计类的另一种方法:让他们写CRC卡(类名,职责,协作者)。如果一个班级的协作者或职责过多,则很可能不够SRP。换句话说,所有文本都必须适合索引卡,否则它做得太多。
斯派克,2012年

18
我知道喷火器是做什么用的,但是你怎么用杆子钓鱼呢?
R. Martinho Fernandes

13
+1 SOLID是达到目的的一种手段,而不是目的本身。
B

1
+1:我之前曾争论过诸如“得墨meter耳的法则”之类的名字被错误命名,它应该是“得墨meter耳的指南”。这些东西应该为您工作,您不应该为它们工作。
Binary Worrier 2012年

2
@EmmadKareem:的确,DAO对象应该具有多个属性。但是话又说回来,您可以将几类东西组合在一起,就像一个Customer类一样简单,并且具有更多可维护的代码。见的例子在这里:codemonkeyism.com/...
Spoike

32

我认为在Martin Fowler的Refactoring中,我曾经阅读过SRP的反规定,定义了SRP的发展方向。还有第二个问题,与“每个班级只有一个改变的理由吗”一样重要。那就是“每一次改变只会影响一个阶级吗?”

如果在每种情况下第一个问题的答案都是“是”,而第二个问题是“甚至不很接近”,那么您需要再次查看如何实现SRP。

例如,如果向表中添加一个字段意味着您必须更改DTO,验证器类,持久性类和视图模型对象,等等,那么您就创建了一个问题。也许您应该重新考虑如何实施SRP。

也许您已经说过,添加字段是更改Customer对象的原因,但是更改持久层(例如,从XML文件到数据库)是更改Customer对象的另一个原因。因此,您决定也创建一个CustomerPersistence对象。但是,如果这样做使得添加字段STILL要求更改CustomerPersisitence对象,那又有什么意义呢?您仍然有一个需要更改的两个原因的对象-它不再是Customer。

但是,如果引入了ORM,则很有可能可以使类起作用,以便在DTO中添加字段时,它将自动更改用于读取该数据的SQL。然后,您就有充分的理由将这两个问题分开。

总而言之,这就是我倾向于做的事情:如果我说“不,更改此对象的原因不止一次”与我说“不,此更改将不止一次”之间有一个大致的平衡影响一个以上的对象”,那么我认为我在SRP和碎片化之间具有平衡。但是,如果两者仍然很高,那么我开始想知道是否有其他方法可以分离关注点。


+1代表“每项变更只会影响一个班级吗?”
2014年

我未曾讨论过的一个相关问题是,如果绑定到一个逻辑实体的任务分散在不同的类中,那么可能有必要代码保留对所有绑定到同一实体的多个不同对象的引用。例如,考虑具有功能“ SetHeaterOutput”和“ MeasureTemperature”的窑。如果窑炉由独立的HeaterControl和TemperatureSensor对象表示,则没有什么可以防止TemperatureFeedbackSystem对象持有对一个窑炉加热器和另一个窑炉温度传感器的引用。
2014年

1
相反,如果将这些功能组合到由Kiln对象实现的IKiln接口中,那么TemperatureFeedbackSystem将只需要保存一个IKiln引用。如果必须使用带有独立售后温度传感器的窑炉,则可以使用CompositeKiln对象,该对象的构造函数接受IHeaterControl和ITemperatureSensor并使用它们来实现IKiln,但是这种故意的松散成分很容易在代码中识别。
2014年

23

仅仅因为一个系统是复杂的并不意味着你必须使它变得复杂。如果您的类具有过多的依赖性(或协作者),例如:

public class MyAwesomeClass {
    public class MyAwesomeClass(IDependency1 _d1, IDependency2 _d2, ... , IDependency20 _d20) {
      // Assign it all
    }
}

...然后变得太复杂了,您并没有真正遵循SRP,是吗?如果你写下什么我敢打赌MyAwesomeClass一上做CRC卡它不适合索引卡上,或者你必须写在真的很小潦草的信件。

您在这里所拥有的是,您的家伙们只遵循了接口隔离原则,并且可能将其推向了极致,但这完全是另外一回事了。您可能会争辩说,依赖项是域对象(会发生这种情况),但是拥有一个同时处理20个域对象的类会使它有点过分。

TDD将为您很好地指示一堂课的成绩。直言不讳 如果测试方法的安装代码需要花费大量时间编写(即使您重构测试),那么您MyAwesomeClass可能还有太多事情要做。

那么如何解决这个难题呢?您将职责移至其他班级。您可以对有此问题的类采取一些步骤:

  1. 确定类对其依赖项执行的所有操作(或职责)。
  2. 根据密切相关的依赖项将操作分组。
  3. 重新委托!即将每个已识别的动作重构为新的类别(或更重要的是其他类别)。

重构责任的抽象示例

我们C是有一些依赖性的一类D1D2D3D4,你需要重构少用。当我们确定C调用依赖项的方法时,我们可以列出它的简单列表:

  • D1- performA(D2)performB()
  • D2 -- performD(D1)
  • D3 -- performE()
  • D4 -- performF(D3)

查看列表,我们可以看到,D1并且D2彼此相关,因为班级以某种方式需要它们。我们也可以看到D4需求D3。因此,我们有两个分组:

  • Group 1- D1<->D2
  • Group 2- D4- >D3

分组表明班级现在有两个职责。

  1. Group 1-一个用于处理彼此需要的两个调用对象。也许您可以让您的类C消除处理两个依赖关系的需要,而让其中一个处理这些调用。在此分组中,显然D1可以参考D2
  2. Group 2-其他责任需要一个对象来调用另一个。无法D4处理D3,而不是你的班?然后,我们可以改为通过调用来D3从类中消除。CD4

不要把我的答案一成不变,因为该示例是非常抽象的,并且有很多假设。我敢肯定,还有更多方法可以重构它,但是至少这些步骤可以帮助您获得某种过程来转移职责,而不是拆分类。


编辑:

在评论中,@ Emmad Karem说:

“如果您的类在构造函数中有20个参数,这听起来并不像您的团队完全知道SRP是什么。如果您的类只做一件事,那么它如何有20个依赖项?”-我认为,如果有一个Customer类,在构造函数中有20个参数并不奇怪。

确实,DAO对象倾向于具有很多参数,您必须在构造函数中设置这些参数,并且这些参数通常是简单的类型,例如字符串。但是,在一个Customer类的示例中,您仍然可以将其属性分组到其他类中,以简化操作。例如,有一个Address带有街道的Zipcode类和一个包含邮政编码的类,该类还将处理诸如数据验证之类的业务逻辑:

public class Address {
    private String street1;
    //...

    private Zipcode zipcode;

    // easy to extend
    public bool isValid() {
        return zipcode.isValid();
    }
}

public class Zipcode {
    private string zipcode;
    public bool isValid() {
        // return regex match that zipcode contains numbers
    }
}

在博客文章“永不,永不,永远不要在Java中使用String(或至少经常)”中对此问题进行了进一步讨论。作为使用构造函数或静态方法使子对象更易于创建的一种替代方法,可以使用流体生成器模式


+1:好答案!分组是IMO的一种非常强大的机制,因为您可以递归应用分组。粗略地说,使用n个抽象层可以组织2 ^ n个项目。
乔治

+1:您的前几段完全总结了我团队所面临的问题。实际上是服务对象的“业务对象”,以及令人麻木的单元测试设置代码。我知道我们的服务层调用包含一行代码时会遇到问题。调用业务层方法。
男人

3

我同意有关SRP的所有答案以及如何将其付诸实践。在您的帖子中,您提到由于遵循SRP的“过度重构”,您发现封装被破坏或被修改。对我有用的一件事是始终坚持基础知识,并尽一切努力达到最终目的。

在使用Legacy系统时,团队负责人尤其是那些刚开始使用该角色的人,对修复所有问题使其变得更好的“热情”通常很高。SOLID,只是没有SRP-就是S。请确保如果您遵循SOLID,也不要忘记OLID。

我现在正在开发Legacy系统,一开始我们就走了类似的道路。对我们有用的是团队集体决策,要充分利用SOLID和KISS(保持简单愚蠢)这两个方面的优势。我们共同讨论了代码结构的重大更改,并在应用各种开发原理时应用了常识。它们非常适合作为指导方针,而不是“软件开发的规律”。团队不仅与团队负责人有关,还与团队中所有开发人员有关。对我来说一直有效的是让每个人都在一个房间里,并提出一套整个团队都同意遵循的共享准则。

关于如何解决当前问题,如果您使用VCS并且未在应用程序中添加太多新功能,则始终可以返回整个团队认为可以理解,可读和可维护的代码版本。是! 我要你放弃工作,从头开始。这比尝试“修复”已损坏的东西并将其移回已存在的东西要好。


3

答案是代码的可维护性和清晰度高于一切。对我来说,这意味着编写更少的代码,而不是更多。更少的抽象,更少的接口,更少的选项,更少的参数。

每当我评估代码重组或添加新功能时,我都会考虑与实际逻辑相比需要多少样板。如果答案超过50%,则可能意味着我已经想不通了。

除了SRP之外,还有许多其他开发样式。在您的情况下,听起来似乎肯定缺少YAGNI。


3

这里的许多答案确实不错,但侧重于此问题的技术方面。我只是简单地补充说,这听起来像是开发人员尝试遵循SRP的声音,就像他们实际上违反了SRP。

您可以在此处查看Bob的博客了解这种情况,但是他认为,如果跨多个类抹上了责任,那么将违反责任SRP,因为这些类是并行变化的。我怀疑您的开发人员真的很喜欢Bob博客顶部的设计,并且看到它撕裂可能会有些失望。特别是因为它违反了“通用封闭原则”-一起变化的事物保持在一起。

请记住,SRP指的是“变革的原因”,而不是“做一件事”,在真正发生更改之前,您无需担心更改的原因。第二个人为抽象付费。

现在有第二个问题-“大力提倡SOLID开发”。肯定听起来您与该开发人员的关系不大,所以任何试图说服他(她)在代码库中遇到的问题的尝试都被挫败了。您需要修复关系,以便可以对问题进行真正的讨论。我建议喝啤酒。

不严重-如果您不去咖啡馆喝酒。离开办公室,在一个放松的地方,您可以在这里非正式地谈论这些东西。与其在会议上赢得一场辩论(您不会参加),不如在一个有趣的地方进行讨论。试着认识到这个开发人员,正在使您发疯,是一个实际运转的人,他试图将软件“推向市场”,并且不想发布废话。由于您可能有共同点,因此您可以开始讨论如何在仍符合SRP的同时改进设计。

如果您都可以承认SRP是一件好事,而您只是以不同的方式解释方面,那么您可能会开始进行富有成效的对话。


-1

我同意您的团队领导的决定[update = 2012.05.31],SRP通常是一个好主意。但是我完全同意@ Spoike -s的评论,即带有20个接口参数的构造函数远远不止于此。[/ update]:

引入带有IoC的SRP,会使复杂性从一个 “多负责人的类”转变为许多 srp类,并且为了使... ...受益,初始化要复杂得多。

  • 更容易的单元测试/ tdd(一次隔离测试一个srp类)
  • 但以
    • 更加困难的代码初始化和集成,以及
    • 更困难的调试
    • 碎片(=在多个文件/目录上分配代码)

恐怕您不能在不牺牲srp的情况下减少代码碎片。

但是,您可以通过实现语法糖类来“减轻代码初始化的痛苦”,该语法糖类在构造函数中隐藏了初始化的复杂性。

   class MySrpClass {
      MySrpClass(Interface1 parm1, Interface2 param2, .... Interface20 param2) {
      }
   } 

   class MySyntaxSugarClass : MySrpClass {
      MySyntaxSugarClass() {
         super(new MyInterface1Implementation(), new MyImpl2(), ....)
      }
   }

2
我相信20个接口可以表明该类要做的事太多了。即有20个原因对其进行更改,这几乎违反了SRP。仅仅因为系统很复杂并不意味着它一定很复杂。
斯派克,2012年
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.