遵循SOLID是否会导致在技术堆栈之上编写框架?


70

我喜欢SOLID,并且在开发时会尽力使用和应用它。但是我不禁感到SOLID方法似乎将您的代码变成了“框架”代码-即,如果您要创建供其他开发人员使用的框架或库,则可以设计代码。

我通常练习两种编程模式-或多或少地准确创建通过需求和KISS(典型编程)提出的要求,或者创建非常通用且可重用的逻辑,服务等,从而提供其他开发人员可能需要的灵活性(框架编程) 。

如果用户确实只希望应用程序执行x和y事情,那么当您甚至都不知道这是否是一个有效的问题时,遵循SOLID并添加一堆抽象入口点是否有意义?与?如果确实添加了这些抽象的入口点,是您真的满足了用户的要求,还是在现有框架和技术堆栈之上创建了一个框架,以使将来的添加变得更加容易?在哪种情况下,您是在为客户或开发人员的利益服务?

这在Java Enterprise世界中似乎很常见,感觉好像您是在J2EE或Spring之上设计自己的框架,以便它对开发人员来说是更好的UX,而不是针对用户的UX?


12
大多数短的编程经验法则的问题是,它们会受到解释,边缘情况的影响,有时在仔细检查时尚不清楚此类规则中的单词定义。对于不同的人,它们本质上可能意味着各种各样的事情。拥有一些非意识形态的实用主义通常可以使人们做出更明智的决定。
马克·罗杰斯

1
您听起来像遵循SOLID原则似的意味着一笔巨额投资,需要大量额外工作。它不是,它实际上是免费的。而且它可能会在将来为您或其他人节省大量投资,因为它使您的代码更易于维护和扩展。您继续问“我们应该做家庭作业还是让客户满意?”之类的问题。这些不是权衡取舍。
马丁·马特

1
@MartinMaat我认为SOLID的极端形式确实意味着巨大的投资。就是 企业软件。在企业软件之外,您几乎没有理由抽象您的ORM,技术堆栈或数据库,因为您很有可能坚持使用所选的堆栈。从同样的意义上讲,通过将自己绑定到特定的框架,数据库或ORM,您将违反SOLID原则,因为您已耦合到堆栈。大多数作业都不需要SOLID的这种灵活性。
Igneous01

1
另请参阅内部平台效应
Maxpm

1
将大多数代码变成类似框架的声音听起来并不可怕。如果过度设计,它只会变得很糟糕。但是框架可以是最小的和有根据的。我不确定这是否会成为遵循SOLID 的不可避免的结果,但这绝对是可能的结果,我认为您应该接受。
康拉德·鲁道夫

Answers:


84

您的观察是正确的,恕我直言,SOLID原则是在考虑可重用的库或框架代码的情况下完成的。如果您只是盲目地关注所有这些信息,而又不问是否有意义,那么您就有冒着过于笼统的风险,向系统中投入了很多不必要的精力。

这是一个折衷,需要一些经验来做出有关何时何时进行概括的正确决策。一种可能的方法是坚持YAGNI原则-不要使您的代码成为SOLID以防万一-或者,或者用您的话:不要

提供其他开发人员可能需要的灵活性

相反,应尽快提供其他开发人员实际需要的灵活性,但不能更早。

因此,每当代码中有一个函数或类时,就不确定是否可以重用它,请不要立即将其放入框架中。请等待,直到有实际的情况可以重用,然后重构为“对于该情况足够坚固”。不要实现更多的可配置性(遵循OCP),也不要实现抽象的入口点(使用DIP)到实际重用情况真正需要的类中。当实际上存在下一个重用需求时,请增加下一个灵活性。

当然,这种工作方式将始终需要在现有的有效代码库中进行一些重构。这就是为什么自动测试在这里很重要的原因。因此,从一开始就使代码SOLID足够正确以使其可以进行单元测试不是浪费时间,并且这样做与YAGNI并不矛盾。自动测试是“代码重用”的有效案例,因为所使用的代码既可以用于生产代码,也可以用于测试。但是请记住,只要增加使测试正常运行所需的灵活性,就可以了。

这实际上是古老的智慧。很久以前,SOLID一词流行之前,有人告诉我在尝试编写可重复使用的代码之前,我们应该先编写可用的代码。我仍然认为这是一个很好的建议。


23
额外的争论点:等到3个用例看到相同的逻辑后,再重构代码以供重用。如果您从2个部分开始重构,那么很容易遇到更改需求或新用例破坏了您所做的抽象的情况。还要将重构限制在具有相同用例的事物上:2个组件可能具有相同的代码,但是做的事情完全不同,并且,如果合并这些组件,最终将链接该逻辑,这可能会在以后产生问题。
Nzall

8
我通常都同意这一点,但是感觉它太关注“一次性”应用程序:您编写代码,就可以了,很好。但是,很多应用程序都具有“长期支持”。您可能编写了一些代码,两年后,业务需求发生了变化,因此您必须调整代码。到那时,很多其他代码可能都依赖它-在这种情况下,SOLID原理将使更改变得更容易。
R. Schmitz

3
“在尝试编写可重用的代码之前,我们应该先编写可用的代码”-非常明智!
格雷厄姆

10
值得注意的是,等到有了实际用例后,您的SOLID代码才能变得更好,因为在假设条件下工作非常困难,而且您可能会误认为未来的需求。我们的项目在许多情况下都将事物设计为可满足未来需求的SOLID和灵活性...除了事实证明,未来的需求是当时没人想到的事物,因此我们既需要重构需要额外的灵活性,仍然不需要-要么在重构时必须维护,要么废弃。
KRyan

2
通常,您仍然需要编写可测试的代码,这通常意味着要具有第一层抽象,以便能够从具体实现转换为测试实现。
Walfrat

49

根据我的经验,编写应用程序时,您有三个选择:

  1. 仅仅为了满足要求而编写代码,
  2. 编写可预测未来需求并满足当前需求的通用代码,
  3. 编写仅满足当前要求的代码,但是以后可以轻松更改以满足其他需求的方式。

在第一种情况下,通常会出现缺少单元测试的紧密耦合代码。当然,编写起来很快,但是很难测试。当需求改变时,改变是正确的皇家痛苦。

在第二种情况下,要花费大量时间来预期将来的需求。而且,这些预期的未来需求经常都无法实现。这似乎是您正在描述的场景。在大多数情况下,这是浪费时间,并且会导致不必要的复杂代码,而当未预料到的需求出现时,仍然很难更改。

我认为最后一种情况是针对的。使用TDD或类似的技术在进行代码测试时,最终会得到松散耦合的代码,该代码易于修改,但编写起来仍然很快。事情就是这样,您自然会遵循许多SOLID原则:小型类和函数;接口和注入的依赖项。利斯科夫夫人通常也很高兴,因为只负责单一职责的简单班级很少受到她的替代原则的侵犯。

SOLID唯一在这里没有实际应用的方面是开放/封闭原则。对于库和框架,这很重要。对于一个自包含的应用程序,不需要那么多。确实是在编写遵循“ SLID ”的代码的情况:易于编写(和读取),易于测试且易于维护。


这个网站上我最喜欢的答案之一!
TheCatWhisperer

我不确定您如何得出结论:1)比3)更难测试。当然,要进行更改更加困难,但是为什么不能测试?如果有的话,一心一意的软件比更通用的软件更容易针对需求进行测试。
李斯特先生,

@MrLister这两个问题并存,1.比3.更难测试,因为定义隐含了它不是“以后很容易改变以满足其他需求的方式”编写的。
Mark Booth,

1
+0; IMVHO您正在误解(尽管是一种通用方式)“ O”(开-关)的工作方式。参见例如codeblog.jonskeet.uk/2013/03/15/…-即使在小型代码库中,也更多地涉及拥有独立的代码单元(例如类,模块,包等),可以对其进行隔离测试并添加/根据需要删除。这样的示例就是一包实用程序方法-不管捆绑它们的方式如何,它们都应该是“封闭的”(即自包含的)和“开放的”(即以某种方式可扩展的)。
vaxquis

顺便说一句,即使鲍勃叔叔也有这样的想法:“ [开-闭]的意思是,您应该努力使自己的代码处于适当的位置,这样,当行为以预期的方式改变时,您不必费心更改系统的所有模块。理想情况下,您将能够通过添加新代码并几乎不更改旧代码或不更改旧代码来添加新行为。” <-这显然适用于小型应用程序,如果要对其进行修改或修复(并且IMVHO这就是通常的情况下,ESP有关修复时。轻笑
vaxquis

8

您的观点可能会因个人经验而有所偏差。尽管事实乍一看像是正确的,但这是事实的滑坡,但事实并非如此。

  • 框架的范围要大于小型项目。
  • 在较大的代码库中,不好的做法很难解决。
  • 构建框架(平均而言)需要比构建小型项目更熟练的开发人员。
  • 更好的开发人员会更多地遵循良好实践(SOLID)。
  • 结果,框架对良好实践的需求更高,并且往往是由对良好实践经验更为丰富的开发人员构建的。

这意味着,当您与框架和较小的库进行交互时,与您进行交互的良好实践代码通常会在较大的框架中找到。

这种谬论是很普遍的,例如我所接受治疗的每位医生都是自大的。因此,我得出结论,所有医生都是自大的。这些谬论总是遭受基于个人经验的笼统推断。

在您的情况下,您可能主要是在较大的框架中而不是在较小的库中经历了良好的实践。您的个人观察并没有错,但这只是传闻,并非普遍适用。


2种编程模式-或多或少地准确创建通过需求和KISS(典型编程)提出的要求,或者创建非常通用且可重用的逻辑,服务等,从而提供其他开发人员可能需要的灵活性(框架编程)

您在这里对此有所确认。考虑一下什么是框架。它不是一个应用程序。这是一个通用的“模板”,其他人可以用来制作各种应用程序。从逻辑上讲,这意味着框架是用更加抽象的逻辑构建的,以便每个人都可以使用。

框架构建器无法采用快捷方式,因为它们甚至不知道后续应用程序的要求。建立一个内在的框架会激励他们使自己的代码对他人可用。

但是,由于应用程序构建者专注于交付产品,因此它们有能力牺牲逻辑效率。他们的主要目标不是代码的工作原理,而是用户的经验。

对于框架,最终用户是另一个开发人员,他将与您的代码进行交互。代码的质量对最终用户很重要。
对于应用程序,最终用户是非开发人员,不会与您的代码进行交互。您的代码质量对他们并不重要。

这正是开发团队的架构师经常充当良好实践的执行者的原因。它们是交付产品的第一步,这意味着它们倾向于客观地查看代码,而不是专注于应用程序本身的交付。


如果确实添加了这些抽象的入口点,是您真的满足了用户的要求,还是在现有框架和技术堆栈之上创建了一个框架,以使将来的添加变得更加容易?在哪种情况下,您是在为客户或开发人员的利益服务?

这是一个有趣的观点,并且(根据我的经验)这是人们仍然试图为避免良好实践辩护的主要原因。

归纳以下几点:仅当您的要求(如目前所知)是不可变的,并且没有对代码库进行任何更改/添加时,才可以跳过良好实践。 剧透警报:很少有这种情况。
例如,当我编写一个5分钟的控制台应用程序来处理特定文件时,我没有采用好的做法。因为我将仅在今天使用该应用程序,并且将来不需要进行更新(如果我需要一个应用程序,编写另一个应用程序会更容易)。

假设您可以在4周内伪造一个应用程序,并且可以在6周内正确构建一个应用程序。乍一看,伪造的建筑似乎更好。客户可以更快地获得他们的应用程序,并且公司必须花费更少的时间来支付开发人员的工资。双赢吧?

但是,这是没有事先考虑就做出的决定。由于代码库的质量,对伪造的代码进行重大更改将需要2周,而对正确构建的代码进行相同更改则需要1周。将来可能会有许多这样的变化。

此外,有一种变化的趋势是,意外地需要比最初在简陋的代码库中想象的更多的工作,因此可能会将您的开发时间从两周缩短到三周。

然后还有浪费时间寻找错误的趋势。在由于时间限制或完全不愿执行日志记录而忽略日志记录的项目中,这是经常发生的情况,因为您会以最终产品将按预期工作的假设为前提,而无心工作。

它甚至不需要进行重大更新。在我现在的老板那里,我看到了几个快速而又肮脏的项目,当由于需求沟通不畅而需要进行最小的错误/更改时,导致需要在每个模块之间进行重构的连锁反应。其中一些项目甚至在发布第一个版本之前就崩溃了(并留下了无法维护的混乱)。

快捷的决策(快速而肮脏的编程)只有在您可以最终保证要求完全正确且永远不需要更改时才有用。以我的经验,我从未遇到过一个真实的项目。

在良好实践中投入额外的时间就是对未来的投资。当现有代码库建立在良好实践的基础上时,将来的错误和更改将变得更加容易。仅进行两次或三个更改后,它将已经派发股息。


1
这是一个很好的答案,但我要澄清的是,我并不是在说我们放弃良好做法,而是要追求什么水平的“良好做法”?在每个项目中抽象您的ORM是否是一种好习惯,因为您“可能”需要稍后将其换出另一个?我不这么认为,我愿意接受某些级别的耦合(即,我与所选的框架,语言,ORM和数据库紧密相关)。如果我们遵循SOLID的极端原则,我们是否真的只是在所选堆栈之上实现了自己的框架?
Igneous01

您否认OP的经验是“谬误”。不是建设性的。
max630

@ max630我不是否认它。我花了很大一部分答案来解释OP的观察为何有效。
平坦的

1
@ Igneous01 SOLID不是框架。SOLID是一种抽象,在框架中更常见。当实现任何种类的抽象(包括SOLID)时,总会有一条合理性。您不能只是为了抽象而进行抽象,而是花了很长时间才想出过于笼统的代码,很难遵循。仅将您合理怀疑的内容摘要化,将来对您有用。但是,不要陷入假设您被绑定到例如当前数据库服务器的陷阱中。您永远不会知道明天将发布什么新数据库。
扁平的

@ Igneous01换句话说,您有一个正确的想法,就是不想抽象所有内容,但是我确实感觉到您在那个方向上的倾斜度太高了。开发人员通常会假设当前的需求是一成不变的,然后根据该(一厢情愿的)假设做出体系结构决策,这很常见。
扁平的

7

SOLID如何将简单代码变成框架代码?无论如何,对SOLID都不满意,但是您在这里的意思真的并不明显。

  • KISS是精华小号英格尔归责原则。
  • O pen / Closed Principle中没有任何东西(至少据我所知-参见Jon Skeet)与编写代码不能擅长完成一件事情背道而驰。(实际上,代码越集中,“封闭”部分就越重要。)
  • 大号 iskov替代原则并没有说你必须让人们继承你的类。它说,如果您对类进行子类化,则您的子类应履行其超类的契约。那只是很好的面向对象设计。(并且,如果您没有任何子类,则它不适用。)
  • KISS也是精华覆盖整个院落隔离原则。
  • d ependency倒置原则是唯一一个我可以看到远程应用,但我认为这是普遍误解夸大了。这并不意味着您必须使用Guice或Spring注入所有内容。这只是意味着您应该在适当的地方进行抽象,而不要依赖于实现细节。

我承认我自己并不以SOLID的方式思考,因为我是通过“ 四人帮”Josh Bloch编程学院而不是鲍勃·马丁学院毕业的。但是我真的认为,如果您认为“ SOLID” =“在技术堆栈中添加更多层”,那么您读错了。


PS不要卖掉“为开发人员提供更好的UX”的好处。代码将大部分时间用于维护。开发者就是你


1
关于SRP-有人可能会争辩说,任何带有构造函数的类都违反SRP,因为您可以将这种责任转移给工厂。关于OCP-这实际上是框架级别的问题,因为一旦发布供外部使用的接口,就无法对其进行修改。如果仅在项目内部使用该接口,则可以更改合同,因为您可以在自己的代码中更改合同。关于ISP-有人可能会争辩说,应该为每个单独的操作定义一个接口(从而保留SRP),并涉及外部用户。
Igneous01 '18

3
1)可以,但是我怀疑有人值得倾听。2)您可能会惊讶于一个项目能够快速增长到可以自由修改内部接口的大小,这是一个坏主意。3)参见1)和2)。可以说,我认为您对这三个原则都读得太多。但是,评论并不是真正解决这些争论的地方。我建议您将每个问题摆成一个单独的问题,然后看看会得到什么样的答案。
David Moles

4
@ Igneous01使用该逻辑,您最好放弃getter和setter,因为您可以为每个变量setter创建一个单独的类,为每个getter创建一个class。IE:class A{ int X; int Y; } class A_setX{ f(A a, int N) { a.X = N; }} class A_getX{ int f(A a) { return X; }} class A_setY ... etc.我认为您从工厂主张的角度太过分了。初始化不是域问题的一个方面。
亚伦

@亚伦这。人们可以使用SOLID提出错误的论点,但这并不意味着做坏事情=“遵循SOLID”。
David Moles
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.