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

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

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

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

评论

“它将为所有私有字段创建getter,setter和设置构造函数。” -您描述此工具的方式,听起来好像是在维护封装。 (至少在某种程度上是松散的,自动化的,贫血的模型。)那么到底是什么问题呢?

封装将对象的实现内部隐藏在其公共协定(通常是接口)之后。吸气剂和吸气剂的作用恰恰相反-它们公开了对象的内部结构,因此问题出在吸气剂/吸气剂上,而不是封装。

@VinceEmigh数据类没有封装。它们的实例完全是原始类型的意义上的值

使用JavaBeans的@VinceEmigh不是OO,它是过程式的。文献称它们为“物体”是历史的错误。

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

#1 楼

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

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

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

编辑器

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


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

所以,如果我们回到为什么应该这样做的问题。考虑一个简单的例子:在这里,我们有文档通过getter公开内部细节,并在printDocument函数中具有外部过程代码,该代码与对象外部的代码一起工作。为什么这样不好?因为现在您只有C样式代码。是的,它是结构化的,但真正的区别是什么?您可以在不同的文件中使用名称来构造C函数。那些所谓的层正是这样做的。服务类只是一堆处理数据的过程。该代码不太容易维护,并且有很多缺点。

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


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

评论


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

– Maple_shaft♦
17-10-10在18:24

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

–凯特
17-10-11在0:39

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

– T. Sar
17-10-13在12:36



如果要采用“ Pure OO”方法,则适当的OO方法将使用实现抽象PrinterService类的东西,该类通过消息请求应打印的内容-使用各种GetPrintablePart。实现PrinterService的东西可以是任何类型的打印机-打印到PDF,屏幕,TXT ...您的解决方案使得无法将打印逻辑换成其他东西,从而导致耦合性更高且难以维护而不是您的“错误”示例。

– T. Sar
17-10-13在12:41

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

– T. Sar
17-10-13在12:42



#2 楼


有人可以解释一下,将所有字段都隐藏为私有是什么感觉,然后再通过一些额外的技术将所有字段都暴露出来?
为什么我们不只是简单地使用公共字段呢?


感觉是不应该这样做。

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

不要只为所有字段默认设置getter和setter!

这完全违反了JavaBeans规范的精神,具有讽刺意味的是,公共获取器和设置器的概念源自此。我会看到它打算非常有选择性地创建getter和setter,并讨论了“只读”属性(无setter)和“仅写”属性(无getter)。

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

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

评论


“ [G]写信人和二传手不一定是简单的途径”-我认为这是关键。如果您的代码正在按名称访问字段,则以后无法更改行为。如果您使用的是obj.get(),则可以。

–丹·安布罗焦
17-10-5在14:07

@jrh:我从来没有听说过它被称为Java的不良习惯,这很普遍。

–迈克尔·伯格沃德(Michael Borgwardt)
17-10-5在14:52

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

– jrh
17-10-5在15:05



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

–曼上尉
17-10-5在19:44

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

–伊兹巴萨尔·托莱根(Izbassar Tolegen)
17-10-6在16:10

#3 楼

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


我完全同意您的想法。但是我们必须在某个地方向对象加载数据。例如,来自XML。当前支持它的平台通过getter / setter来做到这一点,从而降低了代码的质量和思维方式。 Lombok本身本身并不坏,但是它的存在本身表明我们有坏处。


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

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


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

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

通常,用于与外部进行通信的数据模型应隔离并独立于要执行计算的内部数据模型(业务对象模型,BOM):




独立:这样您可以根据需要在BOM表上添加/删除属性,而无需更改所有客户端/服务器,...

隔离的:这样,所有计算都在BOM表上进行,其中不变量存在,并且那变化从一项服务到另一项服务,或升级一项服务,不会在整个代码库中引起涟漪。

在这种情况下,对于通信层中使用的对象来说,将所有项目公开或被吸气剂/设置者暴露。那些都是普通对象,没有任何不变性。

另一方面,您的BOM应该具有不变性,这通常排除了很多setter(getter不会影响不变式,尽管它们确实在一定程度上减少了封装) )。

评论


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

–user253751
17-10-5在22:45

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

– Matthieu M.
17-10-6在6:27

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

– Matthieu M.
17-10-6在12:32

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

– Matthieu M.
17-10-6在17:38

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

– Adrian Iftode
17-10-10在8:28

#4 楼

请考虑以下内容。.
您具有一个具有属性Userint 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
}

没有违反合同,没有代码破坏的问题。内部表示形式,以便为更复杂的行为做准备。


现在,为了消除顾虑。.
Lombok的@Data注释与Kotlin的数据类类似。
并非所有类都代表行为对象。至于破坏封装,这取决于您对功能的使用。您不应该通过getter公开所有字段。

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

封装是隐藏信息的行为。如果您滥用@Data,那么很容易假设您可能正在破坏封装。但这并不是说它没有目的。例如,JavaBean受到某些人的反对。但是它在企业开发中得到了广泛使用。
您是否可以得出结论,由于使用bean,企业开发是不好的?当然不是!要求与标准制定的要求不同。可以滥用豆子吗?当然!他们一直被滥用!
Lombok还独立支持@Getter@Setter-使用您的要求。

评论


这与在类型上拍打@Data注释无关,后者在设计上会取消封装所有字段

– Caleth
17-10-5在14:56

不,这些字段没有隐藏。因为任何事情都会发生,并且setAge(xyz)

– Caleth
17-10-5在15:11

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

–二恶英
17-10-5在15:16



我的意思是不重要

– Caleth
17-10-5在15:40

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

–二恶英
17-10-5在15:42



#5 楼


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


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

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

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


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


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

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

那就是说,Lombok的@Data做得更多而不是生成getter / setter:它还会生成toString()hashCode()equals(Object),它们可能非常有价值。 br />
封装是无价的或完全无用的,它取决于要封装的对象。通常,类中的逻辑越复杂,封装的好处就越大。

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

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

摘要

getters / setters

Java中的getters / setter方法很普遍,这是由于缺乏对属性的语言级别支持,以及其历史性组件模型中存在可疑的设计选择,这些选择已包含在许多教材和

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

评论


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

–绞肉
17-10-6在11:43



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

–绞肉
17-10-6在11:47

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

–法老王
17-10-6在12:25

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

– Meriton
17-10-6在12:37

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

– Meriton
17-10-8在21:38



#6 楼

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

但这并非完全正确。 IMO的getter / setters流行是由Java Bean规范引起的,它需要使用它。因此,如果您愿意的话,它的主要功能不是面向对象的编程,而是面向Bean的编程。两者之间的区别在于它们所在的抽象层。 Bean更像是系统接口,即在更高层上。它们是从OO基础工作中抽象出来的,或者至少是要-象往常一样,事物被驱动得过于频繁了。程序设计并没有增加相应的Java语言功能-我想到的是类似C#中的Properties概念的东西。对于那些不了解它的人来说,它是一种如下所示的语言结构:

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


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

评论


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

– JAB
17-10-5在13:53

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

–绞肉
17-10-5在13:55

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

–绞肉
17-10-5在13:56

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

–二恶英
17-10-5在14:21



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

–绞肉
17-10-6在11:58

#7 楼


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


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


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


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



使用示例Bean

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

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

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

评论


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

–绞肉
17-10-6在11:54

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

–法老王
17年10月6日在12:30

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

–绞肉
17-10-6在12:42

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

–法老王
17-10-6在12:47



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

–绞肉
17-10-6在12:56

#8 楼

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

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

而不是让代码问您问题,而是告诉事情要做的事情。如果这些东西也不能返回,那么您就不必为它们返回的东西做任何事情。因此,当您考虑使用return时,请尝试使用一些输出端口协作器,该协作器将完成剩下的工作。

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

好处是,现在您正在面对界面进行交谈。这意味着您可以充分利用抽象。

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

它可能看起来像这样:不是必需的邪恶。

评论


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

–绞肉
17-10-6在12:05

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

–candied_orange
17-10-6在12:18

#9 楼

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

为什么使用getter和setter而不是公共字段?

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

但是,如果getter / setter只反映了一个底层的私有字段,那岂不是一样糟糕?

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

但是,不是通过破坏封装的字段来自动生成获取器/设置器吗?

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

但是有些人说getter / setter总是很糟糕!

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

评论


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

–绞肉
17-10-6在13:06

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

–绞肉
17-10-6在13:39

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

–绞肉
17-10-6在19:40



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

–雅克B
17-10-6在22:21

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

–雅克B
17-10-7在8:44

#10 楼

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

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

另一方面,POD或普通的旧数据类型(例如来自C / C ++的结构,其中所有字段都是公共的)也是有用的。在Lombok中使用无用的getter / setter就像@data注释生成的那样,只是保持“封装模式”的一种方法。我们在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的其他实现者之间的共同特征。因此,我可以有一个这样的方法:现在,在谈论封装时,我认为它对于解决语法问题也很重要。我经常看到人们抱怨强制封装而不是封装本身所需的语法。想到的一个例子是Casey Muratori(您可以在这里看到他的怒吼)。

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

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


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

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


这里他认为封装会带来更多输入而没有更多好处,这在很多情况下是正确的,但会引起注意。该参数违反语法,而不是封装本身。即使在诸如C之类的缺少封装概念的语言中,您也经常会在以'_'或'my'前缀或后缀的结构中看到变量,以表明它们不应被API的使用者使用,就像它们被

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

player.posX++;


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

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

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

我们在这里实现了与Java示例相同的功能,但是我们可以像使用公共变量一样使用posX和posY。但是,当我尝试更改其值时,将执行setter set()的正文。例如,在Kotlin中,这等效于Java Bean,其中包含getter,setter,hashcode, equals和toString实现:

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


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

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

评论


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

–绞肉
17-10-6在12:25

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

–BananyaDev
17年10月7日在13:01

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

– jrh
17-10-7在13:25

#11 楼

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

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

拥有公共字段,getter / setter或更高级别的事务处理方法或消息是否有意义传递取决于要建模的域的性质。在《 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之后)是大规模编程软件的关键,尤其是在多线程系统中。

评论


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

–绞肉
17-10-10在7:43

但是我不能将您的帖子标记为答案,因为这与向对象/从对象大批量加载数据无关,这是由于出现诸如bean和Lombok之类的平台的问题。也许您可以朝这个方向阐述自己的想法,如果可以的话?我本人还没有重新考虑您的思想给该问题带来的后果。而且我不确定我是否适合它(请记住,糟糕的多线程背景:-[)

–绞肉
17-10-10在7:48

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

– Joeri Sebrechts
17-10-10在7:51

#12 楼

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

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

评论


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

– jrh
17-10-5在13:34



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

–绞肉
17-10-5在13:54

@Gangnus单词“ interface”在Java关键字之外具有含义:dictionary.com/browse/interface

– glennsl
17-10-5在14:17

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

– glennsl
17-10-5在14:24

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

–绞肉
17-10-6在11:32



#13 楼

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

一个有些人为的示例:一个点开始为带有p.x()p.y()的笛卡尔对。稍后您将创建一个使用极坐标的新实现或子类,因此您也可以调用p.r()p.theta(),但是调用p.x()p.y()的客户端代码仍然有效。该类本身从内部极性形式透明地转换,即y()现在将变为return r * sin(theta);。 (在此示例中,仅设置x()y()没有多大意义,但仍然可以实现。)在这种情况下,您可能会发现自己说:“很高兴我很麻烦地自动声明了吸气剂和吸气剂而不是将这些字段公开,否则我不得不在那儿破坏我的API。”

评论


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

–绞肉
17-10-6在11:40

如果您不喜欢该特定示例,则可以肯定会想到其他确实具有多个等效表示形式的人,或者可以将新的表示形式与旧的表示形式相互转换。

–戴维斯洛
17-10-6在12:06



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

–绞肉
17-10-6在12:10



这样更有效,不是吗? :)

–戴维斯洛
17-10-6在18:09

#14 楼


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


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

类实例的某些数据将始终是内部的,并且永远不应公开。类实例的某些数据将需要在外部设置,某些数据可能需要从类实例中传回。我们可能想更改类在表面下的填充方式,因此我们使用函数来获取和设置数据。一些序列化接口。我们添加了更多函数,这些函数使类实例将其状态存储到存储中并从存储中检索其状态。因为类实例仍在控制其自身的数据,所以这保持了封装。我们可能正在序列化私有数据,但是程序的其余部分无法访问它(或更准确地说,我们通过选择不故意破坏私有数据来维护中国墙),并且类实例可以(并且应该)对反序列化进行完整性检查,以确保其数据恢复正常。

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

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

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

是否可以这样做?更清晰?

评论


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

–绞肉
17-10-6在12:34



#15 楼

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

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

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

我们最终得到了很多许多琐碎的吸气剂和复杂的吸气器。最后,我们还获得了许多缓存获取器。最终结果是允许您假设吸气剂相当便宜,但塞特尔可能并非如此。另一方面,我们很明智地决定不让设置器持久化。

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

评论


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

–绞肉
17-10-6在12:11

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

–约书亚
17-10-6在15:19

#16 楼

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

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

现在,“过时”的问题像Java这样的语言鼓励它们为此使用方法,这只会使您的代码无限冗长。编写起来很痛苦,而且很难看懂,这就是为什么我们一直使用IDE来以某种方式缓解这一问题的原因。除非另有说明,否则大多数IDE都会自动为您生成getter和setter并隐藏它们。 Lombok更进一步,它只是在编译期间以过程方式生成它们,以使您的代码更加流畅。但是,其他更现代的语言只是简单地以一种或另一种方式解决了该问题。例如,Scala或.NET语言允许您安全地忽略方法,因为内置的解决方案使您在将来更改更改时可以保持外部接口/合同/ API /外观/其他内容,因此在将来是否真正需要getter和setters内部结构。

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

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

评论


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

–Warbo
17-10-5在17:05

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

–绞肉
17-10-6在12:32

#17 楼


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


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

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

但是属性是有道理的,因为属性的获取器和设置器是分开的。暴露没有属性的字段是全部还是什么-如果您可以读取它,则可以对其进行更改。因此,现在可以使用受保护的setter来证明弱类设计的合理性。

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

评论


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

–绞肉
17-10-6在12:03



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

–法老王
17-10-6在12:43



“白痴”?你不是说“特质”吗?

– Peter Mortensen
17-10-6在19:35

我的意思是白痴

– radarbob
17-10-7在4:39