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


43

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

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

   getters/setters, maybe a constructor
}

我对OO的理解是,类是数据的结构以及对该数据进行操作的方法。这似乎排除了此类对象。对我而言,它们无非是structs一种失败的面向对象的目的。我认为这不一定是邪恶的,尽管它可能是一种代码味道。

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


1
这并不能完全回答您的问题,但是仍然很相关:stackoverflow.com/questions/36701/struct-like-objects-in-java
Adam Lear

2
我认为您正在寻找的术语是POD(普通旧数据)。
加拉夫

9
这是结构化编程的典型示例。不一定坏,只是不面向对象。
康拉德·鲁道夫

1
这不应该在堆栈溢出吗?
Muad'Dib 2010年

7
@ Muad'Dib:从技术上讲,它关于程序员。您的编译器不在乎是否使用普通的旧数据结构。您的CPU可能会喜欢它(采用“我喜欢从缓存中获取的新鲜数据气味”之类的方式)。这是谁得到挂了“这是否让我的方法不太纯的?” 问题。
Shog9

Answers:


67

在我心中绝对不是邪恶的,没有代码的味道。数据容器是有效的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#中,类和结构的行为不同,因为结构是值类型,而类是引用类型。


25
没事 DoStuffWith应该是一个方法Bottle类OOP(应该最有可能是不可变的,太)。上面您所写的内容不是OO语言中的好模式(除非您与旧版API交互)。但是,它在非OO环境中的有效设计。
Konrad Rudolph 2010年

11
@哈维尔:那么Java也不是最好的答案。Java对OO的重视是压倒性的,该语言基本上在其他方面都不擅长。
Konrad Rudolph 2010年

11
@JohnL:我当然是在简化。但是通常,OO中的对象封装状态,而不是数据。这是一个很好的但重要的区别。OOP的重点恰好是没有大量数据。它在创建新状态的状态之间发送消息。我看不到如何向无方法对象发送消息或从无方法对象发送消息。(是OOP的原始定义,所以我不会认为它有缺陷。)
Konrad Rudolph 2010年

13
@Konrad Rudolph:这就是为什么我在方法内部明确进行注释的原因。我同意影响Bottle实例的方法应该在该类中。但是,如果另一个对象需要根据Bottle中的信息修改其状态,那么我认为我的设计将是相当有效的。
亚当李尔

10
@Konrad,我不同意doStuffWithBottle应该进入瓶子类。为什么瓶子应该知道如何自己做东西?doStuffWithBottle表示其他事情将对瓶子起作用。如果瓶子里有那个,那将是紧密的耦合。但是,如果Bottle类具有isFull()方法,那将是完全合适的。
米(Nemi)2010年

25

数据类在某些情况下有效。DTO是Anna Lear提到的一个很好的例子。通常,您应该将它们视为方法尚未发芽的类的种子。而且,如果您在旧代码中遇到了很多此类问题,请将它们视为强烈的代码气味。它们经常被从未使用过OO编程的老C / C ++程序员使用,并且是过程编程的标志。始终依靠获取器和设置器(或者更糟糕的是,直接访问非私有成员)会使您不知所措。考虑需要从中获取信息的外部方法的示例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沉默,我们让关于瓶的隐性知识(例如使一个人(the Cap)陷入困境)陷入了Bottle类之外的逻辑。当您习惯依赖吸气剂时,必须警惕,以防止此类“泄漏”。

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

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


1
不能相信这个答案没有投票(好吧,你现在有票)。这可能是一个简单的示例,但是当OO被充分滥用时,您将获得服务类,这些类变成了噩梦,其中包含大量本应封装在类中的功能。
Alb 2012年

“由从未进行面向对象转换的原始C / C ++程序员”?C ++程序员通常是相当OO,因为它是一个面向对象的语言,使用C ++而不是C.的即整点
nappyfalcon

7

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

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;
    }
}

请。


14
唯一的问题是没有验证。关于什么是有效Bottle的任何逻辑都应该在Bottle类中。但是,使用您提出的实现,我可以得到一个高度和直径为负的瓶子-无法在不每次使用该对象时都进行验证的情况下,对该对象实施任何业务规则。通过使用第二种方法,根据我的合同,我可以确保如果我有一个Bottle对象,它过去,现在和将来始终是有效的Bottle对象。
汤玛斯·欧文斯

6
这是.NET在属性上略有优势的领域,因为您可以添加具有验证逻辑的属性访问器,语法与访问字段时相同。您也可以定义一个属性,类可以在其中获取属性值,但不能对其进行设置。
JohnL 2010年

3
@ user9521如果您确定您的代码不会导致带有“错误”值的致命错误,那么请使用您的方法。但是,如果需要进一步的验证,或者需要使用延迟加载的能力,或者在读取或写入数据时进行其他检查,则可以使用显式的getter和setter。就我个人而言,我倾向于将变量保持私有状态,并使用getter和setter来保持一致性。这样,不管验证和/或其他“高级”技术如何,所有变量都被相同地对待。
乔纳森

2
使用构造函数的优点是,使类变得不可变更加容易。如果您想编写多线程代码,这是必不可少的。
Fortyrunner 2010年

7
我会尽可能使字段最终确定。恕我直言,我希望默认情况下首选字段为final,并且具有可变字段的关键字。例如var
Peter Lawrey 2010年

6

正如@Anna所说,绝对不是邪恶的。当然,您可以将操作(方法)放入类中,但前提是您愿意。你不具备对。

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

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

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

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

这是我尝试做的事情:

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

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


3

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

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

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


3

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


3

同意安娜李尔,

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

有时,人们会忘记阅读1999 Java编码约定,这使这种编程非常好很清楚。实际上,如果您避免使用它,那么您的代码就会闻起来!(太多的获取者/设置者)

从Java Code Conventions 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

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

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

如果是这种情况,则应将这些字段设为公开字段,并跳过获取/设置方法。无论如何,getter和setter都是笨拙的,而Java由于没有像有用语言这样的属性而很愚蠢。由于类结构对象无论如何都不应具有任何方法,因此公共字段最有意义。

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

}

0

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

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

但是在许多情况下,此类数据字段的范围将非常狭窄。一个非常简单的示例是Node数据结构的私有类。如果说Node可以仅由原始数据组成,它通常可以通过减少正在进行的对象交互次数来大大简化代码。充当由于替代形式的脱钩机制可能需要从,比如说双向耦合,Tree->NodeNode->Tree与简单地Tree->Node Data

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

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

在此处输入图片说明

...与此相反:

在此处输入图片说明

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


-1

OOP世界中甚至存在着许多奇怪的生物。我曾经创建一个类,它是抽象的,不包含任何内容,只是为了成为其他两个类的共同父类……这是Port类:

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends Port

因此,SceneMember是基类,它负责显示在场景上的所有工作:添加,删除,获取ID等。消息是端口之间的连接,它具有自己的生命。InputPort和OutputPort包含自己的I / O特定功能。

端口为空。它仅用于将InputPort和OutputPort组合在一起,例如一个端口列表。它是空的,因为SceneMember包含所有用作成员的内容,而InputPort和OutputPort包含端口指定的任务。InputPort和OutputPort如此不同,以至于它们没有共同的功能(SceneMember除外)。因此,端口为空。但这是合法的。

也许这是一种反模式,但我喜欢它。

(这就像“伴侣”一词,用于“妻子”和“丈夫”。您永远不会将“伴侣”一词用于具体的人,因为您知道他/她的性别,这并不重要(如果他/她是否已婚),则您改用“某人”,但它仍然存在,并且在罕见的抽象情况下使用,例如法律文本。)


1
为什么您的端口需要扩展SceneMember?为什么不创建要在SceneMembers上运行的端口类?
Michael K 2010年

1
那么为什么不使用标准标记器界面模式呢?基本上与空的抽象基类相同,但这是更常见的习惯用法。
TMN 2010年

1
@Michael:仅出于理论上的原因,我保留了Port供将来使用,但这个未来尚未到来,也许永远不会到来。我没有意识到他们根本没有共同点,不像他们的名字那样。我将为因空旷的课堂而遭受任何损失的所有人赔偿...
ern0 2010年

1
@TMN:SceneMember(派生)类型具有getMemberType()方法,在某些情况下,会在Scene中扫描SceneMember对象的子集。
ern0

4
这不能回答问题。
伊娃(Eva)
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.