简洁的代码:使用较少参数的简短方法的后果


15

最近,在代码审查期间,我遇到了一个新同事写的代码,其中包含带有气味的模式。我怀疑我同事的决定是基于著名的《清洁法规》(也许还有其他类似书籍)提出的规则。

据我了解,类构造函数完全负责创建有效对象,并且其主要任务是分配对象的(私有)属性。当然,可能会通过类构造器以外的方法设置可选属性值,但是这种情况很少见(尽管不一定是错误的,只要类的其余部分都考虑了此类属性的可选性)。这很重要,因为它可以确保对象始终处于有效状态。

但是,在我遇到的代码中,大多数属性值实际上是由构造方法以外的其他方法设置的。计算得出的值被分配给属性,以便在整个类的多个私有方法中使用。作者似乎在使用类属性,好像它们是应在整个类中访问的全局变量一样,而不是将这些值参数化为需要它们的函数。此外,应按特定顺序调用类的方法,因为否则该类不会做很多事情。

我怀疑这段代码是受建议的启发而提出的,即应保持方法简短(<= 5行代码),以避免大的参数列表(<3个参数),并且构造函数一定不能工作(例如执行某种计算)这对于对象的有效性至关重要。

现在,如果可以证明当未按特定顺序调用方法时,可能会出现各种不确定的错误,那么我当然可以反对这种模式。但是,我预测对此的响应将是添加验证,以验证一旦调用需要设置那些属性的方法,就必须设置属性。

但是,我宁愿建议完全更改代码,以使该类成为实际对象的蓝图,而不是应按特定顺序调用(过程上)的一系列方法。

我觉得我遇到的代码有异味。实际上,我相信在何时将值保存在类属性中以及何时将其保存到参数中以供不同方法使用方面存在明显的区别-我真的不相信它们可以彼此替代。我正在寻找这种区别的词。


6
1.扮演恶魔的拥护者…… 代码实际上起作用了吗?因为数据传输对象是一种完全有效的技术,如果仅此
Robert Harvey

7
2.如果您缺乏描述问题的措辞,则您没有足够的经验来驳斥同事的立场。
罗伯特·哈维

4
3.如果您有一些可以使用的代码,则可以将其发布,然后将其发布到Code Review中,然后让他们查看。否则,这只是一个徘徊的普遍性。
罗伯特·哈维

5
@RobertHarvey“将计算所得的值分配给将在整个类的多个私有方法中使用的属性”对我来说听起来并不像是自尊自大的DTO。我确实同意,多一点专一性会有所帮助。
topo恢复莫妮卡

4
旁听:听起来好像有人在扑打干净代码之前并没有真正阅读它。我再次对其进行了扫描,但找不到任何表明“构造函数不应该工作”的地方(实际上有些示例确实可以完成工作),为避免过多的参数,建议的解决方案是创建一个合并相关的参数对象参数组,而不是混用您的函数。本书确实建议重构代码,以避免方法之间的时间依赖性。我认为您对他偏爱的几种代码风格的偏见使您对这本书的看法更加丰富多彩。
埃里克·金

Answers:


13

作为多次阅读“清洁代码”并观看过“清洁代码”系列的人,并且经常教别人编写清洁代码,我确实可以保证您的观察是正确的-书中提到了所有指出的指标。

但是,本书继续提出其他要点,这些要点也应与您所指出的准则一起应用。这些似乎在您正在处理的代码中被忽略了。之所以发生这种情况,是因为您的同事仍处于学习阶段,在这种情况下,有必要指出他们代码的味道,因此最好记住他们是在善意地学习,尝试编写更好的代码。

Clean Code确实建议方法应简短,且参数应尽可能少。但是按照这些指导方针,它建议我们必须遵循S OLID原则,增加凝聚力并减少耦合

SOLID中的S代表“单一责任原则”,该原则指出对象应仅负责一件事。“事物”不是一个非常精确的术语,因此对该原理的描述千差万别。但是,《清洁法》的作者鲍伯叔叔也是创造这一原理的人,他形容为:“将由于相同原因而改变的事物聚集在一起,将因不同原因而改变的事物分开。” 他继续说出自己的意思,并有理由在 这里这里改变(这里的详细解释会太多)。如果这一原则应用到你正在处理的类,它很可能是件该处理的计算会从那些待分离处理保持状态,通过拆分类在两个或更多,取决于有多少理由改变那些计算了。

同样,Clean类应该是有凝聚力的,这意味着其大多数方法都使用其大多数属性。因此,最大内聚的类是所有方法都使用其所有属性的类。例如,在图形应用程序中,您可能有一个Vector具有属性Point a和的类Point b,其中唯一的方法是scaleBy(double factor)printTo(Canvas canvas),这两种方法都对这两个属性都起作用。相反,最小内聚类是这样一种类,其中每个属性仅在一种方法中使用,而每种方法使用的属性绝不超过一个。平均而言,一个类会呈现出具有内聚力的部分的非内聚性“组”-即,一些方法使用属性abc,而其余​​方法使用cd -表示如果将类一分为二,则会得到两个内聚的对象。

最后,清洁类应尽可能减少耦合。尽管这里有很多类型的耦合值得讨论,但似乎手头的代码主要受时间耦合的影响,正如您所指出的那样,对象的方法仅在按正确的顺序调用时才能按预期工作。就像上面提到的两个准则一样,解决此问题的方法通常是将类分为两个或多个内对象。在这种情况下,拆分策略通常涉及诸如Builder或Factory之类的模式,在高度复杂的情况下则涉及State-Machines。

TL; DR:您的同事遵循的“清洁代码”准则是好的,但前提是还要遵循本书中提到的其余原则,实践和模式。在清洁的“阶级”你看到的将被分成多个等级的版本,每个都有一个责任,有凝聚力的方法和没有时间耦合。在这种情况下,小型方法和很少使用的参数才有意义。


1
您和topo morto都写了一个很好的答案,但我只能接受一个。我喜欢您谈到SRP,凝聚力和耦合问题。这些是我可以在代码审查中使用的有用术语。将对象分解为具有自己职责的较小对象显然是可行的方法。一种将值初始化为一堆类属性的(非构造函数)方法是一个死胡扯,即应该返回一个新对象。我应该已经看到了。
user2180613 '16

1
SRP是最重要的准则;一环来统治他们所有的一切。SRP做得好自然会导致方法更短。示例:我有一个仅带有2个公共方法和大约8个非公共方法的前置类。没有超过3行;全班约35个LOC。但是我最后写了这堂课!在编写所有底层代码时,该类基本上已经编写了自己,而我不必,实际上也不必使方法更大。我从来没有说过:“如果杀死我,我将分5行编写这些方法。” 每次您应用SRP都会发生。
雷达波

11

据我了解,类构造函数完全负责创建有效对象,并且其主要任务是分配对象的(私有)属性。

通常负责将对象置于初始有效状态。然后其他属性或方法可以将状态更改为另一个有效状态。

但是,在我遇到的代码中,大多数属性值实际上是由构造方法以外的其他方法设置的。计算得出的值被分配给属性,以便在整个类的多个私有方法中使用。作者似乎在使用类属性,好像它们是应在整个类中访问的全局变量一样,而不是将这些值参数化为需要它们的函数。此外,应按特定顺序调用类的方法,因为否则该类不会做很多事情。

除了您提到的可读性和可维护性问题外,听起来类本身也正在进行多个阶段的数据流/转换,这可能表明该类违反了单一责任原则。

怀疑此代码是否受建议的启发而保持方法简短(<= 5行代码),以避免大的参数列表(<3个参数),并且构造函数不得进行工作(例如执行某种形式的计算,对于对象的有效性至关重要)。

遵循一些编码准则而忽略其他准则通常会导致愚蠢的代码。例如,如果我们要避免构造函数执行工作,那么明智的方法通常是在构造之前完成该工作,并将该工作的结果传递给该构造函数。(这种方法的一个论点可能是您避免给班级承担两个责任:初始化工作和“主要工作”,无论如何)。

以我的经验,将类和方法缩小是我单独考虑时很少要记住的事情-而是自然而然地遵循了单一职责。

但是,我宁愿建议完全更改代码,以使该类成为实际对象的蓝图,而不是应按特定顺序调用(过程上)的一系列方法。

您这样做可能是正确的。编写简单的过程代码没有错;滥用OO范式编写混淆的过程代码时存在问题。

我相信对于何时将值保存在类属性中以及何时将其放入参数中以供不同方法使用方面存在明显的区别-我真的不相信它们可以彼此替代。我正在寻找这种区别的词。

通常,您不应该在字段中放置值,以将其从一种方法传递到另一种方法。字段中的值应该是给定时间对象状态的有意义的部分。(我可以想到一些有效的异常,但不能想到此类方法是公共的或类的用户应注意的顺序依赖性)


2
支持:1.强调SRP。“……小方法……自然地遵循” 2.构造函数的目的-有效状态。3. “遵循一些编码准则,而忽略其他准则。” 这是编码的行尸走肉。
雷达波

6

您很可能会在这里针对错误的模式进行操作。小功能和低礼貌本身很少有问题。这里真正的问题是导致函数之间顺序依赖的耦合,因此寻找解决该问题的方法而不丢掉小函数的好处。

代码胜于雄辩。如果您实际上可以进行部分重构并显示出改进,则人们可以更好地接受这些类型的更正,也许可以作为一对编程练习来进行。当我这样做时,我常常发现平衡所有标准来实现正确的设计比我想的要困难得多。

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.