我正在阅读有关稳定抽象原理(SAP)的Wiki。
SAP指出,软件包越稳定,它应该越抽象。这意味着,如果包装的稳定性较差(更容易更改),则应更具体。我不太了解的是为什么会这样。当然在所有情况下,无论稳定性如何,我们都应该依赖抽象并隐藏具体实现?
我正在阅读有关稳定抽象原理(SAP)的Wiki。
SAP指出,软件包越稳定,它应该越抽象。这意味着,如果包装的稳定性较差(更容易更改),则应更具体。我不太了解的是为什么会这样。当然在所有情况下,无论稳定性如何,我们都应该依赖抽象并隐藏具体实现?
Answers:
可以将您的包视为API,以本文中的示例为例,对Reader
with string Reader.Read()
和Writer
with 进行定义,并将其void Writer.Write(string)
作为抽象API。
然后,您可以Copy
使用方法Copier.Copy(Reader, Writer)
和实现Writer.Write(Reader.Read())
以及可能的健全性检查来创建一个类。
现在,你做出的具体实现,例如FileReader
,FileWriter
,KeyboardReader
和DownloadThingsFromTheInternetReader
。
如果您想更改您的实现,该FileReader
怎么办?没问题,只需更改类并重新编译即可。
如果要更改抽象的定义Reader
怎么办?哎呀,你不能改变,但你也必须改变Copier
,FileReader
, KeyboardReader
和DownloadThingsFromTheInternetReader
。
这就是“稳定抽象原理”的基本原理:使具体化不如抽象稳定。
因为YAGNI。
如果您目前只有一件事情的实现,那为什么还要多花一层多余的钱呢?这只会导致不必要的复杂性。更糟糕的是,有时您会提供抽象的想法,直到第二次实现的那一天……而这一天再也没有发生。真是浪费工作!
我还认为,要问自己的真正问题不是“我需要依赖抽象吗?” 而是“我需要模块化吗?”。并非总是需要模块化,请参见下文。
在我正在工作的公司中,我开发的某些软件与某些必须与之通信的硬件设备紧密相连。这些设备是为实现非常具体的目标而开发的,除了模块化以外,都是其他设备。:-)一旦第一生产设备就走出了工厂,并在某处安装,无论是它的固件和硬件无法改变,永远。
因此,我可以确定软件的某些部分将永远不会发展。这些部分不需要依赖抽象,因为它仅存在一种实现,并且这一实现永远不会改变。在代码的这些部分上声明抽象只会使每个人困惑,并且会花费更多时间(不产生任何值)。
我想您可能会对罗伯特·马丁(Robert Martin)选择的马stable这个词感到困惑。我认为这是混乱的开始:
这意味着,如果包装的稳定性较差(更容易更改),则应更具体。
如果您通读了原始文章,您将看到(重点是我):
稳定一词的经典定义是:“不易移动”。 这就是我们将在本文中使用的定义。就是说,稳定性不是衡量模块更换可能性的标准。而是衡量更换模块难度的衡量标准。
显然,更难更改的模块将减少易失性。模块更换的难度越大,即模块越稳定,其挥发性就越小。
我一直在为作者选择“ 稳定 ”一词而苦恼,因为我(像您一样)倾向于考虑稳定的“可能性”方面,即不太可能改变。困难之处在于,更改该模块将破坏许多其他模块,并且修复该代码将需要进行大量工作。
马丁还使用独立和负责任的词,对我来说,传达了更多的含义。在培训研讨会上,他用一个比喻来说明孩子的父母成长过程,以及他们应该如何“负责任”,因为他们的孩子依赖他们。在现实生活中,离婚,失业,监禁等都是父母的改变会对孩子产生负面影响的典型例子。因此,父母应该为了孩子的利益而“稳定”。顺便说一下,孩子/父母的这种隐喻不一定与OOP中的继承有关!
因此,本着“负责任”的精神,我提出了难以更改(或不应更改)的替代含义:
因此,将这些定义插入语句中
包越稳定,它应该越抽象
让我们引用稳定抽象原理(SAP),强调令人困惑的单词“稳定/不稳定”:
最大稳定的软件包应该最大程度抽象。不稳定的包装应该是具体的。包装的抽象程度应与其稳定性成正比。
不用这些令人困惑的词来澄清它:
最大程度依赖于系统其他部分的软件包应该最大程度地抽象。可以轻松更改的软件包应该是具体的。程序包的抽象性应与修改的难度成比例。
您的问题标题是:
依赖抽象有什么明显的缺点吗?
我认为,如果您正确地创建了抽象(例如,它们存在是因为很多代码依赖于它们),那么就没有任何明显的缺点。
这意味着,如果包装的稳定性较差(更容易更改),则应更具体。我不太了解的是为什么会这样。
抽象是很难在软件中更改的事物,因为一切都取决于它们。如果您的程序包将经常更改并且提供抽象,那么当您更改某些内容时,依赖它的人将被迫重写大量代码。但是,如果您的不稳定软件包提供了一些具体的实现,则更改后必须重写的代码要少得多。
因此,如果您的程序包经常更改,则最好提供具体的而不是抽象的内容。否则...谁会使用它?;)
请记住马丁的稳定性指标以及“稳定性”的含义:
Instability = Ce / (Ca+Ce)
要么:
Instability = Outgoing / (Incoming+Outgoing)
也就是说,如果一个软件包的所有依赖项都传出,则它被认为是完全不稳定的:它使用其他东西,但没有任何东西使用它。在这种情况下,只有将事情具体化才有意义。由于也没有其他使用它,因此它也将是最容易更改的代码,因此,如果修改了该代码,其他任何内容都不会中断。
同时,当您遇到一个完整的“稳定性”的相反情形时,一个或多个事物使用的包却没有单独使用任何东西,例如软件使用的中央包,那就是马丁说这件事应该抽象。SOLI(D)的DIP部分(依赖项反转原则)也对此进行了增强,该原则基本上指出,对于低级和高级代码,依赖项应均匀地流向抽象。
也就是说,依赖关系应均匀地流向“稳定性”,更确切地说,依赖关系应流向具有比传入依赖关系更多的传入依赖关系的包,此外,依赖关系应流向抽象。其背后的基本原理是,抽象提供了喘息的空间,可以用一种子类型替代另一种子类型,从而为实现接口更改的具体部分提供了一定程度的灵活性,而又不会破坏对该抽象接口的传入依赖性。
依赖抽象有什么明显的缺点吗?
好吧,实际上我至少在这里就我的领域不同意马丁,在这里我需要引入“稳定性”的新定义,例如“缺乏改变的理由”。在那种情况下,我会说依赖关系应该朝着稳定的方向发展,但是如果抽象接口不稳定,那么抽象接口就无济于事(根据我对“不稳定”的定义,因为容易反复更改,而不是马丁的)。如果开发人员无法正确地提取抽象,并且客户反复改变主意,从而使抽象的建模软件模型不完整或无效,那么我们将不再受益于抽象接口增强的灵活性来保护系统免受级联的破坏性依赖的更改。就我个人而言,我发现了ECS引擎,例如AAA游戏中的引擎,最具体:针对原始数据,但是此类数据非常稳定(例如,“不太可能需要更改”)。我经常发现某些需要将来更改的可能性比指导SE决策中传出与总耦合的比率更有用。
因此,我会稍微改变一下DIP,然后说:“依赖关系应该流向那些需要进一步更改的可能性最低的组件”,无论这些组件是抽象接口还是原始数据。对我而言,最重要的是他们可能需要直接破坏设计的更改。仅当某种事物通过抽象降低了这种可能性时,抽象才在稳定的上下文中有用。
在许多情况下,体面的工程师和客户可能会遇到这种情况,他们预见了软件的需求并设计了稳定的(如不变的)抽象,而这些抽象为他们提供了交换具体实现所需的所有喘息空间。但是在某些领域中,抽象可能不稳定并且容易出现不足,而引擎所需的数据可能更容易预测和预先稳定。因此,在这些情况下,从可维护性的角度(易于更改和扩展系统)的角度来看,依赖关系流向数据而不是抽象流实际上会更加有益。在ECS中,最不稳定的部分(如最经常更改的部分)通常是系统中的功能(PhysicsSystem
(例如),而最稳定的部分(至少可能要更改)是仅由MotionComponent
所有系统使用的原始数据(例如)组成的组件。