依赖抽象有什么明显的缺点吗?


9

我正在阅读有关稳定抽象原理(SAP)的Wiki

SAP指出,软件包越稳定,它应该越抽象。这意味着,如果包装的稳定性较差(更容易更改),则应更具体。我不太了解的是为什么会这样。当然在所有情况下,无论稳定性如何,我们都应该依赖抽象并隐藏具体实现?


尝试使用您不满意的组件,而不使用它提供的抽象,而是在比您习惯的低一个级别上完成所有细节。这将使您对抽象的优点和缺点有一个很好的印象。
Kilian Foth,2015年

2
您是否阅读了链接的文章和/或该文章所基于的书
约尔格W¯¯米塔格

1
+1好问题,尤其是因为我认为稳定性和抽象之间的关系不会立即变得直观。 本文的第11页有帮助,其抽象案例的例子很有意义,但也许有人可以写出更具体的案例。请求取消保留。
迈克

您要针对这些抽象处理哪个问题领域?如C2所述:“在对现实世界域进行建模时–客户,员工,发票,物料清单,产品,SKU,工资单等的世界-可能很难找到稳定的抽象。计算域-堆栈,队列,函数,树,进程,线程,图形控件,报告,表单等的世界-更有可能保持稳定。” 和“在某些领域,很难获得稳定的抽象。” 不知道您要使用SAP解决什么问题,很难给您一个好的答案。

@JörgWMittag和Mike-是的,我阅读了这篇文章。我只是觉得为什么“不稳定的包装应该是具体的”缺乏解释。在所述文章的第13页上,他显示了一个图表,但并未真正详细地解释为什么应避免图表上的(1,1)?基本上不稳定的想法是否意味着较少的传入依赖,并且那里不需要使用抽象?如果是这样的......难道不是很好的做法,使用抽象反正,以防万一与需求变化的稳定性变化..
SteveCallender

Answers:


7

可以将您的包视为API,以本文中的示例为例,对Readerwith string Reader.Read()Writerwith 进行定义,并将其void Writer.Write(string)作为抽象API。

然后,您可以Copy使用方法Copier.Copy(Reader, Writer)和实现Writer.Write(Reader.Read())以及可能的健全性检查来创建一个类。

现在,你做出的具体实现,例如FileReaderFileWriterKeyboardReaderDownloadThingsFromTheInternetReader

如果您想更改您的实现,该FileReader怎么办?没问题,只需更改类并重新编译即可。

如果要更改抽象的定义Reader怎么办?哎呀,你不能改变,但你也必须改变CopierFileReaderKeyboardReaderDownloadThingsFromTheInternetReader

这就是“稳定抽象原理”的基本原理:使具体化不如抽象稳定。


1
我同意您所说的一切,但我相信作者对稳定性的定义与您的定义不同。您将稳定性视为需要更改的内容,作者说:“稳定性不是度量模块更改可能性的方法;而是度量更改模块的难度的方法。” 因此,我的问题更多是,为什么对于易于更改为更具体而不是抽象的软件包有什么好处?
SteveCallender 2015年

1
@SteveCallender这是一个微妙的区别:作者对“稳定性”的定义是大多数人所说的“对稳定性的需求”,即,模块越依赖模块,模块就越需要“稳定”。
Residuum

6

因为YAGNI

如果您目前只有一件事情的实现,那为什么还要多花一层多余的钱呢?这只会导致不必要的复杂性。更糟糕的是,有时您会提供抽象的想法,直到第二次实现的那一天……而这一天再也没有发生。真是浪费工作!

我还认为,要问自己的真正问题不是“我需要依赖抽象吗?” 而是“我需要模块化吗?”。并非总是需要模块化,请参见下文。

在我正在工作的公司中,我开发的某些软件与某些必须与之通信的硬件设备紧密相连。这些设备是为实现非常具体的目标而开发的,除了模块化以外,都是其他设备。:-)一旦第一生产设备就走出了工厂,并在某处安装,无论是它的固件和硬件无法改变,永远

因此,我可以确定软件的某些部分将永远不会发展。这些部分不需要依赖抽象,因为它仅存在一种实现,并且这一实现永远不会改变。在代码的这些部分上声明抽象只会使每个人困惑,并且会花费更多时间(不产生任何值)。


1
我倾向于同意YAGNI,但我想知道您的榜样。您是否从未在不同的设备上重复任何代码?我很难相信,同一家公司的设备之间没有通用的代码。另外,当您不修复固件中的错误时,客户端会如何?你是说有从未有错误,永远?如果您在4个不同的实现中具有相同的代码,但是如果不在通用模块中,则必须修复4次。
Fuhrmanator 2015年

1
@Fuhrmanator通用代码不同于抽象。通用代码只能表示一个辅助方法或库-不需要抽象。
艾隆2015年

@Fuhrmanator当然我们在库中有通用的代码,但是正如Eilon所说,并不是所有的东西都依赖抽象(但是某些部分依赖抽象)。我从来没有说过从来没有错误,我说过不能对其进行修补(出于超出OP问题范围的原因)。
斑点

@Eilon我的评论是关于模块化并非总是需要的(不是抽象的)。
Fuhrmanator 2015年

@Spotted关于无法修补没有问题。这只是一个非常具体的示例,并不是大多数软件的典型示例。
Fuhrmanator 2015年

6

我想您可能会对罗伯特·马丁(Robert Martin)选择的马stable这个词感到困惑。我认为这是混乱的开始:

这意味着,如果包装的稳定性较差(更容易更改),则应更具体。

如果您通读了原始文章,您将看到(重点是我):

稳定一词的经典定义是:“不易移动”。 这就是我们将在本文中使用的定义。就是说,稳定性不是衡量模块更换可能性的标准。而是衡量更换模块难度的衡量标准

显然,更难更改的模块将减少易失性。模块更换的难度越大,即模块越稳定,其挥发性就越小。

我一直在为作者选择“ 稳定 ”一词而苦恼,因为我(像您一样)倾向于考虑稳定的“可能性”方面,即不太可能改变困难之处在于,更改该模块将破坏许多其他模块,并且修复该代码将需要进行大量工作。

马丁还使用独立负责任的词,对我来说,传达了更多的含义。在培训研讨会上,他用一个比喻来说明孩子的父母成长过程,以及他们应该如何“负责任”,因为他们的孩子依赖他们。在现实生活中,离婚,失业,监禁等都是父母的改变会对孩子产生负面影响的典型例子。因此,父母应该为了孩子的利益而“稳定”。顺便说一下,孩子/父母的这种隐喻不一定与OOP中的继承有关!

因此,本着“负责任”的精神,我提出了难以更改(或不应更改)的替代含义:

  • 强制的-表示其他类依赖于该类,因此不应更改。
  • Beholden-同上。
  • 受限-此类的义务限制了其更改的便利。

因此,将这些定义插入语句中

包越稳定,它应该越抽象

  • 包越有义务,它应该越抽象
  • 感激包更抽象的应该是
  • 包越受约束,它应该越抽象

让我们引用稳定抽象原理(SAP),强调令人困惑的单词“稳定/不稳定”:

最大稳定的软件包应该最大程度抽象。不稳定的包装应该是具体的。包装的抽象程度应与其稳定性成正比。

不用这些令人困惑的词来澄清它:

最大程度依赖于系统其他部分的软件包应该最大程度地抽象。可以轻松更改的软件包应该是具体的。程序包的抽象性应与修改的难度成比例。

TL; DR

您的问题标题是:

依赖抽象有什么明显的缺点吗?

我认为,如果您正确地创建了抽象(例如,它们存在是因为很多代码依赖于它们),那么就没有任何明显的缺点。


0

这意味着,如果包装的稳定性较差(更容易更改),则应更具体。我不太了解的是为什么会这样。

抽象是很难在软件中更改的事物,因为一切都取决于它们。如果您的程序包将经常更改并且提供抽象,那么当您更改某些内容时,依赖它的人将被迫重写大量代码。但是,如果您的不稳定软件包提供了一些具体的实现,则更改后必须重写的代码要少得多。

因此,如果您的程序包经常更改,则最好提供具体的而不是抽象的内容。否则...谁会使用它?;)


0

请记住马丁的稳定性指标以及“稳定性”的含义:

Instability = Ce / (Ca+Ce)

要么:

Instability = Outgoing / (Incoming+Outgoing)

也就是说,如果一个软件包的所有依赖项都传出,则它被认为是完全不稳定的:它使用其他东西,但没有任何东西使用它。在这种情况下,只有将事情具体化才有意义。由于也没有其他使用它,因此它也将是最容易更改的代码,因此,如果修改了该代码,其他任何内容都不会中断。

同时,当您遇到一个完整的“稳定性”的相反情形时,一个或多个事物使用的包却没有单独使用任何东西,例如软件使用的中央包,那就是马丁说这件事应该抽象。SOLI(D)的DIP部分(依赖项反转原则)也对此进行了增强,该原则基本上指出,对于低级和高级代码,依赖项应均匀地流向抽象。

也就是说,依赖关系应均匀地流向“稳定性”,更确切地说,依赖关系应流向具有比传入依赖关系更多的传入依赖关系的包,此外,依赖关系应流向抽象。其背后的基本原理是,抽象提供了喘息的空间,可以用一种子类型替代另一种子类型,从而为实现接口更改的具体部分提供了一定程度的灵活性,而又不会破坏对该抽象接口的传入依赖性。

依赖抽象有什么明显的缺点吗?

好吧,实际上我至少在这里就我的领域不同意马丁,在这里我需要引入“稳定性”的新定义,例如“缺乏改变的理由”。在那种情况下,我会说依赖关系应该朝着稳定的方向发展,但是如果抽象接口不稳定,那么抽象接口就无济于事(根据我对“不稳定”的定义,因为容易反复更改,而不是马丁的)。如果开发人员无法正确地提取抽象,并且客户反复改变主意,从而使抽象的建模软件模型不完整或无效,那么我们将不再受益于抽象接口增强的灵活性来保护系统免受级联的破坏性依赖的更改。就我个人而言,我发现了ECS引擎,例如AAA游戏中的引擎,最具体:针对原始数据,但是此类数据非常稳定(例如,“不太可能需要更改”)。我经常发现某些需要将来更改的可能性比指导SE决策中传出与总耦合的比率更有用。

因此,我会稍微改变一下DIP,然后说:“依赖关系应该流向那些需要进一步更改的可能性最低的组件”,无论这些组件是抽象接口还是原始数据。对我而言,最重要的是他们可能需要直接破坏设计的更改。仅当某种事物通过抽象降低了这种可能性时,抽象才在稳定的上下文中有用。

在许多情况下,体面的工程师和客户可能会遇到这种情况,他们预见了软件的需求并设计了稳定的(如不变的)抽象,而这些抽象为他们提供了交换具体实现所需的所有喘息空间。但是在某些领域中,抽象可能不稳定并且容易出现不足,而引擎所需的数据可能更容易预测和预先稳定。因此,在这些情况下,从可维护性的角度(易于更改和扩展系统)的角度来看,依赖关系流向数据而不是抽象流实际上会更加有益。在ECS中,最不稳定的部分(如最经常更改的部分)通常是系统中的功能(PhysicsSystem(例如),而最稳定的部分(至少可能要更改)是仅由MotionComponent所有系统使用的原始数据(例如)组成的组件。

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.