您似乎使用了“封装”的相当狭窄的定义。假设您将封装定义为“将数据与方法相结合”,我是否正确?</p>
如果我错了,请忽略这篇文章的其余部分。
封装不是一个松散的术语;事实上,它是由国际标准化组织定义的。ISO 的开放分布式处理参考模型 - 定义了以下五个概念:
实体:任何具体或抽象的感兴趣的事物。
对象:实体的模型。一个对象的特征在于它的行为,双重地,它的状态。
(对象的)行为:行为的集合,对何时可能发生具有一组约束。
接口:对象行为的抽象,由该对象的交互的子集以及它们何时可能发生的一组约束组成。
封装:对象中包含的信息只能通过对象支持的接口的交互来访问的属性。
我们可以进一步提出一个不言而喻的建议:由于某些信息可以通过这些接口访问,因此某些信息必须在对象中隐藏和无法访问。此类信息表现出的属性称为信息隐藏,Parnas 通过认为模块应该被设计为隐藏困难的决策和可能改变的决策来定义,参见一篇伟大的计算论文:
http://www.cs.umd.edu/class/spring2003/cmsc838p/Design/criteria.pdf
重要的是要注意,不仅数据是信息隐藏的:它是与对象相关联的一些行为的子集,这些行为很难或可能会改变。
在您的帖子中,您似乎在说 OO 和函数式编程中的封装之间的区别源于数据管理,但至少根据 ISO 和 Parnas,数据管理不是封装的关键。所以我不明白为什么函数式编程中的封装需要与 OO 中的封装有任何不同。
此外,您在帖子中提到函数式编程提供封装,“……通过方法而不是数据结构。” 我认为,这是规模上的差异,而不是绝对的差异。如果我使用“对象”这个词而不是“数据结构”(如果我误解了,请再次告诉我),那么您似乎发现了 OO 的对象封装和函数式编程的方法封装的意义。
然而,根据上面的 ISO 定义,对象是我希望建模的任何东西。因此类可以被封装在一个包中,只要这些类中的一些对包的接口有贡献(即包的公共类)并且一些是信息隐藏的(包中的私有类)。
出于同样的原因,方法被封装在一个类中——一些方法是公共的,一些是私有的。您甚至可以将其降低一个档次,并说 McCabian 顺序代码序列封装在方法中。每个都形成一个封装在封装区域内的节点图;所有这些图形成一个图栈。因此函数式编程可以很好地封装在函数/文件级别,但这与OO的方法/类图没有区别,与OO的类/包图本质上也没有区别。
另外,请注意 Parnas 在上面使用的词:change。信息隐藏涉及潜在事件,例如未来困难设计决策的变化。你问OO的优势在哪里;好吧,封装当然是面向对象的强项,但问题就变成了,“封装的强项在哪里?” 答案非常明确:变革管理。特别是,封装减少了最大的潜在变化负担。
“潜在耦合”的概念在这里很有用。
“耦合”本身被定义为“通过从一个模块到另一个模块的连接建立的关联强度的度量”,在另一篇计算的伟大论文中:
http://www.research.ibm.com/journal/sj/382/stevens.pdf
正如论文所说,用从未有过的改进的话,“最小化模块之间的连接还可以最小化更改和错误传播到系统其他部分的路径,从而消除灾难性的“涟漪”效应,其中一个部分的变化会导致另一个错误,需要在其他地方进行额外的更改,从而产生新的错误,等等。”</p>
然而,正如这里所定义的,有两个限制可以很容易地解除。首先,耦合不能衡量模块内的连接,这些模块内的连接会产生与模块间连接一样多的“涟漪”效应(论文确实定义了“内聚”来关联模块内元素,但这不是根据定义耦合的元素之间的连接(即,对标签或地址的引用)来定义的)。其次,任何计算机程序的耦合都是给定的,因为模块是相互连接的;在耦合的定义中几乎没有管理 Parnas 所说的潜在变化的范围。
在某种程度上,这两个问题都通过潜在耦合的概念得到解决:程序的所有元素之间可形成的最大可能连接数。例如,在 Java 中,包中的包私有(默认访问器)类不能在其上形成连接(即,没有外部类可以依赖它,尽管有反射),但包中的公共类可以对它有依赖。即使目前没有其他类依赖它,这个公共类也会促成潜在的耦合——当设计发生变化时,类可能会依赖它。
要查看封装的强度,请考虑负担原则。负担原则有两种形式。
强形式表明,转换实体集合的负担是转换实体数量的函数。弱形式表明转换实体集合的最大潜在负担是转换实体的最大潜在数量的函数。
创建或修改任何软件系统的负担是创建或修改的类数量的函数(这里我们使用“类”,假设一个 OO 系统,并且关注类/包级别的封装;我们同样可以有关注函数式编程的函数/文件级别)。(请注意,“负担”是现代软件开发通常是成本或时间,或两者兼而有之。)依赖于特定修改类的类比不依赖于修改类的类更有可能受到影响.
修改后的类可以施加的最大潜在负担是对依赖它的所有类的影响。
因此,减少对已修改类的依赖关系会降低其更新影响其他类的可能性,从而减少该类可能施加的最大潜在负担。(这只不过是对“结构化设计”论文的重新陈述。)
因此,减少系统中所有类之间的最大潜在依赖关系数会降低对特定类的影响将导致对其他类进行更新的可能性,从而减少所有更新的最大潜在负担。
封装通过减少所有类之间的最大潜在依赖数,因此减轻了负担原则的弱形式。这一切都包含在“封装理论”中,它试图用数学方法证明这样的断言,使用潜在的耦合作为构建程序的逻辑手段。
但是请注意,当您问“封装是所有有价值代码的关键吗?”时,请注意。答案肯定是:不。所有有价值的代码都没有单一的关键。在某些情况下,封装只是一种帮助提高代码质量的工具,因此它可能变得“值得”。</p>
您还写道,“……封装可能成为对象灵活扩展的障碍。” 是的,它肯定可以:它确实被设计为阻止扩展难以或可能更改的对象的设计决策。然而,这并不是一件坏事。另一种方法是让所有类都公开,并让程序表达其最大的潜在耦合;但随后负担原则的弱形式表明更新将变得越来越昂贵;这些是衡量扩展障碍的成本。
最后,您在封装和语义之间进行了有趣的比较,在您看来,OO 的语义是其更大的优势。我也不是语义学家(在优秀的拉姆齐先生在他的评论中提到它之前,我什至不知道存在这样一个词)但我认为你的意思是“语义”,在“意义或解释一个词的含义”,并且非常基本的是,具有 woof() 方法的类应该称为 Dog。
这种语义确实有很大的力量。
令我好奇的是,您将语义与封装相提并论并寻找赢家;我怀疑你会找到一个。
在我看来,有两种力量推动封装:语义和逻辑。
语义封装仅仅意味着基于节点(使用通用术语)封装的含义的封装。所以如果我告诉你我有两个包,一个叫做“动物”,一个叫做“矿物”,然后给你三个类 Dog、Cat 和 Goat 并询问这些类应该封装到哪些包中,那么,给定没有其他信息,您完全正确地声称系统的语义表明这三个类被封装在“动物”包中,而不是“矿物”包中。
然而,封装的另一个动机是逻辑,特别是上面提到的潜在耦合的研究。封装理论实际上提供了用于封装多个类以最小化潜在耦合的包数量的方程式。
对我来说,封装作为一个整体是这种语义和逻辑方法之间的权衡:如果这使程序在语义上更易于理解,我将允许我的程序的潜在耦合高于最小值;但是巨大而浪费的潜在耦合级别将警告我的程序需要重新构建,无论它在语义上多么明显。
(如果优秀的拉姆齐先生还在阅读,你或你的语义学家朋友能否给我一个更好的词来形容我在这里使用的“语义”阶段?最好使用更合适的术语。)
问候,埃德。