23

getter是设计对象的失败。它违反了封装这一面向对象编程的核心原则。

现在请告诉我,你是如何在这种理念下设计一个图书馆散列表集合的?

我最近刚开业我的回答前面两行。他们引起了一位长期贡献者的回应:

“现在请告诉我,你是如何在这种理念下设计一个图书馆散列表集合的?”——我在这里感觉到一种消极的氛围。我不是在评判,我有时也会这么做。如果我觉得有真诚的努力去理解,我很乐意回答。

罗伯特·布劳蒂加姆

我用这个问题来回答这个问题。当然,由于标题和正文不同,听起来好像我在问两个问题,但我在这里尝试的不仅仅是哈希表设计。

我开始相信面向对象编程很好。但并不是程序的每一行都可以是纯面向对象的。这很好。就像你不能编写一个没有静态方法的程序一样(你至少需要main),但这并不意味着它必须是静态的。

这个想法正是我那个有点笨拙的例子想要说明的。但如果这个想法有缺陷,我很想知道为什么。

OOP的一个实践是,当一个方法需要数据时,最好是移动方法,而不是移动数据以将它们放在一起。这将启用封装。状态不是可以分享的东西,只是改变行为的东西。

然而,一些边界(如库边界)使得无法移动方法,因此您需要移动数据。因此,充斥着getter的数据传输“对象”诞生了。因此,OOP理想在现实面前受到了损害。面对这种情况,我并没有完全放弃,而是认为这是一种孤立而非扩散的妥协。这就是为什么我认为OOP在任何地方都是不可能实现的,但当你能做到的时候,仍然认为OOP是好的。

要获得额外的积分,请务必向我展示OOP纯粹主义者如何在没有getter的情况下设计哈希表。

40
  • @Ewan这个词吸气剂这里不是用来指一个getter从技术上讲但任何类型的返回对象内部状态的方法(包括getter)。
    ——Ced公司
    5月11日21:06
  • 4
    @Ewan Getters是返回状态的方法,在某些语言中,为了更好地使用而存在特殊语法是无关紧要的。 5月11日21:06
  • 40
    “getter是设计对象的失败。”-我认为问题的关键是,从表面上看,这是一个过于笼统的说法。许多人对getter所做的就是公开每个私有字段。虽然这提供了某种程度的间接寻址,但它是最小的,如果您的目标是首先封装内部构件,那么这样做很快就会很难在不进行大量重写的情况下更改这些内部构件,或提取足够抽象的接口。那是人们所警惕的。但是,如果获取值是一种核心行为,那么getter就可以了。 5月11日23:06
  • 14
    很明显,你想给罗伯特·B一个回答这个问题的舞台。但也许更诚实的头衔应该是“你能把Robert B的面向对象编程思想推进多远?”,或“你能把Tell-Don't-Ask推多远”因为IMHO是Robert面向对象方法的核心点。还要注意的是,还有其他思想学派,他们不会将OOP与TDA混为一谈。 5月12日6:17
  • 8
    你——或者你所指的人,我在这里无法真正解决——可能正遭受着我所说的“客体幸福障碍”的折磨:认为OOP本身就是一个目标,而不是实现目标的一种技巧。要问的问题不是“这个选择是否符合OOP教条?”而是这个选择是否使我的程序更正确、更清晰、更可用、更健壮、功能更完整、更可扩展,或者其他任何实际目标。
    ——利珀特
    5月13日17:50

15个答案15

重置为默认值
10

然而,一些边界(如库边界)使得无法移动方法,因此您需要移动数据。

我同意。有时您不知道(或不知道)将附加到数据的行为,在这些情况下,您将发布数据。

所以,如果你的问题是,你是否可以(以合理的方式)总是编写代码时不返回实例变量/内部状态,即“getter”,我的答案是.

例子:在这里是我编写的一个小型HTTP/REST库。它有两个“getter”,比如内容响应.getContent()正如@ced所指出的,域是从HTTP调用返回“content”,我不知道该如何使用该内容,因此getter是合适的。

但是,这是我的观点,这些异常会发生远的比人们想象的要少。就像少了几个数量级。

这就好比人们一个月只洗一两次澡,我认为你应该每天洗澡,而你却在说“他不是指每一个 单一的天?".

就像上面的库一样,总共有大约3个getter,即使有了库的新功能,也不太可能增长。

即使在以下文章中:数据边界是维护问题的根本原因,我非常有意识地反复说,这只适用于“内部”应用程序(或库)。这并不是说,库边界可以充满getter。很多时候你可以找到一个合适的行为,而不是给出数据。

我的观点是,大多数设计1.adj.千疮百孔的通过面向数据的内部边界、DTO、Bean、层等,我们可以讨论OOP的精细点和灰色区域,但事实是,对于大多数项目来说,我们甚至还没有接近这些点。

现在我们发现有时在库的外部接口中使用getter是可以的。一个更有趣的问题是,可以有一个分层体系结构干净的体系结构里面应用程序的。如果是,在什么情况/要求下。

18
  • 2
    我同意DTO,但Account.display确实比Account好。获取总计作为基本类型()?在我看来,NumberView泄漏到Account的次数与Account泄漏到UI的次数一样多
    ——伊万
    5月12日11:02
  • 1
    @Ewan的一个区别是,您现在使用的是一个类,而不是两个类(视图可以被隐藏),域对象在某种程度上充当带有帐户.displayCard,帐户.displayDetail。这也比更常见的方法增加了更多的可发现性:账户卡(account),AccountDetailView(帐户)。另一个好处是,您的对象现在是一棵树,域位于顶部。因此,您的对象树从业务相关性到细节。
    ——Ced公司
    5月12日13:00
  • 1
    @但最重要的是,您的帐户对象已关闭。你知道如何使用它的数据。你不能提前知道账户。获取总计作为基本类型()。结果可以用于显示、计算、任何实际情况,关键是你不知道。这是否是一个真正的问题是另一个问题
    ——Ced公司
    5月12日13:01
  • 1
    @Ewan继承不会更改接口。包装不会将其与数据一起移动。它确实允许您隔离非面向对象的代码。所以也许我们用不同的方式说同样的事情。我只是想确定一下。
    ——糖果橙
    5月12日17:03
  • 1
    我发现在处理来自多个外部源的相互依赖数据时,将数据+行为封装到单个类中是有问题的;例如,考虑一个业务规则,它使用来自入站HTTP请求的数据来获取缓存在redis中的JWT Bearer令牌,这反过来又导致我们查询数据库(基于JWT声明和入站请求),这反过来需要向下游API发送请求,试图将这个事件链及其所有数据封装到一个类中有可能成为“上帝对象”。 5月13日7:25
26

我认为所有getter在某些基本级别上都违反了oop原则的原则是错误的。如果我有一个字符串类,我想通过调用获取长度这是否意味着它不是纯OOP?不!当然不会;这太荒谬了。字符串具有长度。这是字符串的固有属性。就像人们有名字、年龄、生日和颜色偏好一样。公开这些属性并不违反oop中的封装原则。

共享模型或问题域固有的信息不会违反OOP。如果无法获取数组、列表或集合的内容,那么它们有什么用?这就暴露了封装作为一种原则所要解决的局限性。封装是为了防止实现耦合。它并没有试图阻止数据的耦合。如果我打电话获取长度那么我就不能处理没有长度的字符串。因此,我耦合或依赖于该数据或细节。

发生耦合。为了构建软件,它必须发生。但OOP所要处理的只是实现耦合,而不是数据或接口等其他形式的耦合。这是OOP试图解决或真正提供机制来让你解决它的一个低得多的标准。批评者很快就会混淆所有形式的封装,认为这是在某种程度上诋毁OO在从未打算解决所有形式的情况下没有解决所有形式的理由。

这就是接口概念的由来。因为OO程序将自己与使用的接口耦合,这意味着调用的方法。但如果另一个对象实现了相同的接口,则可以替换一个换另一个,代码仍然可以工作。因此,大多数OO语言都提供了一些机制来声明这些接口,以正式声明这些集成点。大多数还没有发展到关注基于方法而不是属性的接口,因为它们比数据属性提供了更多的灵活性。

OO将重点放在方法上而不是数据上,这就是为什么getter进入这一领域的原因,因为OO更重视方法的灵活性。例如字符串.字节必须违反封装,对吗?它公开了String的实现细节,对吗?嗯,不完全是这样。我可以用字符数组实现String,并在以下情况下将其动态转换为字节数组字节数组方法。如果您没有返回字节数组的方法,并且您需要它。那么您必须在字符串中获取底层的char数组,以便将该方法写入string对象之外。这肯定会破坏封装。

这就是OO将方法作为首选耦合方式的原因。它们为插入逻辑提供了一个灵活的位置,并为依赖项提供了集成点。

14
  • “公开这些属性并不违反oop中的封装原则。”。尽管如此。这不是意见问题,而是事实。封装是指“封装”,在周围竖起一个胶囊、一道屏障,即不可用、内部状态,即实例变量。无论是无法避免,这都无关紧要,无论是好是坏。从实际出发,唯一重要的是:你是否会失去对数据的控制。如果你暴露了它,你就会失去控制,如果你不暴露,你就不会。 5月14日7:09
  • 4
    @RobertBräutigam在什么宇宙中封装等同于在对象周围创建一堵100%不透明的墙。如果我有一个person实例,那么(通过一些属性getter或方法)知道他们的名字有什么害处呢?
    ——游隼
    5月14日7:17
  • 1
    @Peregrine它不是100%不透明的。这是一堵墙行为,即源于需求,即泛在语言。暴露一个名字的“危害”在于你无法再对其进行推理。这是怎么一回事?对于? 我需要一个字符串吗,我需要名字和姓氏吗,我是否需要称呼?你不知道。如果你包含行为而不是给出行为,你就会知道上下文,因此可以孤立地进行推理和更改。这是我在评论中能做的最好的事情。,高温高压。 5月14日7:53
  • 1
    @RobertBräutigam:如果您公开字符串的“长度”,但不允许修改长度,那么这是如何失去对数据的控制的? 5月14日12:16
  • @格雷格·伯格哈特字符串.长度这是一个糟糕的例子,它位于图书馆的边缘。尽管如此,“失去控制”意味着一旦发布,就不能将字符串修改为无限制的字符串长度是int,现在不能有比它更长的字符串。如果您以前基于字节计算长度,并且人们开始使用它来存储它,那么当unicode出现时,您将面临一个巨大的问题。它一出版就在那里。你不能收回它,你必须支持它。这就是我所说的“失控”。 5月14日16:28
17

这不是一个成功者

只是因为方法具有得到它的名字并不意味着它是一个吸气剂;重命名getter为eg也不表示获取名称阻止它成为一个积极进取的人。

事实上获取项目方法更多的是一种查找:在库存中搜索商品x并将其返回。在后台,您可以有各种复杂的逻辑,包括从数据库中获取结果、读取文件等:您不仅仅是返回属性。

相反,假设您将HashDict实现为其现有实现的包装器(例如,以后可以交换它)。然后有一个获取收藏返回内部字典的方法将是getter(因此可以避免);相反,您应该使用其他接口方法与它交互。

总之,这只是命名混淆:字典的getItem方法不是getter。

10

我想试一试,但我只想谈谈散列表问题。

首先,回复您的人是面向对象领域驱动开发(由他创造)的倡导者,因此我将在这种背景下回答。这是一个具有特定含义的术语。即:面向对象(带有状态封装),应用于域对象,即在应用程序中有意义的对象。在这种意义上,HashTable不是领域对象,至少在大多数应用程序中,它是域对象可以使用的工具。您不会与客户谈论HashTable是与业务相关的实体/值。

现在如果您的任务是设计一个存储系统,该系统可以“获取给定密钥的项”、“存储给定密钥的项目”和“返回存储项的计数”,该怎么办?

在这种情况下,这些都是业务需求,根据领域也可以。这就是域名要求您执行的操作。

关键区别在于产品.getName()在存储系统的情况下,没有回答您可能向客户提出的问题“产品可以做什么?”存储.getItem('x')它确实回答了“存储可以做什么?”。

其次,你会注意到产品.getName()是纯getter,因为它没有参数。这意味着它不需要额外的输入来计算返回值。与之相比存储.getItem('x')。然而,方法没有参数这一事实只是一个提示,因为存储.count()考虑到这是一项业务需求(第1点),没关系。

第三,这个概念的重点是尝试垂直分组功能,而不是水平分组。产品.getName()很可能意味着一个分层体系结构,其中数据不与行为分组。

注意:我在这里不判断分层架构是否糟糕,或者这种类型的OO是否是垂直实现功能内聚/分组功能的好方法,这超出了这个答案的范围。然而,这是我从阅读他的文章中得到的。

9
  • 2
    我觉得我理解你的意思,但我也觉得你既不同意也不反对我的中心论点:你不可能到处都有OOP。我想你已经暗示同意了,但恐怕这只是我的一厢情愿。
    ——糖果橙
    5月12日3:04
  • @糖果橙同意,我在发帖时也有同样的感觉。我想说的是,最终你不能,但接下来一个有趣的问题可能是,在什么情况下你可以,最重要的是你应该什么时候?
    ——Ced公司
    5月12日13:06
  • @candied_orange:假设,如果你假设OOP不能孤立地确定地归因于每一行代码,那么你的问题实际上是无法回答的。考虑一下你的答案可能没有决定性的是/否,仅仅因为它没有那么确定和精细的定义。
    ——压扁器
    5月13日4:07
  • @candied_orange得出的结论“你不可能到处都有OOP”是基于这样一个假设,即要严格遵守一组规则,才能被称为“面向对象”。那是不对的。OOP没有一个单一的定义,所有人都同意。
    ——霍尔格
    5月13日11:11
  • 1
    @pjc50所以霍尔格可以越过我说话,然后加倍,但我不能?天啊,很好。我将坚持采取绝对必要的敌对态度。
    ——糖果橙
    5月13日14:19
5

你不能从一行代码中推断OOP

这个问题有点像是一个语义难题。

但并不是程序的每一行都可以是纯面向对象的。

是的,因为对象方向不能从一行代码中推断出来。这就像看着一块砖,问它是不是两层楼的一部分。砖头不知道。这块砖既不能证明也不能证明它是不是两层楼的一部分。

面向对象也不是一种体系结构,例如Clean architecture就是(这不是特定于CA的,我只是将其用作示例体系结构)。对于体系结构,有一个理想的代码库,大多数时候,你所谓的“干净体系结构”代码库是上述体系结构和你在实践中做出的一些妥协的混合。当我们争论某个东西是否是一个特定的体系结构时,我们真正不同意的是,代码库中有多少部分符合该体系结构。换言之,不同的人对他们认为可以接受的杂质百分比(即折衷)有不同的看法。

面向对象是一种建模哲学。我想从一个过于简单的例子开始。你可以对此吹毛求疵,但它的目的只是作为一个非常简单的基准,而不是一个非常精确的基准。
面向对象性的衡量标准并不是以哪些部分是(不是)面向对象的百分比来表示,而是对整体设计质量和实现深度的衡量。

考虑以下简要内容:

我们需要一个应用程序,在其中我们可以保存供应商,验证我们与他们的合同,并根据这些合同购买物品。

一个面向对象的程序员将会看到小贩,合同,项目虽然这个答案的重点不是定义函数式编程,但函数式程序员最终会得到的相反结论是保存供应商,确认合同,购买商品(我认为在这些名字中省略名词是正确的FP更为正确,但我更倾向于OOP阵营,所以我可能倾向于包括它们)。

这不是一个错误或正确的问题,甚至不是标记代码库的%是或不是面向对象的问题。

最终,面向对象的程序员仍然需要编写节约,验证购买逻辑,但他们会将其归类为他们设计的对象的一部分。类似地,函数式程序员仍然需要创建某种数据容器,其中包含例如供应商的信息,但他们将根据它如何插入到他们构建的功能中来设计它。

“Oriented”意思是“你用什么作为结构的第一顺序”。这并不意味着“只做这件事”。

静力学上的切线

就像你不能编写一个没有静态方法的程序一样(你至少需要main)

这是一个吹毛求疵的问题,但重要的是要与这个答案的其余部分联系起来。你没有需要主要。但只要你不告诉程序启动时要运行什么方法,那么您需要一个将要运行的默认方法。静态与汉兰达意义上的默认重叠:只能有一个。

静态操作与实例操作的特定性质与您如何决定启动时需要执行什么无关。有足够的相似性,一个可以代表另一个,但它们并没有内在地联系在一起。编译器也可以很容易地默认这样做(新程序())。电源()与…相反程序。主要,而不会真正改变正在调用的启动方法的默认性质。

OOP和共享状态

这将启用封装。国家不是要分享的东西,只是改变行为的东西。

这不是OOP的核心原则。这是一个很好的建议,但这并不是OOP概念最初存在的原因。经验丰富的OOP开发人员和FP开发人员都会同意这一点。

这里所做的关键区别,即前封装,是过程明显比静态定义更短暂。您可以更容易地调整流程的逻辑实现(同时保持其整体输入输出结构),而不是更改静态定义,同时保持其与以前的结构兼容。

换言之,函数/方法内在地需要将输入映射到流程,然后(可选)映射到输出,这一事实内在地加强了调用者和实现之间的松散耦合。使用静态定义(例如类周围的接口)可以实现类似的松散耦合,但很容易忘记这样做,这需要付出更多的努力。

特别是对于OOP,这里还有一个额外的考虑因素,即您希望按主题链接处理分组在一起例如,您将在小贩类。
当您对外公开状态时,您是在让外部人员能够基于您的状态字段编写自己的逻辑。但是,如果这种逻辑依赖于您的状态字段,那么这种逻辑的可能性相当高应该与您分组而不是存在于类的其他使用者中。

这导致人们普遍反对将共享状态作为阻止其他人开发您应该保留所有权的流程的手段。通过禁止共享状态,您就阻止了这种情况的发生。

我们应该禁止进入国家吗?

现在,我也在回答你回答的问题时写下了这句话(这激发了你发帖),但我非常同意这种精神(不是在拥有相关字段的类之外开发逻辑),同时也非常不同意这样的观点,即用毯子包住某些东西是正确的执行方式。

所以我的反馈将更多地暴露一些状态是合理的,但在消费所述状态时,应该评估消费者是否是添加您正在创建的新逻辑的正确位置。

我提倡实用主义,而不是盲目的教条,这意味着我通常反对任何形式的总括规则,除非你能彻底证明没有任何反对它的理由。

然而,有些边界,如库边界,使方法无法移动,因此您可以移动数据。[…]因此OOP理想在现实面前受到了损害。

重申一下,在这个库的示例场景中挂起的东西并不是一个OOP理想,但它仍然是我们希望能够做到的。

库边界意味着在某种程度上失去了对源代码的访问权限,您可以根据扩展的需求对其进行调整。这就是将库与“我的代码库中的其他文件”区分开来的原因。这也是一个语义难题。

让我们探索一个我们可以改变这种情况的世界——开源库具有这种能力。虽然能够扩展它会很好,但这成为一个所有权问题。如果库现在有一个bug,那么最初的作者并不是特别关注它,因为他们既不理解也不积极支持您对其逻辑的扩展。同样,您也可能无法解决该问题。
以开放源代码为例,您可以自由创建自己的代码分支,但库的作者对您的分支没有责任慈善的足够帮你了。

这与良好开发实践的另一个考虑因素有关:明确所有权。只有强制将逻辑与不同作者分离,才能真正确保这一点。

请注意,我所说的“作者”并不是指一个特定的人,而是指一个实体,它承担着任何和所有在其之前或与他们一起工作并编写代码的人的责任。如果Bob离开了MyCompany,而Tom现在为他们工作,并且他继承了Bob的代码,那么MyCompany就是该代码的唯一作者。

还有其他方法可以将库集成到代码库中,而不需要打破所有权的边界。包装是这里的简单解决方案。扩展方法可以帮助您的逻辑感觉与库更加集成,但它们实际上只是只能访问公共数据的副加载帮助器逻辑。

通常经过作者的明确同意(通过他们的设计),可以提供可继承的类,这样您就可以访问受保护的状态并集成您的逻辑,而不仅仅是通过包装它。这很好,但它通常需要库作者进行一些设计考虑,因此他们决定是否真的想承担额外的工作(如果工作正常,还取决于库的新增用户)。

额外的学分和教条为什么不好的例子。

为了获得奖励,请向我展示OOP纯粹主义者如何设计一个没有getter的哈希表。

这更像是另一个语义难题。“Getter”对不同的人意味着不同的东西。在具有实际属性getter(例如C#)的语言中公共字符串MyProp{get;}),“getter”是指具体的,还是指类似的任何方法公共字符串GetMyProp(){…}?

如果是前者,那么简单的(但我认为可以忽略)答案是,您可以用一个私有字段替换每个属性,并附带访问它的方法,然后您就从技术上讲在没有吸气剂的情况下创造了一些东西。显然,这并不是对你问题的回答。

此外,这个问题描绘了一个场景,完全忽略了引发前一个问题的建议的目的(这反过来又引发了这个问题)。
如果我没有意识到外部使用者将如何利用我公开的内容,如果以及如何围绕它构建逻辑,以及该逻辑是否更适合添加到我的类中而不是消费者的类中,那么我就不可能最终告诉你如何设计[…]。

如果不知道使用者以及他们打算对您的对象做什么,就没有纯粹的OOP方法来解决这个问题。简单地说,如果你遵循“不公开状态,只公开行为”的准则(为简洁起见,缩写为),那么当你意识到OOP从对象的定义开始,然后在该定义中构建其行为时,你会遇到很多问题。这本质上意味着该对象定义的作者必须始终是该类型定义中包含的行为的作者。

如果它在类型定义之外,那么它违反了您公开行为的“将行为添加到类型”目标。

如果是不同的作者,那么让一个类型由两个不同的作者设计,而他们至少没有意识到对方,这将是不好的。正如我们所建立的那样,在这个场景中,您还不了解您的消费者,因此在这里不可能主动了解您的客户。

这是一个很好的例子,说明了为什么我讨厌盲目教条,并认为它是困扰软件开发的更大问题之一。你在这里提出的问题是对指南文字的盲目应用,你没有注意到你的问题实际上没有触及指南的精神。

8
  • 1
    “在这里不可能主动了解你的消费者。”你在支持我的论点的同时谴责我的想法,这真的让人困惑。 5月13日4:57
  • 1
    @candied_orange该指南指的是消费者希望做一些尚不可能的事情,从而试图根据对象的状态为自己构建它的情况。该准则规定,不应公开状态以避免发生这种情况,而应将行为添加到对象中而不是这样消耗它的状态这本质上只适用于以下情况:(a)你知道你的消费者想要添加什么,(b)你实际上能够将所述行为添加到消费对象中(即其设计尚未最终确定)。
    ——压扁器
    5月13日5:07
  • 1
    @相比之下,你的“额外问题”是:(a)没有解释消费者希望看到添加的内容,(b)要求最终设计考虑到所有未来消费者。这与指导方针的整个上下文是正交的,因此我认为您还没有完全理解指导方针试图传达的内容。
    ——压扁器
    5月13日5:08
  • 这个难题正是我想要描述的。这就是边界。现在把你想象成一个OOP纯粹主义者,告诉我你是如何处理的。
    ——糖果橙
    5月14日13:04
  • @candied_orange我不知道你是在胡扯,还是明显不愿意听到为什么根据你设定的参数,你的问题是不可回答的。仅仅因为它在英语中有语法意义,并不意味着它可以得到有意义的回答。
    ——压扁器
    5月14日23:10
2

这算不算“纯”哈希表?请注意,任何数据容器(如数组或哈希表)都可以公开一个ForEach方法,该方法以函数指针作为参数,并使用数组内的值调用该函数。你实际上不需要公开函数指针,你也可以封装它,但是你必须为你可能需要在其中一个元素上执行的每个操作创建一个新的HashTable类,这很快就会失控(这就是为什么我们在专业软件工程中不做这样的事情——getter允许重用代码,但我相信你已经知道了)。

公共类ThePureHashTable<Tkey,TValue>{public void DoSomethingWithValue(Tkey key,Action<TValue>something){某物(这个[钥匙]);}public void Add(Tkey键,T值){}}
2
  • 这与通用数据结构无关,但为了论证起见,是的,一个无getter的设计看起来应该是这样的(尽管您可能会认为这不再是一个哈希表)。你会的封装动作(我想你的意思是让它完全在类内部),因为这是一个非常不同的设计。传递一个动作只是依赖注入(它也可以是一个对象)-如果你的类需要调用某个东西,但不知道它是什么,你可以做这种事情。但同样,不是哈希表是如何概念化的。 5月11日23:24
  • 是的,你可以随意使用OOP,只需翻转调用函数的方式。我认为这个问题只是对上一个问题争论的延续
    ——伊万
    5月12日11:51
2

在另一个问题上,我几乎得到了与此类似的答案,并决定不这样做。我已经被诱惑过两次了,我会咬人的。

我们到底在争论什么?

为什么我们要进行面向对象编程?(或函数编程,或基于约束的逻辑编程,或…)。我的意思是可以肯定地说,任何程序都可以用Procedural风格编写,几乎每种编程语言(包括Java)都提供了编写Procedurall代码的方法。

这个问题的答案会因你问谁而有所不同(当你问他们时,1997年的答案与2024年的答案并不完全相同)。但他们给出的答案可能包括封装、子类型多态性、数据抽象等单词和短语。

虽然我认为很多反对程序性的论点都是吸管充其量是可能在20年前就已经适用了,我不认为有人会认为程序代码在一般情况下对这些东西做得很好。

那么程序性不好吗?嗯,情况往往很糟糕在那些特定的事情上这些是我们想要的吗?答案同样不同,人们也会争论OOP在他们身上的表现如何,但答案通常是.

但我们并不总是需要它们。有大量的过程代码,甚至是过程代码的大型代码基,没有它们都很好,或者至少没有OOP版本。它们不必要的事实造成了分歧:

  1. 第一个阵营说,既然你不总是需要它们,那么强制遵守是盲目的教条。它迫使程序员扭曲完全合理的方形销钉,以适合任意的圆孔。雅尼:我们是为了安抚OO正统的货运教派牧师而进行的过度工程。我认为你的问题与这个观点有共鸣。我知道我知道。

  2. 第二个阵营说习惯决定一切。纪律规则。养成“正确”做事的习惯和纪律是很有用的,即使在某些时候它并不总是必要的,而且你也不总是提前知道那些时候是什么时候。像原始痴迷这样的坏习惯已经成为众所周知的反模式已经足够长时间了,我们不应该再为它们争论了。我们都被需求变更所折磨,这些变更使我们开始时完全合理但实际上并不需要首先进行的假设失效。

每当一门学科有时有用,但并非普遍必要时,这种二分法就会出现。静态类型与动态类型。强大静态类型(Haskell、Rust、ML)与主流静态类型(Java、C#、Typescript)。TTD与非TDD。关于getter和数据边界可取性的争论有着完全相同的形式。这不是巧合。

这种分裂进一步由种类我们编写的软件。嵌入式系统、关系数据库引擎、视频游戏和桌面GUI的需求看起来非常不同,即使它们都是用同一种语言编写的。Bob完全有可能生活在第一个世界,那里的问题几乎总是源于过度工程,人们应该使用字符串的g*dd*mn哈希图,而不是试图将系统中的每个微小实体建模为“域对象”。Alice几乎完全有可能生活在第二世界,即使是最微小的需求更改也需要涉及十几个或更多类,因为有人将他们的假设乱七八糟地散布在代码库中,而没有考虑未来。鲍伯和爱丽丝将在互联网上争论不休,甚至可能没有意识到他们来自两个截然不同的地方。

我们认为我们在争论同样的事情。我敢打赌,这种情况不会像看上去那样经常发生。很多争论都是由个人偏好所驱动的,但这些偏好是在我们的经历中形成的,并且因人而异。

所以这都是偏好?

不完全是。这是真正的权衡。但他们是权衡:不同的人希望在曲线上处于不同的位置。只需记住,很容易指出您的偏好起作用而他们的偏好失败的情况。但反过来也是如此。如果你发现自己与人们产生了这种分歧,那么我认为有必要指出,这两种潜在的世界观都以各自的方式有效,尽管它们给出了不同的答案。

1

我认为OOP的存在主要是为了允许软件的分散设计。

也就是说,它旨在促进独立团队设计和维护软件组件的能力,与其他必要的团队相比,他们之间的协调更少。

在OOP中,所有事情都是通过动态过程调用完成的,不能直接访问内存中的存储。这意味着组件的提供者在其组件的所有外部限制处保留一个拦截点,在该拦截点之后,他们最终保留对整个内部设计的控制权。

这并不是这方面的万灵药,我不想一一列举这个想法失败的所有方面,但我想设定一个基本场景。

OOP还有另一个合并的用途,即针对单个设计团队(或单个设计师),试图在其权限范围内管理软件内部的复杂性。这里的想法不是要分离人与人之间的设计控制,而是要分割和征服单个应用程序的复杂性。

有很多关于OOP的文章,本质上是关于追求第二个目的,而不是第一个目的。

再一次,我不想深入了解这个想法失败的所有方式。我只想指出的是,实际上有多种目的在起作用,它们并没有真正联系起来,而且关于每一个目的的想法通常会被混淆。

现在,我不太清楚不在对象之间共享数据的想法是如何产生的。哪一个任何一个对于我提到的这些目的,它有用吗?

一般来说,软件的一个基本目的是处理数据,因此内部数据流的存在是内在的、不可简化的(更不用说数据最终是如何从外部流向计算机用户的)。

如果不是通过getter从对象中获取数据,或者向用户显示任何内容(如果不是通过某种setter,则将数据放入另一个对象,或者从用户那里获取输入)?

我很欣赏@RobertBräutigam的观点,即我们不必过于字面地看待每件事,并且忽略了要点,但我认为(尽管他使用不同的语言)他本质上是在将软件问题诊断为内部数据流的存在,他的处方是停止数据流。

当然,有可能存在内部数据流严重混乱的软件,或复杂度不必要的数据流(尽管不是无序的)。

他可能熟悉自己的软件设计经验,其中有太多的对象,有太多数据从一个对象传递到另一个对象,而每个对象对其包含的数据几乎不做任何事情。在这种情况下,他的建议是有意义的:去掉由getter和DTO组成的长桶组,只需在源对象上放置一个方法,用它已经拥有的数据做相关的事情。

但我认为,他认为“没有getter”(或任何等效语言)是设计对象模型的有用通用指南,这一点做得太过分了。这是一个滑稽的讽刺,出自他自己的手笔,他可能想就设计如何出错的某些特殊情况提出更微妙和更狭隘的观点。

1
  • 1
    组件分割定义了什么是实现细节。组件必须隐藏其状态,并在状态访问接缝之间进行拆分,因为状态管理通常是最困难的问题。因此,组件间状态访问表明状态不难管理,或者组件拆分无效(与当前需求不匹配)。因此,getter要么是设计问题的症状,要么是与状态无关的复杂性的症状。回答很好,但结论太客气了。 5月16日0:48
1

你问的实际问题是:“你能把“告诉,不要问”这一原则推进多远?”

答案是,你应该只要能提高封装性,就将其推入,但您可以轻松地将其推到目前为止,它将产生相反的效果。

这个原则是另一种说法,即一个对象应该控制它自己的状态。外部客户端不应对对象执行“开放式大脑手术”并改变其内部状态,如本例所示:

var计数器=新计数器();//递增计数器counter.value=计数器值+1;//打印计数器console.log(“计数:”,counter.value);//重置计数器计数器值=0;

让我们应用“告诉,不要问”,让计数器控制自己的状态:

var计数器=新计数器();//递增计数器counter.increment();//重置计数器counter.reset();

但这样做显然太过分了:

counter.printTo(控制台);

从现在起,计数器突然需要知道我们将如何使用计数器值,这打破了关注点的分离。通过getter公开计数器值非常好。如何使用不应成为计数器关注的问题。

恰当使用的吸气剂不会“损害”OOP的理想。

-1

我对OOP的哲学概念(对我来说是指面向对象编程)的经验很少,在面对这种背景时,我试图与现实生活联系起来,在现实生活中,对象是惰性的存在,它们被外力(物理意义上的力)放置在何处以及如何放置,他们的内部状态对于外人来说是未知的,他们甚至无法从外表中推断出来,因此不使用武力就不可能对物体做任何事情(同样在物理意义上,更具体的是机械意义上)。

所以。。。

你能把面向对象编程推进多远?

在建立一个漂亮的惰性系统之前,如果没有使其充满活力的东西,它是无用的,那么思想至少会是美丽的,让周围的每一个人都感到高兴。如果没有读取权限,数据库会是什么?请原谅糖浆味。

1
  • 1
    阅读数据库是柏拉图的饭菜。而getter则处理数据库的存储和表段计数。 5月16日0:35
-1

这不完全是一个问题。尽管如此,我还是会尽力回答这个问题,因为这是不公平的。

首先,在没有函数的系统中,getter是什么?我通常认为身份函数; 也就是说,我们可以编写类似以下Python的代码:

定义id(x):返回x

这可以是对象上的方法吗?当然:

类Id:定义运行(self,x):返回xid=id().run

但它是一个吸气剂吗?如果是这样的话,那么我们的函数组合就失败了,如果不离开面向对象的世界,我们甚至无法编写标准的面向函数的代码。要使这一点更加清晰,请考虑K组合符:

定义k(x):返回λy:x

如果我们把它变成类,那么我们必须明确地结束第一个参数:

Konst1类:def-run(self,x):返回Konst2(x)Konst2类:定义__init__(self,x):self.x=xdef-run(self,y):返回self。x个

因此,如果没有getter,我们甚至不能做像组合逻辑这样简单的事情!

我认为更好的问题是getter是否真的是面向对象编程的象征。这个E语系没有getter,因为它没有类或原型;相反,对象被写为直接文本,它隐式地封闭其周围的范围,并且需要编译器来计算隐含的类结构。让我用E REPL编写K组合符:

? 定义k{运行(x){return定义{运行#值:<k>? k(4)(2)#值:4

我认为这暴露了你构建事物的方式的一个基本问题,尽管我不知道如何改进框架。如果我用E编写一个哈希表,它是否天生就比Python中的少一些getter-ish?它不是那么面向对象吗?我会分别说是和否。所以“宾语”是多义的;它可以使用类(Java)、原型(Python)或对象文字(E)引用某些内容。

4
  • 2
    如果我们让“getter”包含“返回状态的函数”,那么E肯定有getter吗?
    ——伊万
    5月13日8:43
  • @伊万:这就是我要问的,是的。在结束时返回某个东西是一件好事吗?如果是这样,难道不是所有的函数和组合编程都被排除在外吗?
    ——科尔宾
    5月13日15:05
  • 1
    我想这是我的第二个问题,你认为函数编程仍然是OOP吗?他们肯定是不同的,你不希望他们遵循相同的规则吗?
    ——伊万
    5月13日15:20
  • @Ewan:对我来说,面向对象编程是带有消息发送的函数式编程。它可以是纯的,可以有有限的可变性,可以是静态/渐进类型。例如,e和OCaml之间的最大区别是OCaml没有简单的异步消息传递语法。
    ——科尔宾
    5月13日18:17
-1

哈希表是键和值之间的关联,对键有要求,以使某些操作更快。

具体来说,密钥需要快速映射到一个“散列值”,即一个整数,即尽可能均匀地分布在整数值集上,并且尽可能“随机”,然而任何两个比较相等的密钥都应该具有相同的散列值。

这允许使用一组技术,使冲突检查和查找比传统的平衡树方法快得多。

所以如果我们使用搞砸表中我们必须注意这一区别;碰撞检测的速度一定很有价值。此外,这些表没有按“合理”的顺序(不是字母顺序等)存储键。

有效的操作类型如下:

  1. 一种“foreach member”操作,对每个键值对执行某些操作。它这样做的顺序尚未明确。这是一个昂贵的(O(n))操作。

  2. “快速匹配”子设置操作,对键进行操作,只对匹配的元素执行操作。这是一个快速(O(1))操作。

哈希表中通常会添加一些业务逻辑,例如“每个键只有一个元素”,这些逻辑不会在哈希表的属性中继承。

哈希表的键值结构也可以丢弃;你真正拥有的是元素,它有一个相等比较(形成等价类)和一个散列操作。相等和散列操作不需要检查元素的整个状态。

我们通常将元素投影到(K,V)对中,然后使用Hash(K)和Equal(K,K)对K组件进行操作。

在某些情况下,哈希表实际上关心给定哈希桶中的所有元素。为了更深入地研究纯度,我们可以完全放弃等式运算,并将其留给下一层。

所以现在,我们的E哈希表是由哈希(E)操作提供的。它将元素放入bucket中,其中具有相同Hash(E)值的所有元素都位于同一bucket内,但不能保证具有不同Hash(E)的两个元素位于不同的bucket。

然后,我们可以进一步总结这一点,并有我们的桶表。Bucket表采用(K,V)对,其中K是一个整数,并将它们存储在Buckets中。它的操作是“查看所有内容”和“查看给定的Bucket”。它会在添加元素时调整自身大小,以减少交叉键值桶碰撞的次数。

铲斗表:

铲斗<E>可迭代<(K,E)>和可迭代<E>

您可以将其作为(K,E)值的列表进行迭代。

铲斗<E>。筛子(K,F(Iterable<K,E>或Iterable<E>))

您可以“筛选”出与给定K位于同一桶中的元素。

然后,哈希将调整Bucket表。它使用投影P将元素映射为整数,并提供:

哈希<E,P>是Bucket<E>散列<E,P>。筛子(X,F(Iterable<K,E>或Iterable<E>))

其中筛(X,?)是指筛(P(X),?)。

我们可以在上面添加冲突规则(uniquehashtable?)、insert、exists操作,大致如下:

hash.exists(x):=hash.sesive(x,any_of(e->(e==x))插入(x):=哈希筛子(x,l->l.insert(x))//或可能插入({p(x),x})hash.access(x,f):=hash.sieve(x,过滤器(e->(e==x),f))

现在,这种“缺少getter”方法的一个有趣之处是,如果我们的适度散列表实际上是一个大规模分布的表,那么这些操作仍然有意义。

想象一下,如果我们的存储桶中实际上有不同的服务器,而我们的元素中有大量我们确实负担不起传输的数据,但我们的操作很小,很容易序列化。

现在,我们的bucket.sieve操作打包了我们的函数并将其发送到服务器,在那里运行它并计算所有结果。然后返回结果。

我们在桶上构建的哈希也可以做同样的事情。

类似地,如果我们的表大部分不在内存中,并且“加载它”成本很高,那么基于操作的模型意味着我们知道我们所提供内容的有效性域。我们提供以下能力迭代在我们的桶上,而不是桶列表本身。迭代运算同样提供了以下能力参观元素并对其执行操作,而不是“获取副本”或“获取引用”。

...

我发现,当考虑OOP时,最好回到它的起源。OOP是关于不透明参与者之间的消息传递协议,而不是类似的方法。

“你有一堆数据。下面是我想对所有数据运行的一些操作,请这样做并使用此操作收集结果。”是一个基于消息传递的操作。

想象一下你正在交流的东西并不是生活在同一个记忆空间甚至时区中也有帮助。

这并不意味着您的协议应该强制封送,而是应该看到“我真的需要共享内存空间来高效地完成这项工作吗?”。不要强迫人们编写假冒“相同内存空间”的代理对象。

2
  • 为什么解释哈希表?人们之所以来到这个网站,是因为他们不想阅读和回答有关Stackoverflow的算法问题:) 5月16日1:13
  • 1
    这个答案可以归结为“告诉不要问”,但OP很清楚这一原则。真正的问题是,原则在哪里不适用? 5月16日1:19
-2

您可以轻松创建一种100%面向对象的编程语言。例如红宝石语言只有了解对象。没有什么不是物体。没有什么比Java的“文字”更能超越Java的面向对象性。甚至类本身也是对象。方法是对象。闭包是对象。如果你想深入研究,你可以用谷歌搜索Ruby的超类、模块、mixin,这将为你提供更多的OOP功能(同时巧妙地解决了多重继承问题)。

当你用Ruby编程时,你从来没有感觉到他们把它推得太远。一切都很简单、正常、直截了当、优雅,没有一件事会大喊“OOP太多了”。

显然,作为一名开发人员,您仍然可以在许多方面随意(错误)使用这种特定语言。您可以编写“脚本”,它只是由一长串迭代意大利面条组成,可以对语言和标准库附带的任何对象进行操作,但不定义任何自己的类或方法。另一方面,你可以从旧的设计模式书中取出旧的纸质版,从你的程序中抽取模式,直到它完全无法识别它的功能。或者您可以编写基本上是“C结构”的类,并使用一些与之相关的代码。所有这一切都不是OOP的失败,这只是程序员过度使用或误解它的失败。

我认为,你可以“过度OOP”的感觉主要适用于具有非完整OOP实现的语言(如Java,它分离了像整数这样的基本类型,以及相关的两类公民身份);或者,对于以前纯粹基于迭代/模块化/包的语言,以及OOP功能被追溯添加的语言(即C++、Perl、Python…),在实际使用基于对象的“世界”可能比不使用它更困难。

有趣的是,Ruby通过getter不具有得到_前缀(prefix)——也就是说,如果你想要一个获取人名的getter,你只需调用该方法名称。setter没有套_前缀,但只是一种方法名称=(在Ruby中,“=”在方法名称中是有效的)。这意味着在许多情况下,您会看到类似打印人员姓名person.name=“汉斯”。对象的实际属性位于单独的命名空间中,因此没有问题。在许多情况下,您还可以通过定义catch-all方法(即每个类一个getter和一个setter)来避免输入太多内容,或者直接创建库来封装DAO风格的数据库访问,而这完全是在运行时进行的。

顺便说一句,我看不出您在哈希类上下文中使用可避免的getter/setter的示例给这个问题带来了什么:

具体地说,当设计实现散列的类时,“getter”是完美的OOP设计,它完美地封装了实现细节。

散列中的getter并不是一个简单的“getName()”,但它是知道如何遍历散列实现(可能相当复杂)的方法。散列可以基于内存中的bucket,在存储对象的内容上使用散列(这本身就是一个有趣的问题——首先如何获得对象的这样一个散列代码?)。它还可以是一个隐藏分布式Reddis服务的实现,包括授权、安全性、故障转移主题等。在任何情况下,它都可能处理多线程同步。必须在封装实际实现的散列中包含getter/setter方法。

1
  • 1
    在这个网站上,OOP通常不是语言的属性,而是数据和行为分组的范式或哲学。因此,语言讨论似乎偏离了主题。 5月16日1:04
-2

以下是我试图回答这个似乎无法回答的问题:

设计库哈希表集合的方式与库设计其哈希表集的方式完全相同。

我非常相信OOP和TDA。但面对现实,我坚持自己的理想,承认它们只在特定的情况下起作用。我不会把他们推入他们无法处理的情况。跨越这些界限,你看到的并不是OOP。集合不封装它包含的内容。它不知道什么方法可以提供您需要的行为。这不是一个物体。不是那种意义上的。

现在,每个集合都有一些内部状态,可以很好地封装。从这个意义上说,它是一个物体。只是在您尝试将方法移动到包含此数据时,不会出现这种情况。

这限制了OOP可以为您做的事情。如果你接受这些限制,你实际上可以摆脱对OOP的狂热。与其说getter是一种合理化的东西,不如说它是一种一旦在OOP安全的空间中使用就应该被隔离的东西。

您要做的是创建一些实际的OOP对象,并使用这些有用的库工具对其进行支持。是的,你可以用你的对象把它们抽象出来。所以,不,你没有把方法一直移动到数据上,但你已经尽可能地接近它了。这就足够了。

对象应该包含与对象中的其他功能和数据高度相关的每个业务功能。

数据边界是维护问题的根本原因

理想就在那里。这绝对是真的。这不是你的哈希表会让你做的事情。为你的OOP建立一个空间。不要坚持你的整个程序都是OOP。他们都不是。OOP是一种你可以随心所欲的东西。

如果您愿意这样做,则不需要对哈希表进行任何不同的设计。

2
  • 1
    为什么OOP应该将集合项的状态视为集合本身的一部分?集合在项中没有可见性,所以即使坚持OOP,也没有必要将方法移动到集合中。集合管理项目存储,而不是其数据。 5月16日1:02
  • @因为这就是方法所需的数据。不管收藏家怎么想。重要的是你不能移动这个方法。OOP想要它想要的。这并不是说集合是OOP的特殊例外。这只是现实与理想的较量。
    ——糖果橙
    5月16日8:56
-2

TL;博士

API应该关注并隐藏组件的主要功能的详细信息。当状态管理不是主要功能时,Getter可能很有用。

这与OOP无关

组件设计的目标是将问题分解为子问题,使每个组件的复杂性都是可管理的,而它们交互的复杂性是最小的。这与OOP无关。

组件根据定义隐藏其实现细节。这与OOP无关。

组件通常只关注一个复杂的子问题。这与OOP无关。

国家是一个难以管理的问题。通常状态管理是孤立的,以更好地管理其复杂性。这与OOP无关。

OOP隔离组件中的状态。FP隔离组件外部的状态(例如使用IO monad)。

OOP组件隐藏状态以避免暴露其复杂性。如果这样做会降低总体复杂性,则它们会暴露状态。当子问题的复杂性不是来自状态管理时,可能会发生这种情况。

Getters正在暴露状态。因此,它们的存在表明组件设计不适合手头的问题(泄漏实现细节),或者设计关注的是比状态管理更难解决的问题。

换句话说,问题“getters坏吗”是组件设计问题的特例——这个组件的状态是它试图解决的最难的问题吗?如果组件与状态管理无关,它可以公开它想要的任何私有字段,前提是这将有助于其他组件获得此组件解决的实际硬子问题的答案(显然,解决方案的细节永远不应该公开,而应该只公开结果)。

启发式的“getters are bad”出现的原因是状态管理的难题,以及试图在默认情况下隔离状态管理。启发式方法仅适用于组件管理状态。并非每个组件都必须管理状态,有些组件管理状态的一部分,但公开另一部分。

拆分组件以最小化API表面,将当前实际最困难的问题作为实现细节进行隔离。这与OOP无关。

示例

String.length是一种抽象泄漏(出于多种原因,包括多字节编码),因为可变字符串不应公开其状态,从而允许索引访问。对于具有固定长度编码的不可变字符串,这不是泄漏,因为它们可以提供索引访问并将其公开为API(如数组)。

Array.length并不是一个漏洞,因为它不会改变,而索引访问是Arrays的主要用途。

HashMap.size不是一个漏洞,因为集合是显式设计的有状态的,并且要解决的主要问题是存储,状态管理留给客户端。

Account.balance是银行交易上下文中的一个漏洞,而不是互联网银行UI上下文中的漏洞。

Dog.legCount是家庭宠物管理方面的漏洞,而不是宠物服装店或兽医诊所方面的漏洞。

DTO.property不是漏洞,因为价值传递是DTO的主要目的。

这与OOP无关

拆分组件以最小化API表面,将当前实际最困难的问题作为实现细节进行隔离。这与OOP无关。Getter只是API设计的一小部分。它们并不特别,应用其他启发法。

不是你想要的答案吗?浏览标记的其他问题问你自己的问题.