it-swarm.cn

是否有任何理由使用“普通旧数据”类?

在旧版代码中,我偶尔会看到只不过是数据包装器的类。就像是:

class Bottle {
   int height;
   int diameter;
   Cap capType;

   getters/setters, maybe a constructor
}

我对OO的理解是,类是数据的结构以及对该数据进行操作的方法。)这似乎排除了这种类型的对象。对我来说,它们什么都不是不仅仅是structs,而且有点违背OO的目的,尽管它可能是代码的味道,但我认为它不一定是邪恶的。

是否存在这样的对象必要的情况?如果经常使用,是否会使设计令人怀疑?

44
Michael K

在我心中绝对不是邪恶的,没有代码的味道。数据容器是有效的OO公民。有时您希望将相关信息封装在一起。拥有类似这样的方法要好得多

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

使用类还可以使您向Bottle添加一个附加参数,而无需修改DoStuffWithBottle的每个调用方。并且,如果需要,您可以继承Bottle的子类,并进一步提高代码的可读性和组织性。

例如,还有一些普通的数据对象可以作为数据库查询的结果返回。我认为在这种情况下,针对他们的术语是“数据传输对象”。

在某些语言中,还有其他注意事项。例如,在C#中,类和结构的行为不同,因为结构是值类型,而类是引用类型。

68
Adam Lear

数据类在某些情况下有效。 DTO是Anna Lear提到的一个很好的例子。通常,您应该将它们视为方法尚未萌芽的类的种子。而且,如果您在旧代码中遇到了很多此类问题,请将它们视为强烈的代码气味。它们经常被那些从未进行过OO)编程的老C/C++程序员使用,它们是过程编程的标志。始终依靠getter和setter(或者更糟糕的是,直接访问非私有成员)可能会使您陷入麻烦,请考虑一个需要Bottle信息的外部方法示例。

Bottle是数据类):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

在这里,我们给了Bottle一些行为):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

第一种方法违反了Tell-Don't-Ask原则,通过保持Bottle愚蠢,我们让关于瓶子的隐式知识(例如使一个人(Cap)陷入困境) Bottle类之外的逻辑。当您习惯依靠吸气剂时,必须保持警惕,以防止发生这种“泄漏”。

第二种方法仅询问Bottle做其工作所需的内容,然后让Bottle来确定它是否易碎或大于给定大小。结果是该方法与Bottle的实现之间的耦合松散得多。令人愉快的副作用是该方法更清洁且更具表现力。

如果没有编写一些应该包含在这些字段中的类的逻辑,您将很少利用对象的这么多字段。弄清楚该逻辑是什么,然后将其移至其所属位置。

25
Mike E

如果这是您需要的东西,那很好,但是请,请,请这样做

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

而不是像

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

请。

7
compman

正如@Anna所说,绝对不是邪恶的。当然,您可以将操作(方法)放入类中,但前提是want to。您没有必须参加。

请允许我对必须将操作放入类的想法以及类是抽象的想法稍加抱怨。在实践中,这鼓励程序员

  1. 创建超出所需数量的类(冗余数据结构)。当数据结构包含的组件数量超出最低限度所需的数量时,该数据结构将不规范化,因此包含不一致的状态。换句话说,当更改它时,需要在一个以上的位置进行更改以保持一致。未能执行所有协调的更改会导致不一致,这是一个错误。

  2. 通过引入notification方法来解决问题1,以便如果修改了A部分,则它试图将必要的更改传播到B和C部分。这是建议使用get-and的主要原因。设置访问器方法。由于这是推荐的做法,因此似乎可以解决问题1,从而导致更多的问题1和解决方案2。这不仅导致由于未完全实现通知而导致的错误,而且还导致了性能失控通知的问题。这些不是无限的计算,只是很长的计算。

这些概念被认为是一件好事,通常由不需要在充满这些问题的百万行怪物应用程序中工作的老师讲授。

这是我尝试做的事情:

  1. 保持数据尽可能规范化,以便在对数据进行更改时,在尽可能少的代码点进行更改,以最大程度地降低进入不一致状态的可能性。

  2. 当必须对数据进行非规范化并且不可避免地需要冗余时,请不要使用通知来使其保持一致。相反,可以容忍暂时的不一致。通过仅执行此操作的过程来解决与定期扫描数据的不一致问题。这可以集中维护一致性的责任,同时避免了通知容易产生的性能和正确性问题。这导致代码更小,无错误且高效。

6
Mike Dunlavey

在游戏设计中,数千个函数调用的开销以及事件侦听器有时值得拥有仅存储数据的类,并使其他类遍历所有仅数据的类来执行逻辑,这是值得的。

3
AttackingHobo

同意安娜李尔,

在我心中绝对不是邪恶的,没有代码的味道。数据容器是有效的OO公民。有时您想将相关信息封装在一起。拥有这样的方法要好得多...

有时人们会忘记阅读1999 Java)编码约定,这使这种编程非常完美。这很明显。实际上,如果您避免使用它,则您的代码会发臭!/setter)

从Java代码约定1999:)合适的公共实例变量的一个例子是,该类本质上是一个数据结构,没有任何行为。 ,如果您使用的是结构而不是类(如果Java受支持的结构),那么将类的实例变量设为公共是合适的。- http://www.Oracle.com/technetwork/Java/javase/documentation/codeconventions-137265.html#177

如果正确使用,POD(普通的旧数据结构)比POJO更好,就像POJO通常比EJB更好一样。
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures

3
Richard Faber

由于某些原因,当您处理中型/大型应用程序时,这类类非常有用:

  • 创建一些测试用例并确保数据一致非常容易。
  • 它拥有涉及该信息的所有行为,因此减少了数据错误跟踪时间
  • 使用它们应该使方法args保持轻量级。
  • 使用ORM时,此类提供了灵活性和一致性。添加基于类中已经存在的简单信息计算出的复杂属性,可以编写一种简单方法。与必须检查数据库并确保所有数据库都进行了新修改的补丁相比,这更加敏捷和高效。

综上所述,以我的经验,它们通常比烦人有用。

3
guiman

即使在Java中,结构也有其位置。仅当满足以下两个条件时,才应使用它们:

  • 您只需要汇总没有任何行为的数据,例如作为参数传递
  • 聚合数据具有哪种值并不重要

如果是这种情况,那么您应该将这些字段设为公开,并跳过获取/设置程序。无论如何,getter和setter都是笨拙的,并且Java因为没有像有用的语言这样的属性而很愚蠢。由于类似struct的对象无论如何都不应该有任何方法,所以公共字段最有意义。

但是,如果其中任何一个都不适用,那么您正在处理一个真实的类。这意味着所有字段都应该是私有的。 (如果您绝对需要在更易于访问的范围内的字段,请使用getter/setter。)

要检查您的假定结构是否有行为,请查看何时使用这些字段。如果它似乎违反了 告诉,不要问 ,那么您需要将该行为转移到您的类中。

如果您的某些数据不应更改,那么您需要将所有这些字段定为最终字段。您可以考虑将类设为 不可变 。如果需要验证数据,请在设置器和构造函数中提供验证。 (一个有用的技巧是定义一个私有的setter,并仅使用该setter来修改您的类中的字段。)

您的Bottle示例很可能在两个测试中均未通过。您可能拥有(伪造的)代码,如下所示:

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

相反,它应该是

double volume = bottle.calculateVolumeAsCylinder();

如果更改高度和直径,它会是同一瓶吗?可能不是。这些应该是最终的。直径是否可以为负值?瓶子的高度必须比宽度高吗?上限可以为空吗?没有?您如何验证这一点?假设客户是愚蠢或邪恶的。 ( 不可能说出区别。 )您需要检查这些值。

这是您新的Bottle类的样子:

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}
3
Eva

恕我直言,在高度面向对象的系统中,通常没有这样的足够的类。我需要仔细证明这一点。

当然,如果数据字段具有广泛的范围和可见性,那么如果您的代码库中有成百上千个地方篡改了此类数据,那将是非常不希望的。这要求保持不变性的麻烦和困难。但是同时,这并不意味着整个代码库中的每个类都可以从信息隐藏中受益。

但是在许多情况下,此类数据字段的范围将非常狭窄。一个非常简单的示例是数据结构的私有Node类。如果说Node可以仅由原始数据组成,则通常可以通过减少正在进行的对象交互次数来大大简化代码。这是一种去耦机制,因为替代版本可能需要从Tree->NodeNode->Tree而不是Tree->Node Data

一个更复杂的示例是游戏引擎中经常使用的实体组件系统。在这些情况下,这些组件通常只是原始数据和类,就像您显示的那样。但是,由于通常只有一个或两个系统可以访问该特定类型的组件,因此它们的范围/可见性往往会受到限制。结果,您仍然倾向于在这些系统中维护不变性非常容易,而且,这样的系统几乎没有object->object互动,可以很轻松地了解鸟瞰图。

在这种情况下,就交互作用而言,您最终可能会得到更多类似的信息(此图表示交互作用,而不是耦合,因为耦合图可能包括下面第二张图片的抽象接口):

enter image description here

...与此相反:

enter image description here

……尽管依赖关系实际上正在流向数据,但前一种类型的系统往往更易于维护和进行正确性推理。您得到的耦合要少得多,这主要是因为许多事物可以转化为原始数据,而不是对象彼此交互形成非常复杂的交互图。

0
user204677