封装仍然是OOP坚持的大象之一吗?


97

封装告诉我将所有或几乎所有字段都设为私有,并通过getter / setter公开这些字段。但是现在出现了Lombok之类的库,这些库使我们可以用一个简短的注解公开所有私有字段@Data。它将为所有私有字段创建getter,setter和set构造函数。

有人可以向我解释将所有字段隐藏为私有,然后再通过一些额外的技术将所有字段公开的感觉是什么?为什么我们不仅仅使用公共字段呢?我觉得我们走了漫长而艰难的路,才回到起点。

是的,还有其他一些可以通过getter和setter方法工作的技术。我们不能通过简单的公共领域使用它们。但是出现这些技术仅仅是因为我们拥有众多特性-公共获取者/设置者背后的私有领域。如果我们没有这些财产,这些技术将另辟way径,并为公共领域提供支持。一切都将变得简单,我们现在不需要龙目岛了。

整个周期的整体意义是什么?在现实生活的编程中,封装真的有意义吗?


19
"It will create getters, setters and setting constructors for all private fields."-您描述此工具的方式,听起来好像是在维护封装。(至少在某种程度上是松散的,自动化的,有点贫血的模型。)那么到底是什么问题呢?
大卫

78
封装将对象的实现内部隐藏在其公共协定(通常是接口)之后。getter和setter的作用恰恰相反-它们公开了对象的内部,所以问题出在getter / setter而不是封装中。

22
@VinceEmigh数据类没有封装。它们的实例完全是原始元素的意义上的
Caleth

10
使用JavaBeans的@VinceEmigh 不是OO,它是Procedural。文献称它们为“对象”是历史的错误。
卡莱斯(Caleth),

28
这些年来,我对此进行了很多思考;我认为这是OOP的意图与实现不同的情况。在研究了SmallTalk之后,很明显,OOP的封装意图是什么(即,每个类都像一台独立的计算机,具有作为共享协议的方法),尽管出于我至今未知的原因,它肯定已经流行起来了不提供概念封装的getter / setter对象(它们不隐藏任何内容,不管理任何内容,除了数据外不承担任何责任),但是它们仍然使用属性。
jrh

Answers:


82

如果使用getters / setters公开所有属性,则只会获得仍在C或任何其他过程语言中使用的数据结构。它不是封装,而Lombok只是使使用过程代码的痛苦减轻了。Getters / setter与普通公共领域一样糟糕。真的没有区别。

而且数据结构不是对象。如果您从编写接口开始创建对象,则永远不会在接口上添加getter / setter。公开您的属性会导致意大利面条式程序代码,其中对数据的操作在对象外部,并遍及整个代码库。现在,您正在处理数据并使用数据进行处理,而不是与对象交谈。使用getter / setter,您将拥有数据驱动的过程编程,该过程以直接的命令方式进行操作。获取数据-做某事-设置数据。

如果采用正确的方法,那么在OOP中封装是一头大象。您应该封装状态和实现细节,以便对象可以对此进行完全控制。逻辑将集中在对象内部,而不会散布在整个代码库中。是的-封装在编程中仍然是必不可少的,因为代码将更易于维护。

编辑

看到正在进行的讨论后,我想添加几件事:

  • 通过getter / setter公开多少属性以及执行此操作的谨慎程度无关紧要。更具选择性将不会使您的代码带有封装。您公开的每个属性都将导致某些过程以命令性的方式处理该裸数据。您将更加灵活地选择扩展代码。这不会改变核心。
  • 是的,在系统范围内,您可以从其他系统或数据库获取裸数据。但是,这些数据只是另一个封装点。
  • 对象应该可靠。对象的整个想法是负责任的,因此您不必发出直接且必要的命令。取而代之的是,您要一个对象通过合同来做好它的工作。您可以安全地将代理部分委派对象。对象封装状态和实现细节。

因此,如果我们回到为什么应该这样做的问题。考虑以下简单示例:

public class Document {
    private String title;

    public String getTitle() {
        return title;
    }
}

public class SomeDocumentServiceOrHandler {

    public void printDocument(Document document) {
        System.out.println("Title is " + document.getTitle());
    }
}

这里,我们有文献通过吸气剂暴露的内部细节,并且具有外部在程序代码printDocument功能,其与该工作以外的对象。为什么这样不好?因为现在您只有C样式代码。是的,它是结构化的,但真正的区别是什么?您可以用不同的文件和名称来构造C函数。那些所谓的层正是这样做的。服务类只是一堆处理数据的过程。该代码难以维护,并且具有许多缺点。

public interface Printable {
    void print();
}

public final class PrintableDocument implements Printable {
    private final String title;

    public PrintableDocument(String title) {
        this.title = title;
    }

    @Override
    public void print() {
        System.out.println("Title is " + title);
    }
}

与这个比较。现在我们有了一个合同,该合同的实现细节隐藏对象内部。现在,您可以真正测试该类,并且该类正在封装一些数据。如何处理这些数据是一个关注的对象。为了与对象交谈,您现在需要他自己打印。那就是封装,那是一个对象。使用OOP,您将获得依赖注入,模拟,测试,单一职责和大量好处的全部功能。


评论不作进一步讨论;此对话已转移至聊天
maple_shaft

1
“字母/字母与普通公共领域一样糟糕” –至少在许多语言中,这是不正确的。通常,您不能覆盖纯字段访问,但是可以覆盖getter / setter。这使子类具有多功能性。这也使更改类的行为变得容易。例如,您可能从以私有字段为后盾的getter / setter开始,然后再移至其他字段或从其他值计算值。普通字段都无法做到。某些语言确实允许字段具有自动获取器和设置器,但Java不是这种语言。

嗯,还是不服气。您的示例还可以,但是仅表示一种不同的编码样式,而不是“不正确的编码”(不存在)。请记住,当今大多数语言都是多范式的,很少是纯粹的面向对象程序。如此努力地坚持“纯粹的面向对象概念”。作为示例-您不能在示例上使用DI,因为您已将打印逻辑耦合到子类。打印不应该是文档的一部分-printableDocument会将其可打印部分(通过吸气剂)暴露给PrinterService。
萨尔

一个适当的面向对象的方法来此,如果我们要一个“纯面向对象”的方法,会用的东西,实现了一个抽象PrinterService类请求,通过消息,到底应印什么-使用各种各样的GetPrintablePart。实现PrinterService的东西可以是任何类型的打印机-打印到PDF,屏幕,TXT ...您的解决方案使得无法将打印逻辑换成其他东西,从而导致耦合性更高且难以维护而不是您的“错误”示例。
T. Sar

最重要的是:Getter和Setter并不是邪恶的,也不是打破OOP的人-不了解如何使用它们的人是。您的案例案例是一个教科书示例,人们完全忽略了DI的工作原理。您“更正”的示例已经启用了DI,可以解耦,可以很容易地嘲笑……真的-还记得整个“优先考虑继承而不是继承”的事情吗?OO支柱之一?您只需按照重构代码的方式将其扔到窗口旁边即可。这在任何认真的代码审查中都无法实现。
T. Sar于2010年

76

有人可以解释一下,将所有字段隐藏为私有字段,然后通过一些额外的技术公开所有字段,这是什么感觉?为什么我们不仅仅使用公共字段呢?

感觉是你不应该那样做

封装意味着您只公开了实际上需要其他类访问的那些字段,并且您对此非常有选择性和谨慎。

千万不能只给每默认情况下所有字段的getter和setter!

这完全违背了JavaBeans规范的精神,具有讽刺意味的是,公共获取器和设置器的概念来自此。

但是,如果您看一下该规范,您会发现它打算非常有选择性地创建getter和setter,并且它讨论的是“只读”属性(无setter)和“仅写”属性(无吸气剂)。

另一个因素是,getter和setter不一定是对私有领域的简单访问。获取器可以以任意复杂的方式计算返回的值,也可以将其缓存。设置器可以验证值或通知侦听器。

就是这样:封装意味着您仅公开了实际需要公开的功能。但是,如果您不考虑需要公开的内容,而只是通过遵循一些语法转换就公开展示所有内容,那当然不是真正的封装。


20
“ [G]写信人和二传手不一定是简单的访问方式”,我认为这是关键。如果您的代码正在按名称访问字段,则以后不能更改行为。如果您使用的是obj.get(),则可以。
丹·安布罗焦

4
@jrh:我从来没有听说过Java称之为不良做法,这很普遍。
Michael Borgwardt '10

2
@MichaelBorgwardt 有趣;话题有点偏离,但我一直想知道为什么微软建议不要为C#这么做。我猜这些指导原则暗示着,在C#中,只能使用setter的方法就是将给定值转换为内部使用的其他值,以不会失败或具有无效值的方式(例如,具有PositionInches属性和PositionCentimeters属性)它会在内部自动转换为mm)?这不是一个很好的例子,但这是我目前能提出的最好的例子。
jrh

2
@jrh那个消息来源说不要为getter(而不是setter)这样做。
曼队长

3
@Gangnus,您真的看到有人在getter / setter中隐藏了一些逻辑吗?我们已经习惯了,getFieldName()成为我们的自动合同。我们不会期望背后有一些复杂的行为。在大多数情况下,封装是直截了当的。
伊兹萨萨尔·托莱根

17

我认为问题的症结在于您的评论:

我完全同意你的想法。但是我们必须在某个地方向对象加载数据。例如,来自XML。当前支持它的平台通过获取器/设置器做到这一点,从而降低了代码的质量和思维方式。龙目岛(Lombok)确实本身并不坏,但是它的存在表明我们有坏事。

您遇到的问题是您将持久性数据模型与活动数据模型混合在一起。

一个应用程序通常将具有多个数据模型:

  • 与数据库对话的数据模型,
  • 读取配置文件的数据模型,
  • 与另一个应用程序对话的数据模型,
  • ...

它实际上是在数据模型上用来执行其计算的。

通常,用于与外部通信的数据模型应该是隔离的,并且应与执行计算的内部数据模型(业务对象模型,BOM)无关

  • 独立的:这样您就可以根据需要在BOM表上添加/删除属性,而不必更改所有客户端/服务器,...
  • 隔离的:所有的计算都在不变量存在的BOM上执行,并且从一项服务更改为另一项服务或升级一项服务不会在整个代码库中引起波动。

在这种情况下,对于通信层中使用的对象来说,将所有项目公开或由getter / setter公开是非常好的。这些是普通对象,没有任何不变性。

另一方面,您的BOM应当具有不变式,这通常会避免使用大量的setter(getter不会影响不变式,尽管它们确实在一定程度上减少了封装)。


3
我不会为通信对象创建getter和setter。我只是将这些字段公开。为什么创建的工作多于有用的工作?
immibis

3
@immibis:我总体上同意,但是正如OP指出的那样,某些框架要求使用getter / setter,在这种情况下,您只需要遵守即可。请注意,OP使用的是一个库,该库可通过应用单个属性自动创建它们,因此对他而言几乎没有什么工作。
Matthieu M.

1
@Gangnus:这里的想法是将吸气剂/设置剂隔离到边界(I / O)层,这在正常情况下会变脏,但是应用程序的其余部分(纯代码)没有被污染并且保持优雅。
Matthieu M.

2
@kubanczyk:据我所知,正式定义是业务对象模型。它在这里对应于我所谓的“内部数据模型”,即您在应用程序“纯”核心中执行逻辑的数据模型。
Matthieu M.

1
这是务实的方法。我们生活在一个混合世界中。如果外部库可以知道将哪些数据传递给BOM构造函数,那么我们将只有BOM。
阿德里安·Iftode

12

考虑以下..

您有一个User具有属性的类int age

class User {
    int age;
}

您希望按比例放大,以便User有一个出生日期,而不是年龄。使用吸气剂:

class User {
    private int age;

    public int getAge() {
        return age;
    }
}

我们可以换int age一个更复杂的领域LocalDate dateOfBirth

class User {
    private LocalDate dateOfBirth;

    public int getAge() {
        LocalDate now = LocalDate.now();
        int year = ...; // calculate using dateOfBirth and now
        return year;
    }

    // other behaviors can now make use of dateOfBirth
}

不违反合同,不破坏代码。.无非就是扩大内部表示以为更复杂的行为做准备。

字段本身被封装。


现在,清除担忧。

龙目岛的@Data注释类似于Kotlin的数据类

并非所有类都代表行为对象。至于破坏封装,这取决于您对功能的使用。您不应该通过getter 公开所有字段。

封装用于隐藏类内结构化数据对象的值或状态

从更一般的意义上讲,封装是隐藏信息的行为。如果您滥用@Data,那么很容易假设您可能正在破坏封装。但这并不是说它没有目的。例如,JavaBean受到某些人的反对。但是,它已广泛用于企业开发中。

您是否可以得出结论,由于使用bean,企业发展是不好的?当然不是!要求与标准制定的要求不同。可以滥用豆子吗?当然!他们一直在受虐待!

龙目岛还支持@Getter@Setter独立-用你的要求需要。


2
这与将@Data注释拍打到类型上无关,该类型通过设计取消了所有字段的封装
Caleth,2017年

1
不,这些字段不是隐藏的。因为任何事情都会发生,以及setAge(xyz)
Caleth

9
@Caleth字段隐藏。您似乎在设置器中没有前置条件和后置条件,这是使用设置器的非常常见的用例。您的行为好像是一个field ==属性,即使在像C#这样的语言中,它也不是正确的,因为这两个属性都倾向于被支持(就像在C#中一样)。该字段可以移至另一个类,也可以交换为更复杂的表示形式……
Vince Emigh

1
我的意思是,这并不重要
-Caleth

2
@Caleth怎么不重要?那没有任何意义。您是说封装不适用,因为认为封装在这种情况下并不重要,即使它按照定义适用并且具有用例吗?
文斯·艾米

11

封装告诉我将所有或几乎所有字段都设为私有,并通过getter / setter公开这些字段。

这不是在面向对象的编程中定义封装的方式。封装意味着每个对象都应该像一个胶囊,其外壳(公共api)可以保护和调节对其内部(私有方法和字段)的访问,并使其看不见。通过隐藏内部结构,调用者不必依赖内部结构,从而可以在不更改(甚至重新编译)调用者的情况下更改内部结构。此外,封装仅通过使调用者可以使用安全操作,从而允许每个对象强制执行自己的不变式。

因此,封装是信息隐藏的一种特殊情况,其中每个对象都隐藏其内部并强制执行其不变式。

为所有字段生成getter和setter方法是一种非常弱的封装形式,因为内部数据的结构没有隐藏,并且无法强制执行不变式。它的确具有优点,您可以更改内部存储数据的方式(只要您可以与旧结构进行相互转换),而不必更改(甚至重新编译)调用方。

有人可以向我解释将所有字段隐藏为私有,然后再通过一些额外的技术将所有字段公开的感觉是什么?为什么我们不仅仅使用公共字段呢?我觉得我们走了漫长而艰难的路,才回到起点。

部分原因是由于历史性事故。罢工之一是,在Java中,方法调用表达式和字段访问表达式在调用位置在语法上是不同的,即用getter或setter调用替换字段访问会破坏类的API。因此,如果您可能需要一个访问器,则必须立即编写一个访问器,或者能够中断API。缺少语言级别的属性支持与其他现代语言(最著名的是C#EcmaScript)形成了鲜明的对比。

罢工2是JavaBeans 规范将属性定义为getters / setters,而字段不是属性。结果,大多数早期的企业框架都支持getter / setter,但不支持字段。到目前为止,这已经很久了(Java持久性API(JPA)Bean验证XML绑定的Java体系结构(JAXB)Jackson到目前为止,所有支持领域都还不错),但是旧的教程和书籍仍然存在,并且并不是每个人都知道情况已经改变。在某些情况下,缺少语言级别的属性支持仍然会很痛苦(例如,由于读取公共字段时不会触发单个实体的JPA延迟加载),但是大多数公共字段都可以正常工作。也就是说,我的公司使用公共字段为其REST API编写了所有DTO(毕竟,通过互联网传输的公共信息并没有太多:-)。

这就是说,龙目岛的@Data确实不是生成getter / setter方法:它也产生toString()hashCode()并且equals(Object),这是相当有价值的。

在现实生活的编程中,封装真的有意义吗?

封装是无价的或完全无用的,它取决于要封装的对象。通常,类中的逻辑越复杂,封装的好处就越大。

通常会过度使用每个字段的自动生成的getter和setter,但是在使用旧框架或使用字段不支持的偶然框架功能时可能很有用。

可以使用getter和命令方法来实现封装。设置器通常不合适,因为它们只能更改单个字段,而保持不变性可能需要一次更改多个字段。

摘要

getter / setter提供的封装效果很差。

Java中的getter / setter盛行是由于缺乏对属性的语言级别支持,以及其历史性组件模型中存在可疑的设计选择,这些选择已被许多教材和由他们所教的程序员所接受。

其他面向对象的语言(例如EcmaScript)在语言级别上也支持属性,因此可以引入getter而不破坏API。在这种语言中,可以在实际需要时使用吸气剂,而不是提前一天(如果您可能需要一天)来引入吸气剂,这将带来更加愉悦的编程体验。


1
强制其不变性?是英文吗?您能向一个非英语的人解释吗?不必太复杂-这里的某些人对英语的了解不够。
Gangnus

我喜欢您的依据,我喜欢您的想法(+1),但不喜欢实际结果。我宁愿使用私有字段和一些特殊的库通过反射为它们加载数据。我只是不明白你为什么要把你的答案当作反对我的论点?
Gangnus

@Gangnus 不变是一个不变的逻辑条件(in- 不变, -variant 表示变化)。许多函数的规则在执行之前和之后都必须为真(称为前提条件和后置条件),否则代码中将出现错误。例如,一个函数可能要求其参数不为null(前提条件),并且如果其参数为null则可能引发异常,因为这种情况将是代码中的错误(即,在计算机科学领域,该代码破坏了不变的)。
法拉普

@Gangus:不确定在这个讨论中有何思考;Lombok是注释处理器,即Java编译器的插件,在编译过程中会发出其他代码。但是可以肯定的是,您可以使用lombok。我只是说,在许多情况下,公共字段也一样工作,并且易于设置(并非所有编译器都会自动检测注释处理器……)。
meriton

1
@Gangnus:在数学和CS中不变式是相同的,但是在CS中还有一个附加的观点:从对象的角度来看,必须强制和建立不变式。从该对象的调用者的角度来看,不变性始终为真。
meriton

7

我确实已经问过自己这个问题。

但是,这并非完全正确。IMO的getter / setters流行是由Java Bean规范引起的,它需要使用它。因此,如果您愿意的话,它的主要功能不是面向对象的编程,而是面向Bean的编程。两者之间的区别在于它们所存在的抽象层。Bean更像是系统接口,即在更高层上。它们是从OO基础工作中抽象出来的,或者至少是要这样做的-与往常一样,事情被驱使得足够频繁。

我要说的是不幸的是,这种在Java编程中无处不在的Bean并没有增加相应的Java语言功能-我想到的是C#中的Properties概念。对于那些不了解它的人,它是一种语言构造,如下所示:

class MyClass {
    string MyProperty { get; set; }
}

无论如何,实际实现的实质仍然非常受益于封装。


Python具有类似的功能,其中属性可以具有透明的getter / setter。我敢肯定,还有更多的语言也具有类似的功能。
JAB

1
“ getter / setters IMO的流行是由Java Bean规范引起的,这需要它”是的,您是对的。谁说必须这样做?原因是什么?
Gangnus

2
C#方式与Lombok完全相同。我看不出任何真正的区别。实用性更高,但构思上显然很差。
Gangnus

1
@Gangnis规范需要它。为什么?请查看我的答案,以获取一个具体示例,说明为什么吸气剂与暴露字段不一样-尝试在没有吸气剂的情况下实现相同效果。
文斯·埃米

@VinceEmigh gangnUs,请:-)。(帮派走动,nus-螺母)。那么,仅在我们特别需要的地方使用get / set,而在其他情况下通过反射将其大量加载到私有字段中呢?
Gangnus

6

有人可以解释一下,将所有字段隐藏为私有字段,然后通过一些额外的技术公开所有字段,这是什么感觉?为什么我们不仅仅使用公共字段呢?我觉得我们走了漫长而艰难的路,才回到起点。

这里的简单答案是:您绝对正确。吸收器和设置器消除了大多数(但不是全部)封装价值。这并不是说任何时候只要您有一个get和/或set方法都破坏了封装,但是如果您盲目地将访问器添加到类的所有私有成员中,那么您做错了。

是的,还有其他一些可以通过getter和setter方法工作的技术。我们不能通过简单的公共领域使用它们。但是出现这些技术仅仅是因为我们拥有众多属性-公共获取者/设置者背后的私有领域。如果我们没有这些财产,这些技术将另辟way径,并为公共领域提供支持。一切都将变得简单,我们现在不需要龙目岛了。整个周期的整体意义是什么?在现实生活的编程中,封装真的有意义吗?

Getters是setters在Java编程中无处不在,因为JavaBean概念是作为将功能动态绑定到预构建代码的一种方式而被推崇的。例如,您可能在applet中有一个表单(有人记得吗?),该表单将检查您的对象,查找所有属性并显示as字段。然后,UI可以根据用户输入来修改那些属性。作为开发人员,您只需要担心编写类并将任何验证或业务逻辑放在那里等。

在此处输入图片说明

使用示例Bean

这本身并不是一个糟糕的主意,但是我从来都不是Java方法的忠实拥护者。这只是与事实相反。使用Python,Groovy等。更自然地支持这种方法的东西。

JavaBean之所以失控是因为它创建了JOBOL,即不懂OO的Java编写的开发人员。基本上,对象不过是一堆数据而已,所有逻辑都是用长方法写在外面的。因为这被认为是正常现象,所以像您和我这样的人对此表示怀疑。最近我看到了转变,这不是局外人的位置

XML绑定是一个棘手的问题。这可能不是反对JavaBeans的好战场。如果必须构建这些JavaBean,请尝试将它们排除在实际代码之外。将它们视为序列化层的一部分。


2
最具体,最明智的想法。+1。但是,为什么不编写一组类,每个类将大量地将私有字段加载到/从某个外部结构中保存呢?JSON,XML,其他对象。它可以与POJO一起使用,或者可能仅与带有注释的字段一起使用。现在我正在考虑替代方案。
Gangnus

@Gangnus猜测是因为这意味着很多额外的代码编写。如果使用反射,则只需编写一次序列化代码,它就可以序列化遵循特定模式的任何类。C#通过[Serializable]属性及其亲属支持此功能。当您只能利用反射来编写一种特定的序列化方法/类时,为什么还要编写101种特定的序列化方法/类呢?
法拉普

@Pharap我非常抱歉,但是您的评论对我来说太短了。“利用反射”?将如此不同的事物隐藏在一类中的感觉是什么?您能解释一下您的意思吗?
Gangnus

@Gangnus我的意思是反射,即代码检查其自身结构的能力。在Java(和其他语言)中,您可以获取类公开的所有功能的列表,然后将其用作从类中提取数据的方式,以将其序列化为XML / JSON。使用反射将是一次性的工作,而不是不断地开发新的方法来按类保存结构。这是一个简单的Java示例
法拉普

@Pharap这就是我在说的。但是为什么您称其为“杠杆”呢?我在这里的第一条评论中提到的替代方案仍然存在-(未打包)字段是否应具有特殊注释?
Gangnus

5

没有吸气剂,我们能完成多少工作?是否可以将其完全删除?这会带来什么问题?我们甚至可以禁止该return关键字吗?

事实证明,如果您愿意做的话,您可以做很多事情。那么信息如何从这个完全封装的对象中获得呢?通过合作者。

而不是让代码问您问题,而是告诉事情去做。如果这些东西也不能返回,那么您就不必为它们返回的东西做任何事情。因此,当您考虑伸手return去拿某个输出端口协作者时,可以做剩下的事情。

用这种方式做事会有好处和后果。您不仅需要考虑返回的内容,还需要考虑的更多。您必须考虑如何将其作为消息发送给不需要它的对象。可能是您传出了将返回的对象,或者只是调用一个方法就足够了。这样做是有代价的。

好处是,现在您正在面对界面进行交谈。这意味着您将获得抽象的全部好处。

它还可以为您提供多态调度,因为尽管您知道自己在说什么,但不必知道自己在说什么。

您可能会认为这意味着您只需要遍历一层堆栈即可,但是事实证明,您可以使用多态性向后移动而无需创建疯狂的循环依赖关系。

它可能看起来像这样:

在此处输入图片说明

class Interactor implements InputPort {
    OutputPort out;
    int y = 0;

    Interactor(OutputPort out){
        this.out = out;
    }

    void accumulate(int x) {
        y = y + x;
        out.showAsImage(y);
    }
}

如果可以这样编写代码,则可以选择使用getters。没有必要的邪恶。


是的,这样的获取者/设定者很有道理。但是您知道这与其他事情有关吗?:-)还是+1。
Gangnus

1
@Gangnus谢谢。您可能会谈论很多事情。如果您想至少给他们起个名字,我可以研究一下。
candied_orange

5

这是一个有争议的问题(如您所见),因为一堆教条和误解与对吸气剂和吸气剂问题的合理关注混杂在一起。简而言之,这没有什么问题,@Data并且不会破坏封装。

为什么要使用getter和setter而不是公共领域?

因为获取器/设置器提供封装。如果将值公开为公共字段,然后稍后更改为即时计算该值,则需要修改所有访问该字段的客户端。显然这很不好。是对象的某些属性是存储在字段中,是动态生成的还是从其他位置获取的,这是一个实现细节,因此,差异不应暴露给客户端。Getter / setter setter解决了这个问题,因为它们隐藏了实现。

但是,如果getter / setter仅仅反映了一个潜在的私有领域,那岂不是同样糟糕吗?

没有!关键是封装允许您更改实现而不影响客户端。只要客户不必了解或关心,字段可能仍然是存储价值的完美方法。

但是,不是从破坏封装的字段中自动生成吸气剂/吸气剂吗?

没有封装仍然存在!@Data批注只是编写使用基础字段的getter / setter对的便捷方法。对于客户而言,这就像常规的获取器/设置器对。如果您决定重写实现,则仍然可以执行而不会影响客户端。因此,您将两全其美:封装简洁的语法。

但是有些人说吸气剂/定阻剂总是不好的!

有一个单独的争议,其中一些人认为,无论采用哪种底层实现,吸气剂/设置剂模式总是不好的。这样的想法是,您不应设置对象或从对象中获取值,而应将对象之间的任何交互建模为消息,其中一个对象要求另一对象做某事。从早期的面向对象思维开始,这主要是一条教条。现在的想法是,对于某些模式(例如,值对象,数据传输对象),吸气器/设置器可能是完全合适的。


封装应使我们具有多态性和安全性。获取/设置允许第一个,但强烈反对第二个。
Gangnus

如果您说我在某处使用了教条,请说我的文字是教条。并且不要忘记,我正在使用的某些想法不喜欢或不同意。我用它们来证明矛盾。
Gangnus

1
“ GangNus” :-)。一个星期前,我从事了一个项目,该项目实际上充满了跨越多个层次的getter和setter方法。这绝对是不安全的,甚至更糟的是,它支持不安全编码的人员。我很高兴我已经足够快地换了工作,因为人们已经习惯了这种方式。所以,我不认为,我看到了
Gangnus

2
@jrh:我不知道是否有这样的正式解释,但是C#内置了对属性(语法更好的getter / setter)和匿名类型的支持,匿名类型是具有属性且没有行为的类型。因此,C#的设计人员故意偏离了消息传递的隐喻和“告诉,不要问”的原则。自2.0版以来,C#的开发似乎更多地受到功能语言的启发,而不是传统的OO纯度。
JacquesB

1
@jrh:我现在在猜测,但是我认为C#受到功能语言的启发,这些功能语言中的数据和操作更加分开。在分布式体系结构中,前景也从面向消息的(RPC,SOAP)转变为面向数据的(REST)。公开和操作数据(而非“黑匣子”对象)的数据库已盛行。简而言之,我认为重点已经从黑匣子之间的消息转移到公开的数据模型
JacquesB

3

封装确实有其目的,但也可能被滥用或滥用。

考虑像Android API这样的东西,它具有包含数十个(如果不是数百个)字段的类。暴露给API使用者的这些字段将使导航和使用变得更加困难,同时也给用户带来了错误的观念,即他可以对可能与应该如何使用它们冲突的那些字段执行自己想做的任何事情。因此,从可维护性,可用性,可读性以及避免疯狂的漏洞的角度而言,封装非常有用。

另一方面,POD或普通的旧数据类型(例如来自C / C ++的结构,其中所有字段都是公共的)也是有用的。在Lombok中使用无用的getter / setter(如@data批注生成的getter / setter)只是保持“封装模式”的一种方法。我们在Java中执行“无用” getter / setter的少数原因之一是方法提供了契约

在Java中,接口中不能包含字段,因此可以使用getter和setter来指定该接口的所有实现者都具有的公共属性。在像Kotlin或C#这样的较新的语言中,我们将属性的概念视为可以声明一个setter和getter的字段。最后,除非Oracle向其添加属性,否则无用的getter / setters更是Java所必须承受的遗产。例如,Kotlin是JetBrains开发的另一种JVM语言,其数据类基本上可以完成Lombok中的@data注释。

这里还有一些例子:

class DataClass 
{
    private int data;

    public int getData() { return data; }
    public void setData(int data) { this.data = data; } 
}

这是封装的不良情况。吸气剂和塞特剂实际上是无用的。大多使用封装,因为这是Java等语言的标准。除了在整个代码库中保持一致性之外,实际上并没有帮助。

class DataClass implements IDataInterface
{
    private int data;

    @Override public int getData() { return data; }
    @Override public void setData(int data) { this.data = data; }
}

这是封装的一个很好的例子。封装用于强制执行合同,在这种情况下为IDataInterface。在此示例中进行封装的目的是使此类的使用者使用接口提供的方法。即使getter和setter没什么花哨的东西,我们现在也定义了DataClass和IDataInterface的其他实现者之间的共同特征。因此我可以有一个这样的方法:

void doSomethingWithData(IDataInterface data) { data.setData(...); }

现在,在谈论封装时,我认为同样重要的是也要解决语法问题。我经常看到人们抱怨强制封装而不是封装本身所需的语法。想到的一个例子是Casey Muratori(您可以在这里看到他的怒吼)。

假设您有一个使用封装的玩家类,并且希望将其位置移动1个单位。代码如下所示:

player.setPosX(player.getPosX() + 1);

如果没有封装,它将看起来像这样:

player.posX++;

他在这里辩称,封装会导致更多类型的输入而没有其他好处,这在很多情况下可能是正确的,但请注意。该参数违反语法,而不是封装本身。即使在缺乏封装概念的C语言中,您也经常会在以'_'或'my'前缀或后缀的结构中看到变量,或者表示它们不被API使用者使用的任何东西,就好像它们是私人的。

事实是封装可以帮助使代码更易于维护和使用。考虑此类:

class VerticalList implements ...
{
    private int posX;
    private int posY;
    ... //other members

    public void setPosition(int posX, int posY)
    {
        //change position and move all the objects in the list as well
    }
}

如果在此示例中变量是公共的,则此API的使用者将对何时使用posX和posY以及何时使用setPosition()感到困惑。通过隐藏这些详细信息,您可以帮助消费者以直观的方式更好地使用您的API。

但是,语法是许多语言中的限制。但是,较新的语言提供了一些属性,这些属性使我们可以很好地使用publice成员的语法以及封装的好处。如果使用MSVC,即使在C ++中,您也可以在C#,Kotlin中找到属性。这是科特林的一个例子。

class VerticalList:... {var posX:Int set(x){field = x; ...} var posY:Int set(y){field = y; ...}}

在这里,我们实现了与Java示例相同的功能,但是我们可以使用posX和posY,就像它们是公共变量一样。但是,当我尝试更改其值时,将执行setter set()的主体。

例如,在Kotlin中,这相当于实现了getters,setters,hashcode,equals和toString的Java Bean:

data class DataClass(var data: Int)

请注意,此语法如何使我们可以在一行中完成一个Java Bean。您正确地注意到了Java之类的语言在实现封装时遇到的问题,但这是Java的问题而不是封装本身。

您说过使用Lombok的@Data生成getter和setter。注意名称@Data。它主要用于仅存储数据的数据类,并应进行序列化和反序列化。想一想游戏中的保存文件。但是在其他情况下,例如与UI元素一样,您最明确地希望使用setter,因为仅更改变量的值可能不足以获取预期的行为。


您能在这里举一个关于滥用封装的例子吗?那可能很有趣。您的示例恰好反对缺少封装。
Gangnus

1
@Gangnus我添加了一些示例。无用的封装通常会被滥用,而且根据我的经验,API开发人员也非常努力地强迫您以某种方式使用API​​。我没有为此举一个例子,因为我没有一个容易呈现的例子。如果找到一个,我将明确添加它。我认为大多数反对封装的评论实际上都是反对封装的语法,而不是反对封装本身。
BananyaDev

感谢您的修改。我发现了一个较小的错字:“ publice”而不是“ public”。
jrh

2

封装使您具有灵活性。通过分离结构和界面,您可以在不更改界面的情况下更改结构。

例如,如果发现需要基于其他字段来计算属性,而不是在构造时初始化基础字段,则只需更改吸气剂即可。如果您直接暴露了该字段,则必须更改界面并在每个使用站点进行更改。


从理论上讲,这是个好主意,但请记住,导致API版本2抛出版本1并未引发的异常会严重破坏您的库或类的用户(并且可能会无声!);我有些怀疑,对于大多数这些数据类,都可以在“幕后”进行任何重大更改。
jrh

抱歉,这不是解释。接口中禁止使用字段的事实来自封装思想。现在我们正在谈论它。您基于正在讨论的事实。(请注意,我不是在赞成或反对您的想法,只是关于他们不能在这里用作论点)
Gangnus

@Gangnus“接口”一词的含义不只是Java关键字:Dictionary.com/browse/interface
glennsl

@jrh这是一个完全不同的问题。您可以使用它在某些情况下反对封装,但是它绝不会使它的参数无效。
glennsl

1
没有质量获取/设置的旧封装不仅为我们提供了多态性,而且还提供了安全性。大量的设置/获取使我们学会了不良做法。
Gangnus

2

我将尝试说明封装和类设计的问题空间,并在最后回答您的问题。

如其他答案所述,封装的目的是将对象的内部细节隐藏在充当合同的公共API后面。该对象可以安全地更改其内部,因为它知道只能通过公共API对其进行调用。

具有公共字段,获取器/设置器,高层事务处理方法或消息传递是否有意义取决于要建模的域的性质。在《 Akka并发性》一书中(即使有些过时我也可以推荐),您可以找到一个示例来说明这一点,在此将其简称。

考虑一个用户类:

public class User {
  private String first = "";
  private String last = "";

  public String getFirstName() {
    return this.first;
  }
  public void setFirstName(String s) {
    this.first = s;
  }

  public String getLastName() {
    return this.last;
  }
  public void setLastName(String s) {
    this.last = s;
  }
}

这在单线程上下文中工作正常。被建模的域是一个人的名字,设置者可以完美地封装该名字的存储方式。

但是,想象一下必须在多线程上下文中提供它。假设一个线程定期读取该名称:

System.out.println(user.getFirstName() + " " + user.getLastName());

还有另外两个线程正在与拔河比赛,将其依次设在希拉里·克林顿唐纳德·特朗普身上。他们每个都需要调用两个方法。通常情况下,这种方法很好,但是偶尔您会看到希拉里·特朗普唐纳德·克林顿路过。

您无法通过在设置器中添加锁来解决此问题,因为该锁仅在设置名字或姓氏的过程中才保持。通过锁定的唯一解决方案是在整个对象周围添加一个锁定,但这会破坏封装,因为调用代码必须管理该锁定(并可能导致死锁)。

事实证明,没有通过锁定的干净解决方案。干净的解决方案是通过使内部更加粗糙来再次封装内部:

public class UserName {
   public final String first;
   public final String last;
   public UserName(String first, String last) { ... }
}

public class User
   private UserName name;
   public UserName getName() { return this.name; }
   public setName(UserName n) { this.name = n; }
}

名称本身已经变得不可变,您会看到它的成员可以是公共的,因为它现在是纯数据对象,一旦创建就无法对其进行修改。反过来,User类的公共API变得更加粗糙,只剩下一个设置器,因此名称只能整体更改。它在API后面封装了更多内部状态。

整个周期的整体意义是什么?在现实生活的编程中,封装真的有意义吗?

您在本周期中看到的是,尝试过于广泛地应用适合于特定情况的解决方案。合适的封装级别需要了解要建模的域并应用正确的封装级别。有时这意味着所有字段都是公共的,有时(例如在Akka应用程序中)意味着您完全没有公共API,只有一种接收消息的方法。但是,封装本身的概念(意味着将内部组件隐藏在稳定的API之后)是大规模编程软件的关键,尤其是在多线程系统中。


太棒了 我想说您将讨论提高了一个层次。也许两个。真的,我以为我了解封装技术,有时甚至比标准书更好,并且真的很害怕,由于一些公认的习惯和技术,我们实际上失去了封装技术,而那才是我的问题的真实主题(也许不是好的方法),但是您向我展示了封装的全新方面。我在多任务处理方面绝对不擅长,并且您向我展示了理解其他基本原理可能带来的危害。
Gangnus

但我不能标记您的文章作为答案,因为它是不是质量加载数据/从对象,由于这等平台,豆类和龙目岛出现的问题。也许,如果可以的话,您可以朝这个方向阐述自己的想法吗?我本人还没有重新考虑您的思想给该问题带来的后果。而且我不确定我是否适合它(记住,糟糕的多线程背景:-[)
Gangnus

我没有使用过lombok,但据我所知,它是一种使用较少的输入实现特定级别的封装(每个字段上的getter / setter)的方法。它不会改变问题域API的理想形状,它只是一种可以更快地编写它的工具。
Joeri Sebrechts

1

我可以想到一个有意义的用例。您可能有一个最初通过简单的getter / setter API访问的类。您稍后进行扩展或修改,使其不再使用相同的字段,但仍支持相同的API

一个有些人为的例子:一个点,以p.x()和组成笛卡尔对p.y()。稍后您将创建一个使用极坐标的新实现或子类,因此您也可以调用p.r()p.theta(),但是您的客户端代码可以调用p.x()p.y()保持有效。该类本身从内部极性形式透明转换,即y()now return r * sin(theta);。(在此示例中,仅设置x()y()没有太大意义,但仍然可以进行设置。)

在这种情况下,您可能会发现自己说:“很高兴我不愿自动声明getter和setter而不是公开这些字段,否则我不得不在那儿破坏我的API。”


2
它并不像您看到的那样好。更改内部物理表示确实会使对象与众不同。由于它们具有不同的特性,它们看起来相同,这很不好。笛卡尔系统没有特殊点,极地系统只有一个。
Gangnus

1
如果您不喜欢该特定示例,那么您肯定会想到确实具有多个等效表示形式的其他示例,或者可以与旧版本进行转换的较新版本。
戴维斯洛

是的,您可以非常高效地使用它们进行演示。在UI中可以广泛使用。但是你不是让我回答我自己的问题吗?:-)
Gangnus

这样更有效,不是吗?:)
戴维斯洛

0

有人可以解释一下,将所有字段隐藏为私有字段,然后通过一些额外的技术公开所有字段,这是什么感觉?

绝对没有意义。但是,您提出该问题的事实表明您不了解Lombok的功能,并且不了解如何使用封装编写OO代码。让我们倒带一点...

类实例的某些数据将始终是内部的,并且永远不应公开。类实例的某些数据将需要在外部设置,某些数据可能需要从类实例中传回。我们可能想改变类在表面下的填充方式,因此我们使用函数来获取和设置数据。

一些程序希望为类实例保存状态,因此它们可能具有一些序列化接口。我们添加了更多函数,这些函数使类实例将其状态存储到存储中并从存储中检索其状态。因为类实例仍在控制其自身的数据,所以这保持了封装。我们可能正在序列化私有数据,但是程序的其余部分无法访问它(或更准确地说,我们通过选择不故意破坏私有数据来维护中国墙),并且类实例可以(并且应该)对反序列化进行完整性检查,以确保其数据恢复正常。

有时,数据需要范围检查,完整性检查或类似的东西。自己编写这些功能可以让我们完成所有这些工作。在这种情况下,我们不需要或不需要龙目岛,因为我们自己做了所有事情。

但是,经常会发现,外部设置的参数存储在单个变量中。在这种情况下,您将需要四个函数来获取/设置/序列化/反序列化该变量的内容。每次自己编写这四个功能会减慢速度,并容易出错。使用龙目岛(Lombok)实现流程自动化,可以加快开发速度并消除出错的可能性。

是的,可以将该变量公开。在此特定版本的代码中,它在功能上是相同的。但是回到我们为什么使用函数的原因:“我们可能想改变类在表面下的填充方式……”如果将变量公开,那么您现在将永远约束代码,以使该公用变量成为接口。但是,如果您使用函数,或者使用Lombok自动为您生成这些函数,则将来可以随时更改基础数据和基础实现。

这样更清楚吗?


您正在谈论西南大学一年级学生的问题。我们已经在其他地方了。如果您不是以回答的形式那么不礼貌,我将永远不会这样说,甚至会为您提供一个明智的答案。
Gangnus

0

我实际上不是Java开发人员。但以下内容与平台无关。

我们编写的所有内容几乎都使用访问私有变量的公共获取器和设置器。大多数吸气剂和吸气剂都是微不足道的。但是,当我们确定设置器需要重新计算某些内容或设置器进行一些验证,或者需要将属性转发到此类的成员变量的属性时,这完全不会破坏整个代码并且与二进制兼容,因此我们可以换出一个模块。

当我们决定真正应该立即计算该属性时,不必更改所有看待该属性的代码,只需更改写入该属性的代码即可,IDE可以为我们找到它。当我们确定这是一个可写的计算字段时(只需执行几次),我们也可以这样做。令人高兴的是,这些更改中有相当一部分是二进制兼容的(更改为只读计算字段在理论上并不可行,但实际上仍然可以进行)。

我们最终得到了很多带有复杂设置器的琐碎吸气器。最后,我们还获得了许多缓存获取器。最终结果是,您可以假定吸气剂相当便宜,但塞特尔可能并非如此。另一方面,我们很明智地决定设置器不会持续存在于磁盘上。

但是我不得不追踪那个盲目地将所有成员变量更改为属性的人。他不知道原子加法是什么,因此他将确实需要作为公共变量的内容更改为属性,并以一种微妙的方式破坏了代码。


欢迎来到Java世界。(也是C#)。*叹*。但是,对于使用get / set来隐藏内部表示,请小心。它只关于外部格式,可以。但是,如果涉及数学,或更糟糕的是,涉及物理或其他真实事物,则不同的内部表示形式具有不同的性质。即是我对softwareengineering.stackexchange.com/a/358662/44104的
Gangnus,2015年

@Gangnus:最不幸的例子是。我们有很多这样的东西,但是计算总是准确的。
约书亚

-1

Getter和Setter是“以防万一”的一种措施,目的是在开发过程中内部结构或访问需求发生变化时避免将来进行重构。

假设发布后几个月,您的客户通知您某个类的字段有时会设置为负值,即使在这种情况下最多应固定为0。使用公共字段,您将必须在整个代码库中搜索该字段的每个赋值,以将钳制功能应用于将要设置的值,并请记住,在修改该字段时始终必须这样做,但这很糟糕。相反,如果您碰巧已经在使用getter和setter,则只需修改setField()方法即可确保始终应用此钳位。

现在,Java之类的“过时”语言存在的问题是,它们鼓励为此目的使用方法,这只会使您的代码无限冗长。编写起来很痛苦,而且很难阅读,这就是为什么我们一直在使用IDE来以某种方式缓解这一问题的原因。除非另有说明,否则大多数IDE都会自动为您生成getter和setter并隐藏它们。Lombok更进一步,它只是在编译时按程序生成它们,以使您的代码更加流畅。但是,其他更现代的语言只是简单地以一种或另一种方式解决了这个问题。例如,Scala或.NET语言,

例如,在VB .NET或C#中,您可以简单地使所有要具有普通字段(无副作用)的设置器和设置器只是公共字段,然后将它们设置为私有,更改其名称并使用以前的名称公开属性。字段,如果需要,您可以在其中微调字段的访问行为。使用Lombok,如果您需要微调getter或setter的行为,则可以仅在需要时删除这些标签,并使用新的要求编写自己的标签,从而知道您不必在其他文件中进行任何重构。

基本上,您的方法访问字段的方式应该透明且统一。现代语言使您可以使用与字段相同的访问/调用语法来定义“方法”,因此可以按需进行这些修改,而无需在早期开发过程中考虑太多,但是Java迫使您提前进行这项工作,因为它确实没有此功能。Lombok所做的所有事情为您节省了一些时间,因为您使用的语言不想让您节省时间以防万一。


在那些“现代”语言中,API 看起来像字段访问,foo.bar但是它可以通过方法调用来处理。您声称这优于使API 看起来像方法调用的“过时”方式foo.getBar()。我们似乎同意公共领域是有问题的,但是我声称“过时”的选择优于“现代”的选择,因为我们的整个API是对称的(所有方法调用)。在“现代”方法中,我们必须确定哪些事物应该是属性,哪些应该是方法,这会使所有事情变得过于复杂(尤其是如果我们使用反射!)。
Warbo

1
1.隐藏物理学并不总是那么好。看看我对softwareengineering.stackexchange.com/a/358662/44104的评论。2.我不是在说吸气/塞子的创作有多困难或多么简单。C#具有我们正在讨论的绝对相同的问题。由于质量信息加载的解决方案不好而导致封装不足。3.是的,该解决方案可以基于语法,但是请以与您提到的方式不同的方式进行。那些不好,我们在这里有一个大页面,里面有很多解释。4.解决方案不必基于语法。可以在Java限制内完成。
Gangnus

-1

有人可以向我解释将所有字段隐藏为私有,然后再通过一些额外的技术将所有字段公开的感觉是什么?那么为什么我们不仅仅使用公共字段呢?我觉得我们走了漫长而艰难的路,才回到起点。

是的,这是矛盾的。我首先遇到了Visual Basic中的属性。到那时,以我的其他语言来说,字段周围没有属性包装器。只是公共,私有和受保护的字段。

属性是一种封装。我理解Visual Basic属性是一种在隐藏显式字段甚至其数据类型的同时控制和操纵一个或多个字段的输出的方法,例如以特定格式将日期作为字符串发出。但是即便如此,从更大的对象角度来看,也不是“隐藏状态并公开功能”。

但是属性由于不同的属性获取器和设置器而自我证明。暴露不带属性的字段是全部或全部-如果您可以阅读它,则可以对其进行更改。因此,现在可以用受保护的二传手证明弱类设计。

那为什么我们/他们不只是使用实际方法呢?因为它是Visual Basic(和VBScript)(哦,啊啊!),是为大众(!)编码的,所以风行一时。因此,白痴统治最终占据了主导地位。


是的,UI的属性具有特殊意义,它们是字段的公共表示形式。好点子。
Gangnus

“那么为什么我们/他们不只是使用实际方法呢?” 从技术上讲,它们是在.Net版本中进行的。属性是get和set函数的语法糖
法拉普

“白痴”?你不是在说“ 特质 ”吗?
彼得·莫滕森

我的意思是专制
雷达波
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.