解决方案应该是尽可能通用的还是尽可能具体的?


124

假设我有一个具有“类型”属性的实体。可能有20多种可能的类型。

现在,我被要求实现一些允许从A-> B更改类型的方法,这是唯一的用例。

那么,我应该实现一些允许对类型进行任意更改的方法吗,只要它们是有效的类型?或者我应该只允许它根据要求从A-> B更改而拒绝其他任何类型的更改,例如B-> A或A-> C?

我从双方都可以看到优缺点,在将来出现类似要求的情况下,通用解决方案意味着工作量会减少,但这也意味着出错的可能性更大(尽管我们100%控制着调用方点)。
一种特定的解决方案不太容易出错,但是如果出现类似的要求,将来需要做更多的工作。

我一直在听到,优秀的开发人员应该尝试预期变化并设计系统,以便将来轻松扩展,这听起来像是通用解决方案?

编辑:

在我不太具体的示例中添加更多详细信息:在这种情况下,“通用”解决方案比“特定”解决方案需要的工作更少,因为特定解决方案需要对旧类型和新类型进行验证,而通用解决方案只需要验证新类型。


97
这是一个有趣的问题。我已经和我的gramps(一个非常非常老的计时器程序员)讨论了类似的问题,他的回答是“解决您的特定问题的最通用的东西”,最终归结为“这取决于”。软件开发中的一揽子声明很少起作用-始终应视具体情况而定。
T. Sar

2
@ T.Sar将此添加为答案,我会投票:-)
Christophe

4
您认为什么是“通用解决方案”?另外,请澄清“仅用例”的含义。您的意思是仅允许从A-> B进行转换,还是仅指定该转换,并且从任何其他状态转换为任何其他状态都不是错误条件。作为开发人员,您需要请用例的作者进行澄清。如果不允许任何其他转换,并且无论谁控制调用者,您的代码都允许这样做,则您的代码无法满足要求。
RibaldEddie '17

5
如果X是一个好主意,那么始终只有X必须是最优的原则,似乎对开发人员(或其中至少有一个声音团队)具有特别的吸引力,但请考虑以下几点:编程的经验法则是像谚语一样,因为您经常会发现一对含义相反。这表明您应该运用自己的判断力(如此处答案所倡导的那样),并提防教条。
sdenham

2
过早的泛化会导致与过早的优化一样多的胃灼热-您最终编写了很多可能永远不会使用的代码。首先解决特定问题,然后根据需要进行概括。
John Bode

Answers:


296

我的经验法则:

  1. 第一次遇到问题时,只能解决特定问题(这是YAGNI原理)
  2. 第二次遇到相同的问题时,如果工作量不大,请考虑对第一种情况进行概括
  3. 一旦有了三种可以使用通用版本的特定情况,就应该开始真正计划通用版本了-到现在为止,您应该对问题有足够的了解,可以实际进行通用了。

当然,这只是一个准则,而不是一成不变的规则:真正的答案是根据具体情况运用最佳判断。


1
您能解释一下YAGNI是什么吗?
Bernhard

7
@Bernhard,您将不需要它。一个非常有用的软件设计原则。
Angew

4
“在thing1, thing2,考虑使用数组。在thing1, thing2, thing3,几乎可以肯定使用thing[]数组。” 是决定多个变量还是单个数组的相似经验法则。
Joker_vD

15
陈述规则1的另一种方式:“您不能一概而论。”
韦恩·康拉德

2
@SantiBailors:正如布朗博士在回答中所说,我们经常高估通过建立第一个扔掉的东西可能浪费的精力。弗雷德·布鲁克斯(Fred Brooks)在《神话人月》中说:“计划扔掉一个东西,不管怎样,你会的。” 就是说:如果您立即遇到某件事的多个用途(例如,通过交付一组需求,而您显然需要多次解决同一问题),那么您已经有多个案例可以概括从,这是完全可以的,并且与我的回答没有冲突。
丹尼尔·普里登

95

如果需要类似的要求,则特定的解决方案将来需要做更多的工作

我已经听过数十次这样的争论,而且-根据我的经验-经常证明是谬论。如果现在或以后再归纳一下,当第二个类似的要求出现时,总的工作量几乎是相同的。因此,当您不知道这种努力是否会奏效时,绝对没有必要花更多的精力进行概括。

(显然,当更通用的解决方案比特定解决方案更简单,需要的精力更少时,这将不适用,但以我的经验来看,这些情况很少见。这种情况后来被编辑成问题,而并非我的答案是关于)。

当出现“第二种相似情况”时,就该开始考虑泛化了。正确地归纳起来会容易得多,因为第二个要求为您提供了一个场景,您可以在其中验证是否使正确的事物成为通用。当试图只针对一种情况进行概括时,您就是在黑暗中射击。您很有可能过度概括了某些不需要泛化的内容,而错过了其他应该泛化的部分。当第二种情况出现时,您意识到自己概括了错误的内容,那么您需要做更多的工作来解决此问题。

因此,我建议延迟“以防万一”的诱惑。如果您错过了泛化三,四次或更多次的机会,而后又需要维护一堆看起来相似(因此重复)的代码,则此方法只会导致更多的工作和维护工作。


2
对我来说,这个答案在OP的背景下是没有意义的。他说他现在将花费较少的时间进行泛化,而在未来将花费较少的时间,除非将来需要具体实现。您是在反对“在概括中投入更多的精力”,而OP仅在他们没有概括时才会投入更多的努力。(认为​​)....很奇怪:$
msb

2
@msb:该上下文是在我写完答案后添加的,这与OP之前所说的相反,特别是在我引用的部分中。看到我的编辑。
布朗

2
而且,广义的解决方案将更加复杂,因此,当您不得不将其适应第二种情况时,再次理解它将花费更长的时间。
AndreKR

4
Doc,放在一边,但永远不会猜到你是一个非母语的人。您的答案总是写得很好,论据也很好。
user949300

2
当我阅读您未来的答案时,我会想像您的口音。:-)
user949300

64

TL; DR:这取决于您要解决的问题。

在我谈论C#中的FuncAction很棒时,我与Gramps进行了类似的交谈。My Gramps是一位非常古老的计时器程序员,因为软件在占用整个房间的计算机上运行,​​所以它一直围绕源代码编写。

他一生中多次更换技术。他用C,COBOL,Pascal,BASIC,Fortran,Smalltalk,Java编写代码,并最终以业余爱好开始C#。我学习了如何与他一起编程,那时我还是个小伙子,就坐在他的膝盖上,在IBM SideKick的蓝色编辑器上剔除了我的第一行代码。到20岁时,我已经花了更多的时间在编码上,而不是在外面玩。

这些只是我的记忆,请原谅我,如果我在转述它们时不太实际。我有点喜欢那些时刻。

那是他对我说的:


“您应该问一个问题的概括,还是在特定范围内解决它?好吧,这是一个问题。”

Gramps停下来思考了一会儿,同时将眼镜的位置固定在脸上。他正在电脑上玩三消游戏,同时在旧的声音系统上听着Deep Purple的LP。

他告诉我:“好吧,这取决于您要解决的问题”。“令人信服的是,对于所有设计选择都存在一个单一的,神圣的解决方案,但是没有一个。软件架构就像是奶酪,你知道。”

“……奶酪,格兰普?”

“不管您如何看待自己喜欢的东西,总会有人认为它很臭”。

我困惑地眨了眨眼,但是在我没说什么之前,Gramps继续了。

“当您制造汽车时,如何挑选零件的材料?”

“我……我想这取决于所涉及的成本以及该零件应该做什么。”

“这取决于零件要解决的问题。您不会制造钢制轮胎或皮革制挡风玻璃。您会选择最能解决当前问题的材料。现在,什么是通用解决方案还是特定解决方案?要解决什么问题,要解决什么用例,您应该采用全功能的方法,为仅使用一次的代码提供最大的灵活性吗?您的系统中会有很多用途,甚至可能会有很多变化的部分吗?诸如此类的设计选择就像您为汽车零件选择的材料或您用来建造小房子的Lego砖的形状一样哪种乐高积木是最好的?”

这位年老的程序员在继续之前,摸索了桌子上的小乐高火车模型。

“只有在知道需要什么砖的情况下,您才能回答。如果您根本不知道是什么问题,那么您将如何知道特定解决方案是否比通用解决方案更好,反之亦然试图解决吗?您看不到过去无法理解的选择。”

“ ..您只是引用《黑客帝国》吗?

“什么?”

“没事,继续。”

“好吧,假设您正在尝试为国家发票系统构建某种东西。您会从内部知道该地狱的API及其三万行XML文件的样子。用于创建该文件的“通用”解决方案看起来将如何?该文件充满了可选参数,充满了只有非常特殊的业务部门才应该使用的情况。在大多数情况下,您可以放心地忽略它们。如果您只需要创建通用的发票系统,永远不会卖出鞋子。只要建立一个销售鞋子的系统,使其成为目前最好的销售鞋子的发票系统即可。现在,如果您必须为任何类型的客户创建一个发票系统,那么在更广泛的应用上-可以转售为独立的通用销售系统,例如-现在有趣的是实现那些仅用于天然气,食物或酒精的选项。现在,这些是可能的用例。在它们只是一些假设之前,请勿使用案例,并且您不想实现请勿使用案例。不要使用不需要的弟弟。”

Gramps将乐高火车放回原处,回到了他的3场比赛。

“因此,要为给定问题选择通用或特定的解决方案,您首先需要了解问题的根源。否则,您只是在猜测,而猜测是经理而不是程序员的工作。一切都取决于IT。”


所以你有它。“这取决于”。考虑软件设计时,这可能是最强大的两个单词的表达方式。


14
我确定该故事中有有用的东西,但是阅读起来太痛苦了,对不起
GoatInTheMachine

28
你的第一篇文章是一个短有趣轶事-当然这是见仁见智的问题-但除非我真的很喜欢某人的写作风格,我觉得长期这样的故事真的自我放纵和个人博客的有点不合适之外
GoatInTheMachine

16
@ T.Sar不要听那些讨厌的人的话,您的帖子是来自上师的有用建议的瑰宝。+1
LLlAMnYP'1

7
“软件架构就像奶酪”这一部分真是太棒了。确实,这个答案是一颗宝石!
Mathieu Guindon '17

3
也许这个答案也像奶酪吗?;)但是,请“ SmallTalk”应为“ Smalltalk”,是的,我是个人,对不起。
fede s。

14

首先,您应该尝试预测是否会发生这样的更改-不仅仅是发生变化的可能性很小。

如果没有,通常最好现在就选择简单的解决方案,然后再进行扩展。这样一来,您很可能会更清楚地了解需要的内容。


13

如果您使用的是您不熟悉的领域,那么绝对应该应用Daniel Pryden提到的三项规则。毕竟,如果您是该领域的新手,应该如何构建有用的抽象?人们很少能把握抽象的能力,尽管这种情况很少发生。以我的经验,过早的抽象无非是代码复制。错误的抽象确实让人很难理解。有时甚至更难以重构。

本书解决了我关于开发人员正在从事的未知领域的问题。该书由特定领域和提取的有用抽象组成。


问题是关于一个具体问题的。
RibaldEddie '17

4
答案足以解决这个具体问题。这个问题不够具体,无法给出具体的收据:如您所见,问题中没有提及任何域详细信息。甚至没有指定类名(A和B不计算在内)。
Zapadlo

7
“ stackoverflow答案应该尽可能通用还是尽可能具体?”
林登·怀特

@LyndonWhite一个stackoverflow问题应该尽可能通用(太宽泛!)还是尽可能具体(无论该失败称为什么!)?哈。

4

考虑到您所问问题的性质,假设我理解正确,我实际上将其视为中心系统功能的设计问题,而不是有关通用解决方案与特定解决方案的问题。

谈到中央系统的特性和功能时,最可靠的是那些不存在的特性。在极简主义方面犯错误是值得的,尤其是考虑到通常需要长期集中地添加功能,而要消除长期以来不希望有的功能且具有许多依赖性的功能要容易得多,这是因为它使系统工作变得更加困难每个新功能都引发了无穷无尽的设计问题的同时,它也比以前需要的多。

实际上,由于缺乏对将来是否会经常需要它的强烈期望,因此我将尽量避免将其视为从A到B的类型替换,而只是寻求将其作为转换A状态的一种方式。例如,在A中设置一些字段以使其变形并像B一样显示给用户,而无需实际更改为其他“类型”的东西-当A处于状态时,可以使用B中的合成和调用函数来使A私下存储B设置为表示应模仿B以简化实现。那应该是一个非常简单且微创的解决方案。

因此,无论如何,与其他许多人一样,我建议在这种情况下避免避免通用解决方案,但更多的是,因为我认为这是考虑向中央系统添加非常大胆的功能,因此建议您将其遗漏,尤其是现在。


“您会错过所有未编写的错误。” (摘自篮球海报:您会错过所有未拍摄的照片)

3

对于这个具体问题很难给出一个通用的答案;-)

它越通用,您将获得更多的未来更改时间。例如,由于这个原因,许多游戏程序都使用实体组件模式,而不是为游戏中的角色和对象构建非常复杂但僵化的类型系统。

另一方面,制作通用产品需要在设计上进行前期的时间和精力投入,这要比非常特定的产品要高得多。这承担了过度设计的风险,甚至可能因未来的潜在需求而迷失。

总是值得一看的是,有没有一种自然的概括可以使您取得突破。但是,最后,您现在可以花费的精力与将来可能需要的努力之间是一个平衡的问题。


3
  1. 混合动力车 这不必是一个/或一个问题。您可以为一般类型转换设计API,同时仅实现您现在需要的特定转换。(只要确保有人通过不支持的转换调用您的常规API,它就会失败并显示“不支持”错误状态。)

  2. 测试。对于A-> B转换,我将不得不编写一个(或少量)测试。对于一般的x-> y转换,我可能必须编写一个完整的测试矩阵。即使所有转换共享一个通用实现,也要花更多的功夫。

    另一方面,如果发现有一种通用的方法可以测试所有可能的转换,则没有更多的工作要做,我可能会倾向于尽快采用通用的解决方案。

  3. 耦合。从A到B的转换器可能需要了解有关A和B的实现细节(紧密耦合)。如果A和B仍在发展,这意味着我可能不得不继续回顾很糟糕的转换器(及其测试),但至少它仅限于A和B。

    如果我使用的通用解决方案需要访问所有类型的详细信息,那么即使C和D不断发展,我可能也需要不断调整通用转换器(和大量测试),这甚至会使我的速度减慢虽然还没有人需要转换为C或A d

    如果可以仅将松散耦合到类型的详细信息的方式实现泛型转换和特定转换,那么我就不必为此担心。如果其中一种可以通过松散耦合的方式完成,而另一种需要紧密耦合,那么对于松散耦合的方法而言,这是一个强有力的论据。


2
测试是一个好点。如果您基于类别理论中的概念设计通用解决方案,那么这将是一个很大的优势,因为您经常会得到免费的定理,这些定理证明签名的唯一可能实现(编译器的类型检查器接受)是正确的,或者至少如果算法适用于任何特定类型,则它也必须适用于所有其他类型。
大约

我猜有一个特定于域的原因为什么只要求一种类型转换,这意味着大多数类型转换在问题域中都是无效操作,因此,应用程序不应该支持它们,除非它们被正式允许。这个答案最接近那个论点。
拉尔夫·克莱伯霍夫

3

解决方案应该是尽可能通用的还是尽可能具体的?

这不是一个可以回答的问题。

您可以合理地获得的最佳结果是一些启发式方法,以决定制定给定解决方案的一般性或特定性。通过下面的过程,通常一阶近似是正确的(或足够好)。如果不是这样,原因很可能是特定于域的,因此无法在此处详细介绍。

  1. 一阶近似:Daniel Pryden,Doc Brown 等人所描述的通常的YAGNI 3规则。

    这是一种通常有用的启发式方法,因为它可能是您依赖域和其他变量而能做的最好的事情。

    因此,最初的假设是:我们做最具体的事情。

  2. 二阶近似:根据您对解决方案领域的专业知识,您会说

    在这种情况下,“通用”解决方案比“特定”解决方案所需的工作更少

    因此我们可能会将YAGNI重新解释为建议我们避免不必要的工作,而不是避免不必要的普遍性。因此,我们可以修改我们的初始假设,而做最简单的事情。

    但是,如果您的解决方案领域知识表明,最简单的解决方案可能会引发很多错误,或者很难进行充分测试,或者导致任何其他问题,那么易于编写代码并不一定是更改我们的建议的充分理由。原始选择。

  3. 三阶近似:您的问题领域知识是否表明最简单的解决方案实际上是正确的,还是您允许进行许多您认为无意义或错误的转换?

    如果简单但通用的解决方案看起来有问题,或者您对判断这些风险的能力不自信,那么做额外的工作并坚持最初的猜测可能会更好。

  4. 四阶近似:您是否了解客户的行为,或者该功能与他人的关系,项目管理的优先级,或者...其他任何非严格的技术考虑因素会影响您当前的工作决策?


2

用简单的答案回答这个问题并不容易。许多答案都给试探法建立了3或类似的规则。超越这些经验法则是困难的。

要真正真正回答你的问题,你必须考虑你的工作是最有可能不实现的东西,改变A-> B。如果您是承包商,也许这是必要条件,但是如果您是员工,则被雇用来为公司执行许多其他较小的任务。更改A-> B只是这些任务之一。贵公司将在意将来的更改效果如何,即使请求中未注明。要找到“ TheBestImplementation(tm)”,您必须查看实际上被要求执行的操作的大图,然后使用它来解释您被要求更改A-> B的小请求。

如果您是刚从大学毕业的低级入门程序员,通常建议完全按照您的指示去做。如果您被聘为具有15年经验的软件架构师,通常建议考虑一下大事。每项实际工作都将介于“准确地完成狭窄的任务”和“思考全局”之间。如果您与人们进行充分的交谈并为他们做足够的工作,您就会感觉自己的工作适合该领域。

我可以举几个具体的例子,其中您的问题根据上下文有明确的答案。考虑编写安全性至关重要的软件的情况。这意味着您需要一支测试团队来确保产品按预期执行。这些测试团队中的某些团队需要测试代码中的每个可能路径。如果与他们交谈,您可能会发现,如果对行为进行一般化,则他们的测试成本将增加30,000美元,因为他们将不得不测试所有这些额外的路径。在这种情况下,即使您必须重复工作7或8次,也不要添加通用功能。节省公司资金,并完全按照要求进行操作。

另一方面,请考虑您正在制作一个API,以允许客户访问您公司制造的数据库程序中的数据。客户请求允许更改A-> B。API通常具有金色的手铐:向API添加功能后,通常不应该删除该功能(直到下一个主要版本号)。您的许多客户可能不愿意为升级到下一个主要版本号支付费用,因此您可能会长期选择使用任何解决方案。在这种情况下,我强烈建议从头开始创建通用解决方案。您确实不想开发出一个带有一次性行为的不良API。


1

嗯...没有太多要继续回答的内容...回荡了以前的答案,“取决于”。

从某种意义上说,您必须依靠自己的经验。如果不是您的,则该领域的资深人士。您可以质疑接受标准的状态。如果是“用户应该能够将类型从“ A”更改为“ B”而不是“用户应该能够将类型从其当前值更改为任何允许的备用值”,则可以使用。

经常会接受解释的标准,但是优秀的质量检查人员可以编写适合于当前任务的标准,从而最大程度地减少所需的解释。

是否存在不允许从“ A”更改为“ C”或任何其他选项,而只能从“ A”更改为“ B”的域限制?还是这只是一个狭spec的要求,而不是“超前思维”?

如果一般情况下比较困难,我会在开始工作之前去询问,但是如果您能“预测”将来还会有其他“类型”变更请求,我很想:编写可在一般情况下重用的内容,然后b)将其包装在一个仅允许A-> B的条件下。

足够容易地在自动化测试中验证当前案例,并且足够容易在以后出现不同用例时打开其他选项。


1

对我来说,我不久前建立的指导方针是:“对于假设的要求,只写假设的代码。” 也就是说,如果您预期有其他要求,则应考虑如何实现这些要求,并对当前代码进行结构设计,以使其不会那样阻塞。

但是,现在不要为这些编写实际的代码-只需考虑一下您将要做什么。否则,您通常会使事情变得不必要地复杂,并且当实际需求与您的预期有所不同时,稍后可能会感到烦恼。

以您的示例为例:如果您具有convert方法的所有用法,如果您在控制之下,则可以立即将其称为convertAToB,并计划在IDE中使用“重命名方法”重构来重命名它,如果以后需要更多常规功能。但是,如果转换方法是公共API的一部分,则可能会大不相同:因为该特定方法会在以后阻止泛化,因为在这种情况下很难重命名。


0

我一直在听到,优秀的开发人员应该尝试预期变化并设计系统,以便将来可以轻松进行扩展,

原则上可以。但这并不必然导致通用的解决方案。

就我而言,在软件开发中有两种类型的主题,您应该在这些主题中预测未来的变化:

  • 打算供第三方使用的库,以及
  • 整体软件架构。

第一种情况是通过观察您的内聚/耦合,依赖项注入或其他方法解决的。第二种情况是在更抽象的层次上进行的,例如,为大型应用程序选择面向服务的体系结构,而不是大量的单一代码块。

在您的情况下,您正在寻求针对特定问题的特定解决方案,该解决方案对未来没有任何可预见的影响。在这种情况下,YAGNI和DRY是拍摄以下影片的好座右铭:

  • YAGNI(你是不是要去需要它)告诉您实现您需要和使用的绝对最低,基本的东西现在。如果您应该使用TDD / BDD / FDD样式开发,则意味着要实现使当前测试套件从红色变为绿色的最低要求。没有一行。
  • 干(不要重复)意味着如果您再次遇到类似的问题,那么您将认真考虑是否需要通用解决方案。

结合其他现代实践(例如良好的测试覆盖范围以便能够安全地进行重构),这意味着您最终会获得可以按需增长的快速编写,精简且平均的代码。

听起来通用的解决方案是可行的方法?

不,听起来您应该拥有编程环境,语言和工具,可以在需要时轻松,有趣地进行重构。通用解决方案不提供此功能;它们使应用程序与实际域脱钩。

看一下现代ORM或MVC框架,例如Ruby on Rails;在应用程序级别,所有焦点都放在进行非常规工作上。Rails库本身显然是几乎100%通用的,但是在这方面,域代码(您的问题是关于域代码)应该做的最少。


0

思考问题的另一种方法是考虑什么才有意义。

例如,有一个我正在开发的应用程序,其中的部分执行几乎相同的操作,但权限规则不一致。由于没有理由让它们保持不同,因此当我重构该部分时,我使它们都以相同的方式执行权限。使整个代码更小,更简单,界面更一致。

当管理层决定允许其他人访问某个功能时,我们只需更改一个标志就可以做到。

显然,进行特定的类型转换是有意义的。进行附加类型转换是否也有意义?

请记住,如果通用解决方案的实施速度更快,那么特殊情况也很容易,只需检查它是否是您所允许的唯一类型转换即可。

如果应用程序处于高度管制的区域(医疗或财务应用程序),请尝试让更多的人参与您的设计。

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.