关于语言不可知:实践中的私人与公共成员(封装有多重要?)

关于语言不可知:实践中的私人与公共成员(封装有多重要?)

Private vs. Public members in practice (how important is encapsulation?)

面向对象编程的最大优势之一是封装,我们(或者至少,我已经)教过的"真理"之一是成员应该始终保持私密并通过访问者和变异器提供 方法,从而确保验证和验证更改的能力。

不过,我很好奇,这在实践中有多重要。 特别是,如果你有一个更复杂的成员(例如集合),那么将它公开而不是制作一堆方法来获取集合的密钥,添加/删除集合中的项目是非常诱人的, 等等

你一般遵守规则吗? 你的答案是否会改变,取决于它是为自己编写的代码还是其他人使用的代码? 这种混淆是否有更微妙的原因?


由于过去许多人不得不维护几年前的代码,我很清楚,如果一个成员属性被公开,它最终会被滥用。我甚至听到人们不同意访问者和变异者的想法,因为它仍然没有真正达到封装的目的,即"隐藏一个类的内部工作"。这显然是一个有争议的话题,但我的观点是"让每个成员变量都变得私密,主要考虑类必须做什么(方法),而不是让你如何改变内部变量"。


这取决于。这是必须务实决定的问题之一。

假设我有一个代表一个点的类。我可以为X和Y坐标设置getter和setter,或者我可以将它们公开并允许对数据进行免费的读/写访问。在我看来,这是可以的,因为这个类就像一个美化的结构 - 一个数据集合,可能附加了一些有用的功能。

但是,在很多情况下,您不希望提供对内部数据的完全访问权限,并依赖类提供的方法与对象进行交互。一个例子是HTTP请求和响应。在这种情况下,允许任何人通过网络发送任何内容是一个坏主意 - 它必须由类方法处理和格式化。在这种情况下,该类被视为实际对象而不是简单的数据存储。

这实际上取决于动词(方法)是否驱动结构或数据是否存在。


是的,封装很重要。公开底层实现(至少)有两件事是错误的:

  • 混合责任。呼叫者不应该或不想了解底层实现。他们应该只希望班级做好自己的工作。通过公开底层实现,你就是上课没有做好自己的工作。相反,它只是将责任推到了呼叫者身上。
  • 将您与底层实现联系起来。一旦暴露了底层实现,就会与它相关联。如果您告诉呼叫者,例如,下面有一个集合,您就无法轻松地将集合交换为新的实现。
  • 无论您是直接访问底层实现还是只复制所有底层方法,这些(和其他)问题都适用。您应该公开必要的实现,仅此而已。保持实施私密性使整个系统更易于维护。


    我希望尽可能长时间保持成员私密,并且只能通过getter访问em,即使是在同一个类中也是如此。我也尽量避免使用setter作为初稿来推广价值风格对象,只要它是可能的。使用依赖注入很多,你经常有setter但没有getter,因为客户端应该能够配置对象,但(其他人)不知道什么是实际配置,因为这是一个实现细节。

    问候,
    奥利


    我倾向于严格遵守规则,即使它只是我自己的代码。因为这个原因,我真的很喜欢C#中的Properties。它可以很容易地控制它给出的值,但你仍然可以将它们用作变量。或者将集合设为私有和公开等。


    基本上,信息隐藏是关于代码清晰度的。它旨在使其他人更容易扩展您的代码,并防止他们在使用您的类的内部数据时意外地创建错误。它基于这样的原则:没有人读过评论,特别是那些带有说明的评论。

    示例:我正在编写更新变量的代码,我需要确保Gui更改以反映更改,最简单的方法是添加一个访问器方法(也称为"Setter"),而不是更新数据已更新。

    如果我将这些数据公开,并且某些内容在不经过Setter方法的情况下更改变量(并且每次发誓时都会发生这种情况),那么有人需要花一个小时的时间进行调试,以找出未显示更新的原因。这同样适用于"获取"数据。我可以在头文件中添加注释,但很可能没有人会读到它,直到出现严重错误。使用私有方式执行它意味着无法进行错误,因为它将显示为一个容易定位的编译时错误,而不是运行时错误。

    根据经验,你想要将一个成员变量公开,并省略Getter和Setter方法的唯一时间,如果你想要明确表示改变它将没有副作用;特别是如果数据结构很简单,就像一个简单地将两个变量保存为一对的类。

    这应该是一个相当罕见的事件,因为通常你需要副作用,如果你创建的数据结构如此简单,你没有(例如配对),那么已经有一个更有效的书面可用在标准库中。

    话虽如此,对于大多数一次性使用无延伸的小程序,就像你在大学里学到的那样,它比任何东西都更"好习惯",因为你会记住写作过程中的那些,然后你就是'将它们交给他们,再也不要触摸代码。此外,如果您正在编写数据结构以了解它们如何存储数据而不是发布代码,那么有一个很好的论据,即Getters和Setters无法提供帮助,并会妨碍学习体验。

    只有当你到达工作场所或大型项目时,你的代码才会被不同的人编写的对象和结构调用,这使得这些"提醒"变得强大变得至关重要。它是否是一个单人项目是令人惊讶的无关紧要,原因很简单,"你从现在起六周后"与同事不同。而"我六个星期前"经常变得很懒惰。

    最后一点是,有些人非常热衷于信息隐藏,如果您的数据不必要地公开,他们会很恼火。最好是幽默他们。


    C#Properties'模拟'公共字段。看起来非常酷,语法真的加快了创建这些get / set方法的速度


    请记住在对象上调用方法的语义。方法调用是一种非常高级的抽象,可以通过各种不同的方式在编译器或运行时系统中实现。

    如果您正在调用的方法的对象存在于同一进程/内存映射中,则编译器或VM可以很好地优化方法以直接访问数据成员。另一方面,如果对象位于分布式系统中的另一个节点上,那么您无法直接访问其内部数据成员,但您仍然可以通过向其发送消息来调用其方法。

    通过编码到接口,您可以编写不关心目标对象存在位置或调用方法的代码,或者使用相同语言编写代码。

    在您实现集合的所有方法的对象示例中,当然该对象实际上是一个集合。所以这可能是继承比封装更好的情况。


    使工具适合工作...最近我在我当前的代码库中看到了一些这样的代码:

    1
    2
    3
    4
    private static class SomeSmallDataStructure {
        public int someField;
        public String someOtherField;
    }

    然后,这个类在内部用于轻松传递多个数据值。它并不总是有意义,但如果你只有DATA,没有方法,并且你没有将它暴露给客户,我发现它是一个非常有用的模式。

    我最近使用的是一个JSP页面,我在其中显示了一个数据表,在声明的顶部定义。所以,最初它是在多个数组中,每个数据字段一个数组......这在代码中很难通过在定义中彼此不相邻的字段一起显示...所以我创建了一个简单的像上面这样将它拉到一起的类...结果是真正可读的代码,比之前更加如此。

    道德...有时你应该考虑"接受的坏"替代方案,如果它们可以使代码更简单,更容易阅读,只要你仔细考虑并考虑后果......不要盲目地接受你所听到的一切。

    那就是说......公共吸气者和制定者几乎相当于公共领域......至少基本上(有一点灵活性,但是你应用于每个领域仍然是一个糟糕的模式)。

    甚至java标准库也有一些公共字段的情况。


    当我使对象有意义时,它们更易于使用且更易于维护。

    例如:Person.Hand.Grab(howquick,howmuch);

    诀窍不是将成员视为简单的值而是将对象本身视为对象。


    这一切都是为了控制人们可以用你给他们的东西做些什么。你控制得越多,你就可以做出越多的假设。

    另外,理论上你可以改变底层实现或者其他东西,但是因为大多数情况下它是:

    1
    2
    3
    private Foo foo;
    public Foo getFoo() {}
    public void setFoo(Foo foo) {}

    这有点难以证明。


    我的倾向是尽可能让一切都变得私密。这样可以尽可能明确地定义对象边界,并使对象尽可能地分离。我喜欢这个,因为当我必须重写一个我拙劣的第一个(第二个,第五个?)时间的对象时,它会将所包含的伤害保持在较少数量的对象中。

    如果将对象紧密地耦合在一起,将它们组合成一个对象可能会更直接。如果你放松了耦合约束,你就会回到结构化编程。

    如果您发现一堆对象只是访问器函数,那么您应该重新考虑对象分区。如果您没有对该数据执行任何操作,则它可能属于另一个对象的一部分。

    当然,如果你正在编写类似于库的东西,你想要尽可能清晰明了界面,以便其他人可以对它进行编程。


    当至少其中一个成立时,封装很重要:

  • 除了你之外的任何人都会使用你的课程(或者他们会破坏你的不变量,因为他们没有阅读文档)。
  • 任何不阅读文档的人都会使用你的课程(或者他们会破坏你认真记录的不变量)。请注意,此类别包括您 - 从现在起两年。
  • 在未来的某个时刻,某人将继承你的班级(因为当一个领域的价值发生变化时,可能需要采取额外的行动,因此必须有一个设定者)。
  • 如果它只适合我,并且在很少的地方使用,并且我不会继承它,并且更改字段不会使该类假定的任何不变量无效,只有这样我才会偶尔公开一个字段。


    我认为这个问题确实将封装的概念与"信息隐藏"混为一谈
    (这不是批评者,因为它似乎与'封装'概念的共同解释相匹配)

    但对我来说,'封装'是:

    • 将多个项目重新组合到容器中的过程
    • 容器本身重新组合物品

    假设您正在设计纳税人系统。对于每个纳税人,您可以将儿童的概念封装起来

    • 代表孩子的孩子名单
    • 考虑到来自不同父母的孩子的地图
    • 对象儿童(非儿童)提供所需信息(如儿童总数)

    这里有三种不同的封装,2由低级容器(列表或映射)表示,一种由对象表示。

    通过做出这些决定,你不会

    • 使封装成为公共的或受保护的或私有的:仍然要做出"信息隐藏"的选择
    • 做一个完整的抽象(你需要优化对象子的属性,你可能决定创建一个对象Child,它只保留纳税人系统的相关信息)
      抽象是选择对象的哪些属性与系统相关的过程,必须完全忽略。

    所以我的观点是:
    这个问题的标题可能是:
    实践中的私人与公共成员(信息隐藏有多重要?)

    不过我只差2美分。我非常尊重人们可能会认为封装是一个包括"信息隐藏"决定的过程。

    但是,我总是试图区分"抽象" -"封装" -"信息隐藏或可见性"。


    @VonC

    您可能会发现国际标准化组织的"开放分布式处理参考模型",这是一本有趣的读物。它定义了:"封装:只有通过对象支持的接口上的交互才能访问对象中包含的信息的属性。"

    我试图在这里为信息隐藏提供一个案例:这个定义的关键部分:
    http://www.edmundkirwan.com/encap/s2.html

    问候,

    埃德。


    我发现许多getter和setter都是代码味道,程序的结构设计得不好。您应该查看使用这些getter和setter的代码,并查找真正应该属于该类的功能。在大多数情况下,类的字段应该是私有实现细节,只有该类的方法可以操作它们。

    让getter和setter都等于公共字段(当getter和setter是微不足道/自动生成时)。有时候将字段声明为public可能会更好,这样代码就会更简单,除非你需要多态或框架需要get / set方法(并且你不能更改框架)。

    但也有一些情况下,有吸气剂和制定者是一个很好的模式。一个例子:

    当我创建应用程序的GUI时,我尝试将GUI的行为保存在一个类(FooModel)中,以便可以轻松地对其进行单元测试,并在另一个类(FooView)中实现GUI的可视化,可以对其进行测试只能手动。视图和模型用简单的胶水代码连接起来;当用户更改字段x的值时,视图会在模型上调用setX(String),这反过来可能会引发模型的其他部分已更改的事件,并且视图将从模型中获取更新的值与吸气剂。

    在一个项目中,有一个GUI模型,它有15个getter和setter,其中只有3个g??et方法很简单(这样IDE就可以生成它们)。所有其他包含一些功能或非平凡的表达式,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public boolean isEmployeeStatusEnabled() {
        return pinCodeValidation.equals(PinCodeValidation.VALID);
    }

    public EmployeeStatus getEmployeeStatus() {
        Employee employee;
        if (isEmployeeStatusEnabled()
                && (employee = getSelectedEmployee()) != null) {
            return employee.getStatus();
        }
        return null;
    }

    public void setEmployeeStatus(EmployeeStatus status) {
        getSelectedEmployee().changeStatusTo(status, getPinCode());
        fireComponentStateChanged();
    }

    我几乎一直遵循这个规则。对我来说有四种情况 - 基本上,规则本身和几个例外(所有受Java影响):

  • 可以通过当前类之外的任何东西使用,通过getter / setter访问
  • 内部到类的使用通常以'this'开头,以表明它不是方法参数
  • 意味着保持极小的东西,比如运输物体 - 基本上是属性的直接射击;所有公众
  • 某些类型的扩展需要非私有

  • 这里有一个实际问题,大多数现有答案都没有解决这个问题。封装以及将干净,安全的接口暴露给外部代码总是很好,但是当您编写的代码旨在被空间和/或时间上较大的"用户"基础消耗时更为重要。我的意思是,如果你计划某人(甚至你)将代码很好地保存到未来,或者如果你正在编写一个模块,该模块将与来自少数其他开发人员的代码进行交互,那么你需要考虑更多如果您编写的代码是一次性的或完全由您编写的,请仔细阅读。

    老实说,我知道这是多么糟糕的软件工程实践,但我通常会先将所有内容公之于众,这使得记忆和输入的速度更快,然后在有意义的时候添加封装。如今,大多数流行IDE中的重构工具都会使您使用哪种方法(添加封装与取消封装)的相关性远低于以往。


    当然,无论是编写内部代码还是代码供其他人使用(或者甚至是自己,但作为一个包含的单元),它都会有所不同。)任何将在外部使用的代码都应该有一个明确定义/记录的界面我想尽可能少地改变。

    对于内部代码,根据难度,您可能会发现现在以简单的方式执行操作的工作量较少,稍后会付出一点代价。当然,墨菲定律将确保短期收益将被多次删除,因为您需要在以后需要更改未能封装的类内部时进行广泛的更改。


    我的决定基于模块中代码的深度。
    如果我正在编写模块内部的代码,并且不与外部世界接口,那么我不会将这些代码与private一起封装,因为它会影响我的程序员性能(我可以多快地编写和重写代码)。

    但是对于服务器作为模块与用户代码的接口的对象,我遵守严格的隐私模式。


    在实践中,我总是只遵循一条规则,即"不适合所有人"规则。

    封装及其重要性是您项目的产物。什么对象将访问您的界面,他们将如何使用它,如果他们对成员有不必要的访问权限,这是否重要?在处理每个项目实施时,您需要问自己这些问题以及他们喜欢的问题。


    特别是对于使用您将返回的集合的示例,似乎可能会更改此类集合的实现(与更简单的成员变量不同),从而使封装的实用性更高。

    话虽这么说,我有点像Python处理它的方式。成员变量默认是公共的。如果要隐藏它们或添加验证,则提供了一些技术,但这些都被视为特殊情况。


    推荐阅读