多少个设计模式和抽象级别是必需的?[关闭]


29

我怎样才能知道我的软件有太多的抽象和太多的设计模式,或者反过来,我怎么知道它是否应该有更多的抽象呢?

与我合作的开发人员在这些方面的编程方式有所不同。

有些人确实提取每个小功能,尽可能使用设计模式,并不惜一切代价避免冗余。

其他人,包括我在内,都试图更加务实,并编写出并非完全适合每种设计模式的代码,但由于应用了较少的抽象,因此其理解速度更快。

我知道这是一个权衡。我如何知道何时将足够的抽象放入项目中,又如何知道需要更多抽象呢?

例如,当使用Memcache编写通用缓存层时。难道我们真的需要MemcacheMemcacheAdapterMemcacheInterfaceAbstractCacheCacheFactoryCacheConnector,...,或者这是更易于维护和使用仍然只有一半时,好的代码这些类的?

在Twitter中找到了这个:

在此处输入图片说明

https://twitter.com/rawkode/status/875318003306565633


58
如果您将设计模式当作是从存储桶中拔出来并用来汇编程序的东西,那么您使用的太多了。
Blrfl


5
您可能会想到这种设计模式,例如人们使用的语音模式。因为从某种意义上说,习语,隐喻等都是设计模式。如果您在每个句子中都使用成语,那可能太频繁了。但是它们可以帮助澄清思想,并有助于理解原本漫长的散文墙。回答“我应该多久使用一次隐喻?”并没有真正正确的方法,这取决于作者的判断。
总理

8
单个SE答案不可能完全涵盖此主题。从字面上看,这需要多年的经验和指导。这显然太广泛了。
jpmc26

5
遵循航空业的基本设计原则:“简化并增加亮度”。当您发现修复bug的最佳方法只是删除包含该bug的代码,因为即使没有bug,它仍然无济于事,您就可以正确地进行设计!
alephzero

Answers:


52

一顿饭需要多少成分?您需要制造多少零件的车辆?

您知道,当实现的微小更改导致整个代码的级联更改时,您的抽象度就太少了。适当的抽象将有助于隔离需要更改的代码部分。

您知道,当接口的少量更改导致整个代码在不同级别上进行一系列更改时,您的抽象太多了。您发现自己修改了几十个类和接口,只是为了添加属性或更改方法参数的类型,而不是更改两个类之间的接口。

除此之外,实际上没有办法通过给出数字来回答问题。从一个项目到另一个项目,从一种语言到另一种语言,甚至从一个开发人员到另一个开发人员,抽象的数量都不相同。


28
如果只有两个接口,并且有数百个实现它们的类,则更改接口会导致级联更改,但这并不意味着会有太多抽象,因为只有两个接口。
图兰斯·科尔多瓦

同一项目的不同部分的抽象数量甚至都不一样!
T. Sar-恢复莫妮卡

提示:从Memcache更改为另一种缓存机制(Redis?)是实现上的更改。
Ogre Psalm33年

正如Tulains所展示的那样,您的两个规则(准则,无论您想称呼它们如何)都不起作用。即使他们做到了,他们也可悲地是不完整的。文章的其余部分是一个无答案的内容,仅代表我们无法提供合理范围的答案。-1
jpmc26 2015年

我要说的是,在两个接口和数以百计的执行这些类的话,你很可能已经捉襟见肘的抽象。我肯定在许多地方重复使用非常模糊的抽象的项目中看到了这一点interface Doer {void prepare(); void doIt();},并且当这种抽象不再适合时,进行重构变得很痛苦。答案的关键部分是测试适用的抽象必须改变-如果它从来不会,它永远不会引起疼痛。
James_pic

24

设计模式的问题可以用谚语概括:“当您拿着锤子时,一切看起来都像钉子。” 应用设计模式的行为并不能改善您的程序。实际上,我认为如果要添加设计模式,则您正在编写一个更复杂的程序。问题仍然是您是否在充分利用设计模式,这是问题的核心,即“什么时候抽象太多?”

如果要为一个实现创建接口和抽象超类,则已在项目中添加了两个多余且不必要的组件。提供接口的目的是能够在整个程序中平等地处理它,而又不知道其工作方式。抽象超类的重点是为实现提供底层行为。如果只有一个实现,则将获得所有复杂接口和摘要表类提供的任何优点。

同样,如果您使用的是Factory模式,并且发现自己转换了一个类以使用仅超类可用的功能,则Factory模式不会给您的代码带来任何好处。您仅向项目中添加了可以避免的其他类。

TL; DR我的观点是抽象的目的本身并不是抽象的。它在您的程序中具有非常实用的目的,在决定使用设计模式或创建接口之前,您应该问自己,这样做是否使程序更易于理解,尽管程序更加复杂或功能更强大尽管存在额外的复杂性(最好两者兼而有之)。如果答案是否定的,那么请花几分钟的时间考虑为什么要这样做,如果可以用更好的方法代替,而不必在代码中添加抽象。


类比锤子就是只知道一种设计模式的问题。设计模式应创建一个完整的工具包,以供选择并在适当的地方应用。您无需选择大锤就能开裂。
Pete Kirkham

@PeteKirkham是的,但是即使您要使用一整套设计模式,也可能不适用于特定问题。如果大锤最不适合用来破坏螺母,螺丝刀也不是螺丝刀,卷尺也不是因为错过了锤子而没有卷尺,那并不是大锤是正确的选择,它只是使它是您最合适的工具。但这并不意味着您应该使用大锤破碎螺母。哎呀,如果我们坦率地说,您真正需要的是胡桃夹子,而不是锤子
尼尔(Neil)

我想要一支训练有素的松鼠大军来破解我的坚果。
icc97

6

TL:DR;

我认为没有“必要”数量的抽象级别,低于该级别则太少或高于该级别而又太高。与图形设计一样,好的OOP设计应该是不可见的,并且应该被认为是理所当然的。糟糕的设计总是像拇指酸痛一样伸出来。

长答案

很可能您永远不会知道您要构建多少个抽象级别。

大多数抽象层次对于我们都是不可见的,我们将其视为理所当然。

这种推理使我得出以下结论:

抽象的主要目的之一是使程序员不必时时刻刻都要考虑系统的所有工作。如果设计迫使您对系统了解太多以增加某些内容,那么抽象可能就太少了。我认为不良的抽象(不良的设计,贫乏的设计或过度的工程设计)也可能迫使您了解太多,以添加一些东西。在一个极端中,我们有一个基于上帝类或一堆DTO的设计,在另一个极端中,我们有一些OR /持久性框架,这些框架使您可以跳过无数的篮球,从而建立一个问候世界。两种情况都迫使您太了解了。

错误的抽象坚持高斯的钟声,因为一旦您越过了最佳位置,就会开始妨碍您。另一方面,好的抽象是不可见的,并且不能太多,因为您不会注意到它的存在。考虑一下API,网络协议,库,OS库,文件系统,硬件层等的层数。您的应用程序是建立并理所当然的。

抽象的另一个主要目的是分隔,因此错误不会扩散到特定区域之外,这与双壳体不同,分开的油箱可以防止船体的一部分有孔时完全淹没船舶。如果对代码的修改最终在看似无关的区域中创建了bug,那么抽象的机会就太少了。


2
“很可能您永远不会知道您要构建多少个抽象级别。” –实际上,抽象的全部要点是您不知道它是如何实现的,IOW您不(也不能)知道它隐藏了多少个抽象级别。
约尔格W¯¯米塔格

4

设计模式只是解决问题的常用方法。知道设计模式很重要,但是它们只是设计良好的代码的症状(好的代码仍然可以摆脱四个设计模式的组合),而不是原因。

抽象就像篱笆。它们帮助将程序的各个区域分成可测试和可互换的块(制作非脆弱非刚性代码的要求)。就像栅栏一样:

  • 您希望在自然接口点进行抽象以最小化它们的大小。

  • 您不想更改它们。

  • 您希望他们将可以独立的事物分开。

  • 一个人在错误的地方比没有一个人更糟糕。

  • 他们不应有大的泄漏


4

重构

到目前为止,我都没有提到“重构”一词。所以,我们开始:

随意尽可能直接地实施新功能。如果只有一个简单的类,则可能不需要接口,超类,工厂等。

如果并且当您发现自己以太胖的方式扩展课程时,就该将其撕裂了。当时它使大感想想如何你其实应该这样做。

模式是一种思维工具

模式,或者更确切地说是由四人组成的书《设计模式》一书,是很棒的,原因之一是,它们为开发人员提供了一种思考和交谈的语言。很容易说“观察者”,“工厂”或“工厂”。 “门面”每个人都知道究竟意味着什么,马上。

因此,我的观点是,每个开发人员至少应该对原始书中的模式有足够的了解,仅仅是为了能够谈论OO概念而不必总是解释基础知识。您是否应该在每次出现可能性时实际使用这些模式?很有可能不会。

图书馆

图书馆可能是一个错误的领域,它可能使基于模式的选择太多而不是太多。将某些内容从“胖”类更改为更多来自模式的类(通常意味着更多和更小的类)会从根本上改变界面;那是您通常不想在库中进行更改的一件事,因为这是库用户唯一真正感兴趣的事情。他们不会在乎您如何在内部处理功能,但是当您使用新的API发行新版本时,如果他们经常不得不更改程序,他们会非常在意。


2

抽象的重点应该首先是为抽象的使用者(即抽象的客户,其他程序员,通常是您自己)带来的价值。

如果作为使用抽象的客户,您发现需要混合并匹配许多不同的抽象以完成编程工作,那么潜在的抽象太多。

理想情况下,分层应该将许多较低的抽象集合在一起,并用其消费者可以使用的简单和更高级别的抽象代替,而不必处理任何这些基础抽象。如果他们必须处理基础抽象,则该层正在泄漏(由于不完整)。如果消费者必须处理太多不同的抽象,则可能缺少分层。

在考虑了抽象对于消费程序员的价值之后,我们可以转向评估和考虑实现,例如DRY-ness的实现。

是的,这一切都是为了简化维护,但是我们应该首先通过提供质量抽象和层次来考虑消费者维护的困境,然后再考虑在实现方面(如避免冗余)简化我们自己的维护。


例如,当使用Memcache编写通用缓存层时。我们真的需要Memcache,MemcacheAdapter,MemcacheInterface,AbstractCache,CacheFactory,CacheConnector等吗?还是仅使用其中一半类,这是否更易于维护并且仍然是良好的代码?

我们必须审视客户的观点,如果他们的生活变得更轻松,那就太好了。如果他们的生活更加复杂,那就不好了。但是,可能是缺少一个将这些内容包装到易于使用的东西中的层。在内部,这些可能很好地使实现的维护更好。但是,正如您所怀疑的,它也可能只是过度设计而已。


2

抽象旨在使代码更易于理解。如果抽象层会使事情更加混乱-那就不要这样做。

目的是使用正确数量的抽象和接口来:

  • 缩短开发时间
  • 最大化代码可维护性

仅在需要时摘要

  1. 当您发现自己正在编写超类时
  2. 什么时候可以重用代码
  3. 如果抽象将使代码将成为显著更清晰,更易于阅读

何时不抽象

  1. 这样做将不会在代码重用或清晰度方面带来优势
  2. 这样做会使代码更长/更复杂,没有任何好处。

一些例子

  • 如果您整个程序只打算拥有一个缓存,请不要抽象,除非您认为您可能最终会获得超类
  • 如果您具有三种不同类型的缓冲区,则对所有它们使用通用的接口抽象

2

我认为这可能是一个有争议的meta答案,我参加聚会还有些晚,但是我认为在这里提及这一点非常重要,因为我想我知道您来自哪里。

设计模式的使用方式存在一个问题,即在教授设计模式时,它们会呈现如下情况:

您有此特定方案。这样组织代码。这是一个聪明的例子,但有些人为的例子。

问题在于,当您开始进行实际工程设计时,事情并非一帆风顺。您阅读的设计模式不太适合您要解决的问题。更不用说您正在使用的库完全违反了文本中解释这些模式的所有规定,每种方式都有其自己的特殊方式。结果,您编写的代码“感觉不对劲”,并提出了类似的问题。

除此之外,在谈论软件工程时,我想引用Andrei Alexandrescu的话,他指出:

软件工程可能比其他任何工程学科都具有更多的多样性:您可以通过许多正确的方式来做同样的事情,对与错之间有无限的细微差别。

也许这有点夸张,但是我认为这完全解释了您可能对代码不太自信的另一个原因。

在这样的时刻,Insomniac游戏引擎负责人Mike Acton的预言在我脑海中尖叫:

了解您的数据

他正在谈论程序的输入以及所需的输出。然后是《神话人月》中的弗雷德·布鲁克斯(Fred Brooks)瑰宝:

告诉我您的流程图并隐藏您的表格,我将继续感到困惑。给我看你的表,我通常不需要你的流程图。他们会很明显。

因此,如果您是我,我将根据我的典型输入案例以及是否达到所需的正确输出来对我的问题进行推理。并提出如下问题:

  • 程序输出的数据正确吗?
  • 对于我最常见的输入案例,它能否高效/快速地生成?
  • 我的代码是否对我和我的队友都足够容易在本地进行推理?如果没有,那我可以简化一下吗?

当您这样做时,“需要多少层抽象或设计模式”的问题就容易回答了。您需要多少个抽象层?实现这些目标所需的数量之多,而没有更多。“设计模式呢?我什么都没用!” 好吧,如果上述目标是在没有直接应用模式的情况下实现的,那很好。使它工作,然后继续下一个问题。从您的数据开始,而不是从代码开始。


2

软件架构正在发明语言

在每个软件层中,您都可以创建您(或您的同事)想要表达其下一层解决方案的语言(因此,在我的文章中,我将介绍一些自然语言的类似物)。您的用户不想花几年的时间学习如何阅读或编写该语言。

在决定体系结构问题时,这种观点对我有帮助。

可读性

该语言应易于理解(使下层代码可读)。读取代码的频率远高于写入代码。

一个概念应该用一个词来表达-一个类或接口应该公开这个概念。(斯拉夫语的一个英语动词通常有两个不同的单词,因此您必须学习两次词汇。所有自然语言都将单个单词用于多个概念)。

您公开的概念不应包含任何惊奇。这主要是诸如get-和set-method等的命名约定。设计模式可以提供帮助,因为它们提供了标准的解决方案模式,读者会看到“好,我从工厂得到对象”,并且知道这意味着什么。但是,如果仅实例化一个具体的类就可以完成工作,那么我更愿意这样做。

易用性

该语言应易于使用(易于编写“正确的句子”)。

如果所有这些MemCache类/接口对下一层都是可见的,这将为用户创造陡峭的学习曲线,直到用户了解何时以及在何处将这些单词中的哪一个用于缓存的单个概念为止。

仅公开必要的类/方法可使您的用户更轻松地找到他所需要的内容(请参阅DocBrowns对Antoine de Saint-Exupery的引用)。公开一个接口而不是实现类可以使之更容易。

如果您公开了可以应用既定设计模式的功能,则遵循该设计模式总比发明不同的功能更好。与某些完全不同的概念相比,您的用户将更容易理解遵循设计模式的API(如果您知道意大利语,那么西班牙语会比汉语更容易理解)。

摘要

如果可以简化用法(并且值得同时维护抽象和实现的开销),请引入抽象。

如果您的代码有一个(非平凡的)子任务,请“按预期方式”解决,即遵循适当的设计模式,而不是重新发明其他类型的轮子。


1

要考虑的重要事项是,实际上处理您的业务逻辑的使用代码需要多少了解有关这些与缓存相关的类。理想情况下,如果构造函数方法不够用,您的代码应该只关心要创建的缓存对象,也许只关心工厂来创建该对象。

使用的模式数量或继承级别不是很重要,只要每个级别可以证明给其他开发人员即可。由于每个附加级别都难以证明其合理性,因此创建了一个非正式的限制。更为重要的部分是功能或业务需求的更改会影响多少抽象级别。如果您只能将一个需求的一个级别更改为一个级别,那么您可能不会被过度抽象或抽象得很差,如果您为多个不相关的更改更改了同一级别,则您可能会被抽象,并且需要进一步分离关注点。


-1

首先,Twitter报价是虚假的。新开发人员需要建立模型,抽象通常可以帮助他们“了解情况”。提供的抽象当然是有道理的。

其次,您的问题不是太多或太少的抽象,显然是没有人可以决定这些事情。没有人拥有代码,没有实施任何单一的计划/设计/哲学,任何下一个人都可以做那一刻他认为合适的事情。无论您采用哪种风格,都应该是一种。


2
让我们不要以“伪造”来剥夺经验。太多的抽象是一个实际的问题。可悲的是,人们会在最前面添加抽象,因为这是“最佳实践”,而不是解决实际问题。而且,没有人可以决定“关于这些事情”……人们离开公司,人们加入,没有人拥有自己的所有权。
Rawkode
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.