是否应该始终完全封装内部数据结构?


11

请考虑此类:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

此类将用于存储数据的数组公开给任何感兴趣的客户端代码。

我是在正在开发的应用中完成此操作的。我有一个ChordProgression存储序列Chords的类(并执行其他一些操作)。它有一个Chord[] getChords()返回和弦数组的方法。当必须更改数据结构(从数组更改为ArrayList)时,所有客户端代码都将中断。

这让我思考-也许以下方法更好:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThing(int index){
        return things[index];
    }

    public int getDataSize(){
        return things.length;
    }

    public void setThing(int index, Thing thing){
        things[index] = thing;
    }

}

现在,不再公开数据结构本身,而是使用委托给数据结构的公共方法,由数据结构提供的所有操作直接由包含它的类提供。

当数据结构更改时,仅这些方法就必须更改-但更改后,所有客户端代码仍然有效。

请注意,比数组更复杂的集合可能需要封闭的类来实现甚至三个以上的方法来访问内部数据结构。


这种方法常见吗?你觉得这怎么样?它还有什么缺点?让封闭的类至少实现三个公共方法以委托给内部数据结构是否合理?

Answers:


14

像这样的代码:

   public Thing[] getThings(){
        return things;
    }

没什么意义,因为您的访问方法除了直接返回内部数据结构外什么也不做。您不妨声明Thing[] thingspublic。访问方法背后的想法是创建一个接口,以使客户端与内部更改隔离,并使客户端无法操作实际的数据结构,除非采用接口允许的谨慎方式。如您所见,当所有客户端代码都损坏时,您的访问方法并没有做到这一点-只是浪费了代码。我认为许多程序员倾向于这样编写代码,因为他们了解到某个地方所有内容都必须使用访问方法进行封装-但这就是我解释的原因。当访问方法没有任何作用时,只是为了“遵循表单”而已。

我绝对会推荐您提出的解决方案,该解决方案可以实现封装的一些最重要的目标:为客户提供健壮,谨慎的接口,以使它们与类的内部实现细节隔离开来,并且不允许他们接触内部数据结构以您认为合适的方式期望-“最低必要特权法”。如果您看看流行的大型OOP框架(例如CLR,STL,VCL),正是由于这个原因,您提出的模式已经很广泛了。

你应该一直这样做吗?不必要。例如,如果您有帮助者或朋友类,它们实际上是您的主要工作者类的组成部分,并且不是“正面”的,则没有必要-这是一个过大的选择,它将添加许多不必要的代码。在那种情况下,我根本不会使用访问方法-如所解释的那样,这是毫无意义的。只需以仅适用于使用它的主类的方式声明数据结构-大多数语言都支持这样做的方法- friend或在与主worker类相同的文件中声明它,等等。

我可以在您的建议中看到的唯一缺点是,编码需要更多的工作(现在您将不得不重新编码消费者类-但您仍然必须这样做。)但这并不是真正的缺点。 -您需要做正确的事,有时这需要更多的工作。

使一个好的程序员变得更好的一件事是,他们知道什么时候额外的工作值得,什么时候不值得。从长远来看,现在投入额外的资金将在将来获得丰厚的回报-如果不在该项目上,那么在其他项目上。学习以正确的方式编码并运用您的头脑,而不仅仅是机械地遵循规定的形式。

请注意,比数组更复杂的集合可能需要封闭的类来实现甚至三个以上的方法来访问内部数据结构。

如果要通过包含类(IMO)公开整个数据结构,则需要考虑一下为什么要封装该类,如果不是简单地提供一个更安全的接口-“包装器类”。您说的是为此目的而存在的包含类-因此,您的设计可能不正确。考虑将您的班级细分为更谨慎的模块并进行分层。

一门课程应该有一个明确而谨慎的目的,并提供一个接口来支持该功能,而不再是。您可能正在尝试将不属于一起的东西捆绑在一起。当您这样做时,每次必须实施更改时,事情都会崩溃。您的课程越小越谨慎,就可以更轻松地改变周围的事物:思考乐高。


1
谢谢回答。一个问题:内部数据结构是否可能具有5种公共方法-所有这些都必须由班级的公共接口来体现?例如,一个Java的ArrayList有如下方法:get(index)add()size()remove(index),和remove(Object)。使用建议的技术,包含此ArrayList的类必须具有五个公共方法才能委托给内部集合。此类在程序中的用途很可能不是封装此ArrayList,而是执行其他操作。ArrayList只是一个细节。[...]
阿维夫·科恩

内部数据结构只是普通成员,使用上面的技术-要求它包含的类具有附加的五个公共方法。您认为-这合理吗?而且-这很常见吗?
阿维夫·科恩

@Prog- 如果内部数据结构可能有5个公共方法,该怎么办... IMO如果您发现需要在主类中包装整个助手类并以这种方式公开,则需要重新考虑一下设计-您的公共课程做得太多和/或没有提供适当的接口。一个类应具有非常谨慎和明确定义的角色,并且其界面应仅支持该角色。考虑分解和分层您的课程。一个类不应是以封装名称包含各种对象的“厨房水槽”。
矢量

如果要通过包装程序类(IMO)公开整个数据结构,则需要考虑为什么不仅仅为了提供一个更安全的接口而将该类完全封装。您说的是为此目的不存在包含类-因此,此设计有些不对劲。
矢量

1
@Phoshi- 关键字是只读 -我可以同意。但是OP并不是在谈论只读。例如remove不是只读的。我的理解是,OP希望将所有内容公开-就像在提议的更改之前的原始代码中public Thing[] getThings(){return things;}那样。这就是我不喜欢的。
矢量

2

您问:我是否应该始终完全封装内部数据结构?

简要答案:是的,大多数时候但并非总是如此

长答案:我认为课程分为以下几类:

  1. 封装简单数据的类。示例:2D点。创建提供获取/设置X和Y坐标功能的公共函数很容易,但是您可以轻松隐藏内部数据而不会带来太多麻烦。对于此类,不需要公开内部数据结构的详细信息。

  2. 封装集合的容器类。STL具有经典的容器类。我认为std::stringstd::wstring那些中了。它们提供了丰富的接口,以应对抽象,但是std::vectorstd::stringstd::wstring还提供了可以访问原始数据的能力。我不会急于将它们称为设计不良的类。我不知道这些类公开其原始数据的理由。但是,在我的工作中,由于性能原因,我发现有必要在处理数百万个网格节点和这些网格节点上的数据时公开原始数据。

    公开类的内部结构的重要之处在于,在发出绿色信号之前,您必须认真思考。如果该接口在项目内部,则将来更改它会很昂贵,但并非不可能。如果接口在项目外部(例如,当您正在开发将由其他应用程序开发人员使用的库时),则在不丢失客户的情况下更改接口可能是不可能的。

  3. 本质上大多数是功能类。示例:std::istream,,std::ostreamSTL容器的迭代器。公开这些类的内部细节是完全愚蠢的。

  4. 混合类。这些是封装某些数据结构但也提供算法功能的类。就我个人而言,我认为这是设计不当的结果。但是,如果找到它们,则必须决定根据情况公开其内部数据是否有意义。

结论:我发现唯一需要公开一个类的内部数据结构的时间是当它成为性能瓶颈时。


我认为STL公开其内部数据的最重要原因是与期望指针的所有函数的兼容性,这很多。
思远任

0

尝试直接执行以下操作,而不是直接返回原始数据

class ClassA {
  private Things[] things;
  ...
  public Things[] asArray() { return things; }
  public List<Thing> asList() { ... }
  ...
}

因此,您实质上是在提供一个定制集合,该集合可以呈现所需的任何面孔。在您的新实现中,

class ClassA {
  private List<Thing> things;
  ...
  public Things[] asArray() { return things.asArray(); }
  public List<Thing> asList() { return things; }
  ...
}

现在,您具有正确的封装,隐藏实现细节,并提供向后兼容性(需要付费)。


向后兼容的好主意。但是:现在您有了正确的封装,隐藏实现细节 -并非如此。客户仍然必须处理的细微差别List。仅仅返回数据成员的访问方法,即使使用强制转换来使事情变得更健壮,也不是真正好的封装IMO。worker类应该处理所有这些,而不是客户端。客户必须是“笨拙的”,它将变得更加健壮。顺便说一句,我不知道你回答的问题...
矢量

1
@Vector-你是正确的。返回的数据结构仍然可变,并且副作用将杀死信息隐藏。
BobDalgleish 2014年

返回的数据结构仍然可变,并且副作用将杀死信息隐藏 -是的,这也很危险。我只是在考虑客户的要求,这是问题的重点。
矢量

@BobDalgleish:为什么不返回原始集合的副本?
乔治(Giorgio)2014年

1
@BobDalgleish:除非有良好的性能原因,否则我将考虑返回对内部数据结构的引用,以允许其用户更改它们是一个非常糟糕的设计决定。对象的内部状态只能通过适当的公共方法进行更改。
乔治

0

您应该为这些事情使用接口。由于Java的数组未实现这些接口,因此对您的情况无济于事,但是从现在开始您应该这样做:

class ClassA{

    public ClassA(){
        things = new ArrayList<Thing>();
    }

    private List<Thing> things; // stores data

    // stuff omitted

    public List<Thing> getThings(){
        return things;
    }

}

这样,您可以更改ArrayListLinkedList或其他任何内容,并且不会破坏任何代码,因为所有具有(pseudo?)随机访问权限的Java集合(数组之外)都将实现List

您还可以使用Collection,它提供的方法要少于List但可以支持没有随机访问的集合,或者Iterable甚至可以支持流,但是在访问方法方面却不提供太多...


-1-妥协程度不高且不是特别安全的IMO:您正在向客户端公开内部数据结构,只是对其进行了屏蔽,并希望达到最佳效果,因为“ Java集合...将可能实现List”。如果您的解决方案是真正基于多态性/继承性的-所有集合都始终List作为派生类实现,那将更有意义,但仅“希望达到最佳”并不是一个好主意。“一个好的程序员会在一条单向的道路上两面兼顾”。
矢量

@Vector是的,我假设将来的Java集合将实现List(或Collection,或至少Iterable)。这就是这些接口的全部要点,很遗憾Java Arrays没有实现它们,但它们是Java集合的正式接口,因此假设任何Java集合都将实现它们并不是难事-除非该集合较旧比List,在这种情况下,用AbstractList包装它非常容易。
Idan Arye 2014年

您是在说您的假设几乎可以保证是成立的,所以好吧-因为您足够讲解,而且我不是Java专家,所以我将撤消不赞成投票(如果允许的话)除了通过渗透。尽管如此,无论如何完成,我都不支持公开内部数据结构的想法,并且您还没有直接回答OP的问题,它实际上是关于封装的。即限制对内部数据结构的访问。
矢量

1
@Vector是的,用户可以将强制List转换为ArrayList,但是这并不是100%受保护的实现-您始终可以使用反射来访问私有字段。这样做是不受欢迎的,但是铸造也是不受欢迎的(虽然不那么多)。封装的目的不是防止恶意黑客入侵-而是防止用户依赖于您可能想要更改的实现细节。使用List接口确实可以做到这一点-类的用户可以依赖于List接口,而不是ArrayList可能会更改的具体类。
伊丹·阿里2014年

您总是可以肯定地使用反射来访问私有字段 -如果有人想编写不良代码并颠覆设计,他们可以这样做。相反,它是为了防止用户...... -这是一个原因封装。另一个是确保类内部状态的完整性和一致性。问题不在于“恶意黑客攻击”,而是导致不良bug的不良组织。“最不必需的特权法”-仅向类的消费者提供强制性的内容-不再更多。如果必须公开整个内部数据结构,那么就会遇到设计问题。
矢量

-2

隐藏内部数据结构与外界隔绝是很常见的。有时,这在DTO中是过大的。我建议将此用于域模型。如果完全需要公开,则返回不可变的副本。与此同时,我建议创建一个具有get,set,remove等方法的接口。


1
这似乎并没有提供超过3个先前答案的实质性内容
t
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.