干净的代码和混合对象以及功能嫉妒


10

因此,我最近对代码进行了一些重大的重构。我试图做的主要事情之一就是将类分为数据对象和辅助对象。除其他外,这是由“ 清洁代码”的这一部分启发的:

杂种

这种混乱有时会导致不幸的混合数据结构,即一半对象和一半数据结构。它们具有执行重要功能的函数,也具有公共变量或公共访问器和更改器,它们出于所有意图和目的,将私有变量公开,从而诱使其他外部函数以程序程序将要使用的方式使用这些变量。数据结构。

这样的混合体很难添加新功能,但是也很难添加新的数据结构。他们是两全其美的。避免创建它们。它们表示设计混乱,其作者不确定(或者更糟,是无知)他们是否需要保护功能或类型。

最近,我正在查看我的一个工作对象(恰好实现了Visitor模式)的代码,并看到了:

@Override
public void visit(MarketTrade trade) {
    this.data.handleTrade(trade);
    updateRun(trade);
}

private void updateRun(MarketTrade newTrade) {
    if(this.data.getLastAggressor() != newTrade.getAggressor()) {
        this.data.setRunLength(0);
        this.data.setLastAggressor(newTrade.getAggressor());
    }
    this.data.setRunLength(this.data.getRunLength() + newTrade.getLots());
}

我立即对自己说:“功能令人羡慕!这种逻辑应该在Data类中-特别是在handleTrade方法中。handleTrade并且updateRun应该始终一起发生”。但是后来我想到“数据类仅仅是一个public数据结构,如果我开始这样做,那么它将成为一个混合对象!”

有什么更好的,为什么?您如何决定该做什么?


2
为什么“数据”必须完全是数据结构。它具有明确的行为。因此,只需关闭所有获取器和设置器,以使任何对象都无法操纵内部状态。
Cormac Mulhall 2014年

Answers:


9

您引用的文本有很好的建议,尽管我假设用struct之类的东西将“记录”替换“数据结构”。记录只是数据的愚蠢汇总。尽管它们可能是易变的(因此在函数式编程的思维方式中是有状态的),但它们没有任何内部状态,不需要保护任何不变式。向记录添加操作以使其更容易使用是完全有效的。

例如,我们可以说3D向量是一个哑记录。但是,这不应阻止我们添加类似的方法add,这使添加向量变得更加容易。添加行为不会将(不是很愚蠢的)记录变成混合记录。

当对象的公共接口允许我们破坏封装时,会越过此线:我们可以直接访问一些内部组件,从而使对象进入无效状态。在我看来,它Data确实具有状态,并且可以将其置于无效状态:

  • 处理交易后,最后一个侵略者可能不会更新。
  • 即使没有新交易发生,也可以更新最后一个侵略者。
  • 即使更新了攻击者,游程长度也可能保留其旧值。
  • 等等

如果任何状态对您的数据均有效,那么代码就可以正常工作了,您可以继续进行下去。否则:Data类负责自己的数据一致性。如果处理交易总是涉及更新侵略者,则此行为必须属于此类Data。如果更改攻击者涉及将游程长度设置为零,则此行为必须属于Data该类。Data从来都不是愚蠢的记录。通过添加公共设置器,您已经使其成为一个混合器。

在一种情况下,您可以考虑放宽这些严格的职责:如果Data项目是私有的,因此不属于任何公共接口,则仍然可以确保正确使用该类。但是,这有责任Data在整个代码中保持一致性,而不是在中央位置收集它们。

我最近写了一个关于封装答案,该答案更深入地介绍了什么是封装以及如何确保封装。


5

handleTrade()updateRun()总是一起发生的事实(第二种方法实际上是在访问者上,并且在数据对象上调用了其他几种方法)具有时间耦合的味道。这意味着您必须按特定的顺序调用方法,而我猜想,无序调用方法将在最坏的情况下破坏某些东西,或者在最好的情况下无法提供有意义的结果。不好。

通常,重构依赖关系的正确方法是使每个方法返回结果,该结果可以馈入下一个方法或直接作用。

旧代码:

MyObject x = ...;
x.actionOne();
x.actionTwo();
String result = x.actionThree();

新代码:

MyObject x = ...;
OneResult r1 = x.actionOne();
TwoResult r2 = r1.actionTwo();
String result = r2.actionThree();

这有几个优点:

  • 它将单独的关注点移动到单独的对象(SRP)中。
  • 它消除了时间耦合:不可能无序地调用方法,并且方法签名提供了有关如何调用它们的隐式文档。您是否曾经看过文档,看过想要的对象并向后工作?我想要对象Z。但是我需要Y才能获得Z。要获得Y,我需要X。我有一个W,要获得X,需要将W链接在一起。现在,您的W可以用于获得Z。
  • 像这样分割对象更有可能使它们变得不可变,这具有超出此问题范围之外的众多优点。快速的收获是,不可变的对象倾向于导致更安全的代码。

这两个方法调用之间没有时间耦合。交换顺序,其行为不会改变。
durron597 2014年

1
在阅读问题时,我最初还想到了顺序/时间耦合,但是后来发现该updateRun方法是private。避免顺序耦合是一个很好的建议,但是它仅适用于API设计/公共接口,不适用于实现细节。真正的问题似乎是updateRun应该在访问者中还是在数据类中,而且我看不出此答案如何解决该问题。
阿蒙2014年

的可见性updateRun无关紧要,重要的是this.data在问题中不存在其实现,并且访问者对象正在操纵该对象。

如果有的话,该访问者只是在呼叫一堆二传手,而实际上没有处理任何事情,这就是暂时耦合不存在的原因。设置员的呼叫顺序可能无关紧要。

0

从我的角度来看,一个应该包含“状态值(成员变量)和行为的实现(成员函数,方法)”。

如果将不公开的类状态成员变量(或它们的获取器/设置器)公开,就会出现“不幸的混合数据结构”。

因此,我认为不需要为数据数据对象和辅助对象使用单独的类。

您应该能够使状态成员变量保持非公开状态(您的数据库层应能够处理非公共成员变量)

功能羡慕对象是过度使用另一个类的方法的类。参见Code_smell。具有方法和状态的类可以消除这种情况。

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.