在阅读了关于稳定抽象原则 (SAP) 的这个wiki 之后,我想知道是否有人知道依赖抽象而不是具体的任何缺点(我想,这超过了优点)。
SAP 声明包越稳定,它应该越抽象。这意味着如果一个包不太稳定(更可能改变),那么它应该更具体。我真的不明白为什么会这样。当然,在所有情况下,无论稳定性如何,我们都应该依赖抽象并隐藏具体实现?
在阅读了关于稳定抽象原则 (SAP) 的这个wiki 之后,我想知道是否有人知道依赖抽象而不是具体的任何缺点(我想,这超过了优点)。
SAP 声明包越稳定,它应该越抽象。这意味着如果一个包不太稳定(更可能改变),那么它应该更具体。我真的不明白为什么会这样。当然,在所有情况下,无论稳定性如何,我们都应该依赖抽象并隐藏具体实现?
Robert C. Martin 总是用一种相当晦涩的方式来描述事物。他的观点总是很好,但需要一点破译—— “传入与传出耦合”,呃!关于马丁写作方式的另一件事是,描述性和规范性之间总是有点模糊(“会”还是“应该”?)
“稳定”
首先,重要的是要了解 Martin 如何定义“稳定性”。他根据传入和传出耦合来定义它,从而产生稳定性度量:
instability = efferent / (efferent + afferent)
“传入”和“传出”是这样晦涩难懂的术语。为简单起见,让我们使用“传出依赖”代替“传出耦合”,使用“传入依赖”代替“传入耦合”。所以我们有这个:
instability = outgoing / (outgoing + incoming)
它与改变的可能性非常不同,并且与改变的难度有关。尽管令人困惑,但根据这个定义,一个“稳定”的包仍然可能一直在变化(当然,这会很糟糕,而且真的很难管理)。
如果您使用上述公式得到除以零的错误,那么您的包既没有被使用也没有使用任何东西。
稳定依赖原则
要在上下文中理解 Martin 关于 SAP 的观点,从 SDP(稳定依赖原则)开始会更容易。它指出:
包之间的依赖关系应该是包的稳定性方向。一个包应该只依赖于比它更稳定的包。
这很容易理解。更改设计的成本与传入的依赖项的数量(和复杂性)有关。可能任何在大型代码库中工作过的人都可以很快理解这一点,因为中心设计更改可能最终想要破坏代码库中的 10,000 个非常复杂的部分。
所以依赖应该(会?)流向不变、根深蒂固、坚定不移的部分,就像一棵树从叶子流向根部。
根应该具有的稳定性度量归结为零传出耦合(零传出依赖性)。也就是说,这个稳定的“根”包不应该依赖于其他任何东西。换句话说,它应该完全独立于外部世界。这是根据 Martin 的指标定义“最大稳定性”的特征:完全独立。
Maximum independence = "stable root" (as I'm calling it)
Maximum dependence = "unstable leaf" (as I'm calling it)
鉴于这种完全独立、超稳定的根设计,我们如何才能重新获得一定程度的灵活性,以便我们可以轻松扩展和更改其实现而不影响接口/设计?这就是抽象的用武之地。
稳定抽象原则
抽象允许我们将实现与接口/设计分离。
因此,这里出现了稳定抽象原则:
最稳定的包应该是最抽象的。不稳定的包应该是具体的。包的抽象性应该与其稳定性成正比。
正如 SDP 所说,这个想法是让这些中心根设计非常稳定,同时仍然保留一定程度的灵活性,以进行不通过抽象影响核心设计的更改。
举个简单的例子,考虑一个软件开发工具包,它位于某些引擎的核心,并被全球插件开发人员使用。根据定义,考虑到大量传入依赖项(所有这些插件开发人员使用它)与最小或没有传出依赖项(SDK 几乎不依赖其他)的组合,这个 SDK 必须具有非常稳定的设计。这个原则表明它的接口应该是抽象的,以便在不影响稳定设计的情况下具有最大程度的更改灵活性。
这里的“适度抽象”可能是一个抽象基类。“最大程度地抽象”将是一个纯接口。
具体的
另一方面,抽象是对具体的需求。否则将无法提供抽象的实现。所以这个原则也表明混凝土部分应该(会?)是不稳定的部分。如果你把它想象成一棵树(与通常的编程树相反),依赖关系从叶子向下流到根,叶子应该是最具体的,根应该是最抽象的。
叶子通常具有最外向的依赖关系(对外部事物的大量依赖——对所有这些分支和根的依赖),而它们的传入依赖关系为零(没有任何东西依赖于它们)。根是相反的(一切都依赖于它们,它们不依赖于任何东西)。
我就是这样理解马丁的描述的。它们很难理解,我可能在某些部分上有所偏差。
当然,在所有情况下,无论稳定性如何,我们都应该依赖抽象并隐藏具体实现?
也许您更多地考虑实体。实体的抽象接口仍然需要某个地方的具体实现。具体部分可能是不稳定的,并且同样更容易改变,因为没有其他东西直接依赖于它(没有传入耦合)。抽象部分应该是稳定的,因为许多可能依赖于它(大量传入依赖项,很少或没有传出依赖项),因此很难更改。
同时,如果您逐步建立一个更依赖的包,例如应用程序包,您的应用程序的主要入口点将所有内容组装在一起,那么在这里将所有接口抽象化通常会增加更改的难度,并且仍然会将需要在其他地方进行具体(不稳定)实现。在代码库中的某个时刻,如果只是为抽象接口选择适当的具体实现,就必须依赖于具体部分。
抽象还是不抽象
我想知道是否有人知道依赖抽象而不是具体的任何缺点(我想,这超过了优点)。
性能浮现在脑海。通常,抽象以动态分派的形式具有某种运行时成本,例如,然后变得容易受到分支错误预测的影响。Martin 的很多著作都围绕着经典的面向对象范式展开。此外,OOP 通常希望在单一实体级别对事物进行建模。在极端层面上,它可能希望将图像的单个像素变成具有自己操作的抽象接口。
在我的领域中,我倾向于使用具有面向数据的设计思维的实体组件系统。这种颠覆了经典的 OOP 世界。结构通常被设计为一次聚合多个实体的数据,其设计思维是寻找最佳内存布局(为机器设计,而不是为人类设计)。实体被设计为组件的集合,组件被建模为使用面向数据的思维方式的原始数据。对于处理组件的系统来说,接口仍然是抽象的,但是抽象是为了批量处理事物而设计的,并且依赖关系从系统流向了丝毫没有抽象的中央组件。
这是游戏引擎中使用的一种非常常见的技术,它在性能和灵活性方面提供了很大的潜力。然而,这与 Martin 对面向对象编程的关注形成了鲜明的对比,因为它与 OOP 的整体大相径庭。
这意味着如果一个包不太稳定(更可能改变),那么它应该更具体。我真的不明白为什么会这样。
抽象是软件中难以改变的东西,因为一切都依赖于它们。如果你的包要经常改变并且它提供了抽象,那么当你改变某些东西时,依赖它的人将被迫重写一大堆代码。但是如果你的不稳定包提供了一些具体的实现,那么在更改之后将不得不重写更少的代码。
因此,如果您的包要经常更改,它应该更好地提供具体而不是抽象。不然……谁会用?;)
首先,从您链接到的论文中:
稳定性不是衡量模块发生变化的可能性;而是衡量更改模块的难度
所以难以改变的事情(例如在许多地方使用)应该是抽象的,以使扩展变得容易/可能。
是的,有缺点。这是改变的容易程度。更改具体代码比更改抽象和代码要容易和快捷得多。
当然,在所有情况下,无论稳定性如何,我们都应该依赖抽象并隐藏具体实现?
那是真实的。但抽象级别不同。即时示例:如果我要求您计算正方形对角线的长度,那么您可能只会使用内置double sqrt(double)
函数。是抽象的吗?是的。我们不知道是否使用了牛顿方法,还是直接委托给 cpu。
但是如果我们想创建一个 sqrt 函数并依赖某种物理计算库呢?在这种情况下,之前的抽象是否足够?可能不像我们可能想要(以统一的方式)处理矩阵、相对错误、任意长度数、所需数量的核心/线程的并行化,可能委托给 gpu,它应该为其他扩展做好准备,因为迟早有人可能希望它处理 NaN 和虚数。
所以它仍然是 sqrt 函数,但抽象级别更高一些。那只是因为很多代码都依赖于它。哪个功能更容易改变?