处理库中可见性的常用方法是什么?


12

这个问题何时使用私人和当类使用受保护的让我去思考。(由于与之相关,我还将这个问题扩展到最终的类和方法。我使用Java编程,但是我认为这与每种OOP语言都相关)

接受的答案是:

一个好的经验法则是:使所有内容尽可能私密。

还有一个:

  1. 除非您需要立即对它们进行子类化,否则将所有类定为最终类。
  2. 使所有方法最终化,除非您需要立即子类化并覆盖它们。
  3. 除非需要在方法主体中进行更改,否则将所有方法参数都设置为最终值,这在大多数情况下还是很尴尬的。

这非常简单明了,但是如果我主要是编写库(GitHub上的开源)而不是应用程序怎么办?

我可以说很多图书馆和情况,

  • 库以开发人员从未想过的方式扩展
  • 由于可见性限制,必须使用“类加载器魔术”和其他技巧来完成此操作
  • 库的使用方式不是为它们而建,而是以所需的功能“入侵”
  • 由于存在小问题(错误,功能缺失,“错误”行为),无法使用库,原因是可见度降低,无法更改
  • 无法解决的问题导致了巨大的,丑陋的和错误的解决方法,在这些解决方法中,覆盖简单的功能(私有或最终的)可能会有所帮助

实际上,我开始命名它们,直到问题过长,然后我决定将其删除。

我喜欢这样的想法,即没有多余的代码,比可见性更多的可见性,比需求更多的抽象性。当为最终用户编写应用程序时,这可能会起作用,因为最终代码仅由编写该应用程序的人员使用。但是,如果代码打算供其他开发人员使用,而原始开发人员则不可能事先考虑到每种可能的用例,并且很难/不可能进行更改/重构,那么这将如何保持呢?

既然大型开源库不是什么新鲜事物,那么在使用面向对象语言的此类项目中处理可见性的最常用方法是什么?



鉴于您询问开放源代码,为了解决您列出的问题,弯曲适当的编码原则比关闭源代码更没有意义,这仅仅是因为一个人可以将所需的更正直接添加到库代码中或派生出来并使用他们想进行的任何更正
咬到

2
我的意思不是关于此,而是关于您在这种情况下对开源的引用毫无意义。我可以想象,在某些情况下,务实的需求可以证明偏离严格原则的合理性(也称为产生技术债务),但是从这个角度来看,代码是封闭的还是开源的都无关紧要。或更准确地说,它的意义与您在此想像的方向相反,因为开放源代码可以使这些需求比封闭的需求少,因为它提供了解决这些问题的其他选项
gnat

1
@piegames:我完全同意在这里咬,您解决的问题更有可能在封闭源代码库中发生-如果它是具有许可许可的OS库,如果维护者忽略更改请求,则可以派出该库并如有必要,请自行更改可见性。
布朗

1
@piegames:我不明白你的问题。“ Java”是一种语言,而不是一种库。而且,如果“您的少量开放源代码库”的可见性过于严格,那么以后再扩展可见性通常不会破坏向后兼容性。反之亦然。
布朗

Answers:


15

不幸的事实是,许多库都是写的,不是设计的。这是可悲的,因为一些先验思想可以防止将来出现很多问题。

如果我们着手设计一个库,将会有一些预期的用例。该库可能无法直接满足所有用例,但可以作为解决方案的一部分。因此,库需要足够灵活以适应。

约束在于,通常不建议采用库的源代码并对其进行修改以处理新的用例。对于专有库,该源可能不可用,而对于开源库,可能不希望维护分支版本。将高度特定的适应方案合并到上游项目中可能并不可行。

这就是开放-封闭原则的来源:库应该可以扩展而不需修改源代码。那不是自然而然的。这必须是故意的设计目标。这里有很多技术可以提供帮助,经典的OOP设计模式就是其中的一些。通常,我们指定挂钩,用户代码可以在其中安全地插入库并添加功能。

仅公开每个方法或允许每个类被子类化不足以实现可扩展性。首先,如果不清楚用户可以在哪里链接到库,那么扩展库确实很困难。例如,覆盖大多数方法并不安全,因为基类方法是使用隐式假设编写的。您确实需要设计可扩展性。

更重要的是,一旦某些东西成为公共API的一部分,您就无法收回它。您不能在不破坏下游代码的情况下进行重构。过早的开放性将库限制为次优设计。相反,将内部内容私有化,但是如果以后需要则添加钩子是一种更安全的方法。尽管这是解决库的长期发展的明智方法,但对于现在需要使用该库的用户而言,这并不令人满意。

那么发生了什么呢?如果库的当前状态有很大的麻烦,则开发人员可以获取有关随着时间的推移累积的实际用例的所有知识,并编写库的版本2。这将会非常棒!它将解决所有这些设计错误!在许多情况下,它还会花费比预期更长的时间。而且,如果新版本与旧版本非常不同,则可能很难鼓励用户进行迁移。然后,您需要维护两个不兼容的版本。


因此,我需要添加扩展钩子,因为仅使其公开/可覆盖是不够的。而且由于向后兼容,我还需要考虑何时发布更改/新API。但是,特殊方法的可见性如何?
piegames

@piegames具有可见性,您可以决定哪些部分是公共的(稳定API的一部分),哪些部分是私有的(可能会更改)。如果有人通过反思来规避这一问题,那么将来该功能失效时就是他们的问题。顺便说一下,扩展点通常采用一种可以被覆盖的方法的形式。但是有一个方法之间的差异可以被覆盖,并且这是一个方法意图被覆盖(另见模板方法模式)。
阿蒙(Amon)

8

每个公共和可扩展的类/方法都是您的API的一部分,必须得到支持。将设置限制为库的合理子集可以最大程度地提高稳定性,并限制可能出错的事物的数量。这是基于您可以合理支持的管理决定(甚至OSS项目都得到了一定程度的管理)。

OSS和封闭源代码之间的区别在于,大多数人都在尝试围绕代码创建和发展一个社区,以便由一个人来维护该库。也就是说,有许多可用的管理工具:

  • 邮件列表讨论用户需求以及如何实现事物
  • 问题跟踪系统(JIRA或Git问题等)跟踪错误和功能请求
  • 版本控制管理源代码。

在成熟的项目中,您将看到以下内容:

  1. 有人想对原本不是设计来做的库做点什么
  2. 他们将票添加到问题跟踪
  3. 团队可以在邮件列表或评论中讨论问题,并且总是邀请请求者参加讨论
  4. 由于某些原因,接受了API更改并确定了优先级或拒绝

届时,如果更改被接受,但用户希望加快其修复速度,则他们可以完成工作并提交拉取请求或补丁(取决于版本控制工具)。

没有API是静态的。但是,它的增长必须以某种方式进行调整。通过关闭所有内容直到证明需要打开所有内容,您可以避免获得有问题的或不稳定的库的声誉。


1
完全同意,我已经成功地实践了您为第三方开放源代码库以及开放源代码库编写的更改请求流程。
布朗

以我的经验,即使在小型库中进行很小的更改也是很多工作(即使只是为了说服其他人也是如此),并且可能需要一些时间(如果您不能在那时使用快照,则需要等待下一个版本)。因此,这显然不是我的选择。不过,我会感兴趣:是否有更大的图书馆(在GitHub中)真正使用该概念?
piegames

它总是很多工作。我参与的每个项目几乎都具有类似的过程。回到我的Apache时代,我们可能会讨论几天,因为我们对自己的创作充满热情。我们知道很多人会使用这些库,所以我们不得不讨论一些事情,例如提议的更改是否会破坏API,提议的功能是否值得,何时应该使用?等等
帐户berin Loritsch

0

我将重新表达我的回答,因为似乎它使少数人感到不安。

类属性/方法的可见性与安全性或源代码的开放性无关。

可见性之所以存在,是因为对象易受4个特定问题的影响:

  1. 并发

如果您以未封装的形式构建模块,那么您的用户将习惯于直接更改模块状态。这在单线程环境中可以正常工作,但是一旦您考虑添加线程,就可以了。您将被迫将状态设为私有,并使用锁/监视器以及使其他线程等待资源而不是争夺资源的getter和setter。这意味着您的用户程序将不再起作用,因为无法以常规方式访问私有变量。这可能意味着您需要大量重写。

事实是,考虑到单线程运行时,编写代码要容易得多,并且private关键字允许您简单地添加关键字sync或几个锁,并且从一开始就封装用户代码就不会破坏。

  1. 帮助防止用户在使用界面的过程中冒犯自己。本质上,它可以帮助您控制对象的不变性。

每个对象都有一堆东西,要保持一致状态,它必须为真。不幸的是,这些东西存在于客户端可见的空间中,因为将每个对象移入其自己的进程并通过消息与之对话很昂贵。这意味着,如果用户具有完全可见性,则对象很容易使整个程序崩溃。

这是不可避免的,但是您可以通过仅允许用户通过精心设计的界面使对象与对象的状态进行交互,从而使程序更加健壮,从而对对象的服务进行关闭以防止意外崩溃,从而防止对象意外地进入不一致状态。这并不意味着用户不能故意破坏不变量,但如果这样做,则是客户端崩溃,他们要做的就是重启程序(要保护的数据不应存储在客户端) )。

另一个可以提高模块可用性的好例子是将构造函数设为私有。因为如果构造函数引发异常,它将杀死程序。解决此问题的一种懒惰方法是使构造函数抛出编译时错误,除非在try / catch块中,否则您无法构造该错误。通过将构造函数设为私有,并添加公共的静态create方法,可以使create方法在无法构造时返回null,或者使用回调函数来处理错误,从而使程序更易于使用。

  1. 范围污染

许多类都有很多状态和方法,尝试滚动它们很容易不知所措。这些方法中的许多只是视觉噪声,例如辅助函数,状态。将变量和方法设置为私有有助于减少范围污染,并使用户更轻松地找到所需的服务。

从本质上讲,它使您可以在类内而不是在类外使用帮助函数。没有可见性控制,而不会用一堆用户永远不会使用的服务分散用户的注意力,因此您可以将方法分解为一堆辅助方法(尽管它仍然会污染您的范围,但不会污染用户的范围)。

  1. 被依赖

精心设计的界面可以隐藏其运行所需的内部数据库/窗口/影像,并且如果您想更改为另一个数据库/另一个窗口系统/另一个影像库,则可以使该界面与用户相同不会注意到。

另一方面,如果您不执行此操作,则很容易陷入无法更改依赖项的状态,因为依赖项是公开的,并且代码依赖于此。使用足够大的系统,迁移的成本可能变得难以承受,而对其进行封装可以保护行为良好的客户端用户免于将来做出交换依赖关系的决定。


1
“毫无意义地隐藏任何东西”-那么为什么还要考虑封装呢?在许多情况下,反射确实需要特殊的特权。
Frank Hileman

您之所以考虑封装,是因为它在开发模块时为您提供了喘息的空间,并减少了误用的可能性。例如,如果您有4个线程直接修改类的内部状态,则很容易引起问题,而将变量设为私有,则鼓励用户使用公共方法来操纵世界状态,该状态可以使用监视器/锁来防止问题。这是封装的唯一真正好处。
德米特里(Dmitry)

为了安全起见,隐藏东西是进行设计的一种简便方法,最终导致必须在api中打洞。一个很好的例子是多文档应用程序,其中有许多工具箱,以及许多带有子窗口的窗口。如果您对封装不满意,最终会遇到在一个文档上绘制某些东西的情况,您必须问窗口问内部文档问内部文档要内部文档提出绘制要求并使其上下文无效。如果客户端要与客户端一起玩,则不能阻止它们。
德米特里(Dmitry)

好的,尽管在环境支持的情况下可以通过访问控制来实现安全性,但这样做更有意义,这是OO语言设计的最初目标之一。另外,您正在提倡封装,并说不要同时使用它。有点混乱。
Frank Hileman '17

我从来没有想过不要使用它。我的意思是不要为了安全而使用它;有策略地使用它来改善用户体验并为自己提供一个更流畅的开发环境。我的观点是,它与安全性或源代码的开放性无关。根据定义,客户端对象很容易自省,将它们移出用户进程空间会使未封装的事物与封装的对象同样不可访问。
德米特里(Dmitry)
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.