SOLID与避免过早抽象


27

我了解SOLID在模块化很重要且其目标显然有用的情况下应定期完成并使用的功能。但是,有两点使我无法在代码库中一致地应用它:

  • 我想避免过早的抽象。以我的经验,绘制没有具体用例(现在或可预见的将来)的抽象线会导致将它们绘制在错误的位置。当我尝试修改此类代码时,抽象行会妨碍您的工作,而不是有所帮助。因此,我倾向于不画任何抽象线,直到我对它们的用处有了一个很好的认识。

  • 我发现很难证明增加模块化本身是合理的,如果它使我的代码更冗长,更难理解等,并且不能消除任何重复。我发现有时,简单,紧密耦合的过程或God对象代码比构造良好的馄饨代码更易于理解,因为流程简单且线性。编写起来也容易得多。

另一方面,这种心态常常导致神的对象。我通常会保守地重构它们,仅在看到清晰的模式出现时才添加清晰的抽象线条。如果您显然不需要更多的模块化,没有大量重复且代码可读性强,那么上帝对象和紧密耦合的代码怎么办?

编辑:就个别SOLID原则而言,我想强调的是,Liskov Substitution是IMHO的常识形式化,应在所有地方应用,因为如果不是这样的话,抽象是没有意义的。同样,每个类都应该在某个抽象级别上担负单个责任,尽管这可能是一个很高的级别,将实现细节都塞进一个庞大的2,000行类中。基本上,您的抽象应该在您选择抽象的地方有意义。在模块化不是很有用的情况下,我所质疑的原则是开放式,接口隔离,尤其是依赖关系倒置,因为这些都是关于模块化的,而不仅仅是让抽象有意义。


10
@ S.Lott:这里的关键词是“更多”。这正是YAGNI发明的目的。仅仅出于自身的原因,增加模块化或减少耦合仅仅是“花哨的编程”。
梅森惠勒

3
@ S.Lott:从上下文中应该显而易见。在我阅读本书时,至少“所讨论的代码中存在的数量超过当前数量”。
梅森惠勒

3
@ S.Lott:如果您坚持要学究,“更多”指的是当前存在的更多。这意味着已经存在某种东西,无论是代码还是粗糙的设计,您都在考虑如何重构它。
dsimcha 2011年

5
@ back2dos:是的,但是我对为可扩展性设计某些东西持怀疑态度,但没有清楚如何扩展的想法。没有这些信息,您的抽象行可能会出现在所有错误的地方。如果您不知道抽象线可能在哪里有用,我建议您即使在其中插入了一些上帝对象,也应该编写出最有效且易读的最简洁的代码。
dsimcha 2011年

2
@dsimcha:如果编写一段代码需求有所变化您很幸运,因为通常您执行过程中需求会发生变化。这就是为什么快照实现不适合在实际软件生命周期中生存的原因。而且,SOLID不需要您只是盲目地进行抽象。通过依赖关系反转定义抽象上下文。您可以将每个模块的依赖性降低到它所依赖的其他服务的最简单明了的抽象。它不是任意的,人为的或复杂的,而是定义明确,合理和简单的。
back2dos

Answers:


8

以下是可以应用的简单原理,以帮助您了解如何平衡系统设计:

  • 单一责任原则:S在SOLID中。就方法数量或数据量而言,您可以拥有一个非常大的对象,并且仍然坚持这一原则。以Ruby String对象为例。该对象拥有的方法超出了您的承受范围,但它仍然只有一个责任:为应用程序保留一串文本。一旦您的对象开始承担新的责任,就开始认真思考。维护问题是在问自己“如果以后遇到问题,我希望在哪里找到此代码?”
  • 简单很重要:爱因斯坦(Albert Einstein)说:“让一切尽可能简单,但不要简单。” 您真的需要这种新方法吗?您可以利用现有功能完成所需的工作吗?如果您确实认为需要新方法,是否可以更改现有方法来满足您的所有需求?本质上,您在构建新东西时会进行重构。

本质上,您在尝试维护软件时要尽其所能,以免摔伤自己。如果您的大型对象是合理的抽象,则没有理由将它们分割开来,仅仅是因为有人提出了一个度量标准,即一个类不应超过X行/方法/属性/等。如果有的话,那是指导方针,而不是一成不变的规则。


3
好评论。正如在其他答案中讨论的那样,“单一职责”的一个问题是可以在各种抽象级别上描述类的职责。
dsimcha 2011年

5

我认为您大部分已经回答了自己的问题。当需要将概念提升到抽象级别时,SOLID提供了一组有关如何重构代码的准则。

像所有其他创造性学科一样,没有绝对的取舍。您做得越多,就越容易确定何时才足以满足您当前的问题领域或需求。

话虽这么说-抽象是软件开发的核心-因此,如果没有充分的理由不这样做,那么实践将使您变得更好,并且您会有所取舍。因此,除非它变得有害,否则请青睐它。


5
I want to avoid premature abstraction.In my experience drawing abstraction lines without concrete use cases... 

这是部分正确和部分错误。

错误
这不是阻止人们应用OO原则/ SOLID的原因。这只能防止过早应用它们

那是...

正确
不要重构代码,直到重构为止。当要求完成时。或者,当您所说的“用例”都在那里时。

I find simple, tightly coupled procedural or God object code is sometimes easier to understand than very well-factored...

当单独工作或在筒仓中工作时,
不立即发现OO的问题并不明显。编写程序的第一个版本后:

Code is fresh in your head
Code is familiar
Code is easy to mentally compartmentalize
You wrote it

这些善事中的3/4会在短时间内迅速消失。

最后,您认识到您拥有上帝的对象(具有许多功能的对象),因此您会认识到各个功能。封装。将它们放在文件夹中。偶尔使用接口。请帮您长期维护。特别是由于非重构方法趋于无限膨胀并成为上帝方法。

在团队中
不执行OO的问题将立即引起注意。好的OO代码至少在某种程度上可以自我记录和可读。特别是结合良好的绿色代码时。而且,缺乏结构使得难以分离任务并使集成更加复杂。


对。我模糊地认识到我的对象不只是一件事,但是我没有更具体认识到如何有效地将它们分开,以便在需要维护时将抽象线放在正确的位置。如果维护程序员可能不得不做更多的重构以将抽象行正确地放置在正确的地方,那么重构似乎是浪费时间。
dsimcha 2011年

封装和抽象是两个不同的概念。封装是一个基本的OO概念,基本上意味着您拥有类。类本身提供模块化和可读性。那很有用。
P.Brian.Mackey 2011年

正确应用抽象可以减少维护。使用不当会增加维护。知道何时何地划界线需要对业务,框架,客户有深刻的了解,除了基本的技术方面外,还有如何“应用工厂模式”本身的基本技术方面。
P.Brian.Mackey 2011年

3

大型对象和紧密耦合的代码在适当的情况下且不需要任何经过精心设计的代码时没有任何问题。这只是经验法则变成教条的另一种表现

在许多常见情况下,松散耦合和小的简单对象往往会提供特定的好处,因此通常使用它们是一个好主意。问题出在那些不了解原则背后原理的人,他们盲目地试图普遍应用这些原则,即使在不适用的地方也是如此。


1
+1。仍然需要对“适当”的含义有一个很好的定义。
jprete 2011年

2
@jprete:为什么?试图应用绝对定义是这种概念混乱的部分原因。构建好的软件涉及很多工艺。IMO真正需要的是经验和良好的判断力。
梅森惠勒

4
是的,但是对某种形式的广泛定义仍然有用。
jprete 2011年

1
定义适当的定义将很有用,但这就是重点。很难确定声音的咬合度,因为它仅适用于个案。在每种情况下您是否做出适当选择,很大程度上取决于经验。如果您无法阐明为什么具有高内聚力,那么整体解决方案比低内聚力,高度模块化的解决方案要好,那么“低聚”和模块化将“可能”更好。重构总是比较容易的。
伊恩

1
我希望有人盲目地编写解耦代码,而不是盲目地编写意大利面条式代码:)
Boris Yankov

2

我倾向于从“您将不需要它”的角度来解决这个问题。这篇文章将特别关注一个项目:继承。

在我最初的设计中,我创建了一个层次结构,我知道将有两个或多个层次结构。他们可能需要很多相同的代码,因此值得一开始就进行设计。在初始代码到位之后,我需要添加更多功能/特性,然后查看一下我所拥有的,并认为:“我已经实现了相同或相似的功能吗?” 如果是这样,那很可能是一个新的乞讨被发布。

MVC框架就是一个很好的例子。从头开始,您将创建一个大页面,后面有一个代码。但是然后您想添加另一个页面。但是,后面的一个代码已经实现了新页面所需的许多逻辑。因此,您将抽象出一个Controller类/接口,该类/接口将实现特定于该页面的逻辑而将常见的东西留在原始的“神”代码中。


1

如果您作为开发人员知道由于代码的未来而何时不应用这些原则,那么对您有好处。

我希望SOLID仍然在您的脑海中,知道何时确实需要抽象一些东西,以避免复杂性并提高代码质量(如果它开始降低您声明的值)。

不过,更重要的是,请考虑大多数开发人员都在做日常工作,而不是像您一样不在乎。如果您最初设置了长时间运行的方法,那么代码中的其他开发人员在维护或扩展它时会考虑什么呢?上帝的对象,他们没有记住捕捉不断增长的代码并正确封装它的原则。您“可读”的代码现在变得一团糟。

因此,您可能会争辩说,一个糟糕的开发人员无论如何都会这样做,但如果从一开始就保持简单和井井有条,至少您会拥有更好的优势。

如果它们很简单,我并不特别介意全局的“注册表”或“上帝对象”。它实际上取决于系统设计,有时您可以摆脱它并保持简单,有时则不能。

我们也不要忘记大型函数和God Objects很难测试。如果您想保持敏捷并在重构和更改代码时感到“安全”,则需要进行测试。当函数或对象执行许多操作时,很难编写测试。


1
基本上是一个很好的答案。我的一个抱怨是,如果某个维护开发人员出现并弄得一团糟,那是他/她不进行重构的错,除非这种改变在预见性上似乎足以证明他们的合理性,或者代码记录/测试/如此差等。 。重构本来是一种精神错乱的形式。
dsimcha 2011年

真的是dsimcha。只是我以为在一个系统中,开发人员创建了一个文件夹“ RequestHandlers”,该文件夹下的每个类都是一个请求处理程序,随后出现的下一个开发人员认为,“我需要放置一个新的请求处理程序”,当前的基础设施几乎迫使他遵循公约。我想这就是我要说的。从一开始就建立一个明显的约定,甚至可以指导最没有经验的开发人员继续该模式。
Martin Blore

1

神物的问题在于您通常可以从中获利分成几块。根据定义,它做的不只一件事。将其分为较小的类的全部目的是使您可以拥有一个能够完成一件事的类(并且您也不应该扩展“一件事”的定义)。这意味着,对于任何给定的类,一旦知道了它应该做的一件事,就应该能够相当容易地阅读它,并说出它是否在正确地完成那件事。

我认为这样的东西有太多的模块,和太多的灵活性,但是这更多的过度设计和过度工程问题,在哪里你是会计的要求,你甚至不知道客户需要。许多设计思想都是围绕着使控制代码运行方式的更改变得容易而进行的,但是如果没有人期望更改,那么包含灵活性就毫无意义。


很难定义“一件事”,具体取决于您所使用的抽象级别。可以争论的是,任何具有两行或更多行代码的方法都做不止一件事情。另一方面,诸如脚本引擎之类的复杂事物将需要大量方法,这些方法都可以处理彼此之间没有直接关系的单个“事物”,但是每个构成“元-事情”,这可以使您的脚本正常运行,并且在不破坏脚本引擎的情况下,可以做很多事情。
梅森惠勒

肯定有一系列概念上的层次,但是我们可以选择一个要点:“做一件事的类”的大小应使您几乎可以立即理解实现。也就是说,实施细节适合您的需求。如果变得更大,那么您将无法一次了解整个课程。如果它更小,则说明您抽象得太远了。但是正如您在另一条评论中所述,其中涉及很多判断和经验。
jprete 2011年

恕我直言,最好的抽象层次是您希望对象的使用者使用的层次。假设您有一个名为的对象Queryer,可以优化数据库查询,查询RDBMS并将结果解析为对象以返回。如果在您的应用程序中只有一种方法可以做到这一点,并且Queryer被密封并封装了这种方法,那么它只会做一件事。如果有多种方法可以做到这一点,并且有人可能会关心其中一部分的细节,那么它会做很多事情,您可能希望将其拆分。
dsimcha 2011年

1

我使用一个更简单的准则:如果您可以为其编写单元测试,并且没有代码重复,那么它就足够抽象了。另外,您在以后的重构中处于有利位置。

除此之外,您应该牢记SOLID,但更多的是作为指导而非规则。


0

如果您显然不需要更多的模块化,没有大量重复且代码可读性强,那么上帝对象和紧密耦合的代码怎么办?

如果应用程序足够小,则任何内容都是可以维护的。在更大的应用中,上帝的物体很快成为一个问题。最终实现一个新功能需要修改17个God对象。按照17个步骤进行操作的人做得不好。上帝的对象不断地被多个开发人员修改,这些更改必须重复合并。你不想去那里。


0

我同意您对过多和不适当的抽象的关注,但不必一定担心过早的抽象。

这听起来像是一个矛盾,但是只要您不过早地使用它(即只要您愿意并且能够在必要时进行重构),那么使用抽象就不太可能引起问题。

这意味着在代码中进行某种程度的原型设计,试验和回溯。

也就是说,没有简单的确定性规则可以解决所有问题。经验很重要,但是您必须通过犯错来获得经验。而且总是会有更多的错误使以前的错误不会为您做好准备。

即使这样,也应将教科书的原则作为起点,然后学习编程知识并了解这些原则的工作原理。如果可以提供比SOLID之类的更好,更精确和可靠的指南,那么现在有人可能会这样做-即使有,这些改进的原理仍将是不完善的,人们会问这些规范的局限性。

也是一项好工作-如果任何人都可以提供清晰的确定性算法来设计和编码程序,那么只有最后一个程序可供人类编写-该程序可以自动编写所有将来的程序而无需人工干预。


0

绘制适当的抽象线条是从经验中汲取的东西。这样做会犯错误,这是不可否认的。

SOLID是那种经验的集中形式,这些经验是由很难获得经验的人交给您的。当您说在看到需要抽象之前就不要创建抽象时,您几乎钉住了它。SOLID提供的经验将帮助您解决从未出现过的问题。但是请相信我...这是非常真实的。

SOLID与模块化有关,模块化与可维护性有关,可维护性是指一旦软件进入生产阶段并且客户端开始注意到您所没有的错误,您就可以保持人性化的工作计划。

模块化的最大好处是可测试性。系统的模块化程度越高,测试就越容易,并且可以更快地建立扎实的基础来解决问题的更难方面。因此,您的问题可能并不需要这种模块化,而事实是它可以使您更快地创建更好的测试代码,这是不费吹灰之力争取模块化的事实。

敏捷就是在快速运输和提供优质产品之间寻求微妙的平衡。敏捷并不意味着我们应该走捷径,实际上,我参与过的最成功的敏捷项目就是那些密切关注此类准则的项目。


0

似乎已被忽略的重要一点是SOLID与TDD一起使用。TDD倾向于强调在您的特定情况下什么是“适当的”,并有助于减轻建议中似乎存在的许多歧义。


1
请考虑扩大您的答案。就目前而言,您正在将两个正交的概念放在一起,目前尚不清楚结果如何解决OP的问题。
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.