应该优先考虑什么:YAGNI或Good Design?


76

在什么时候,YAGNI应该优先于良好的编码习惯,反之亦然?我正在研究一个正在工作的项目,并希望向我的同事缓慢介绍良好的代码标准(目前还没有,并且一切都只是某种形式而没有韵律或理由),但是在创建了一系列类之后(我们不要做TDD,或者根本不进行任何类型的单元测试。)我退后一步认为这违反了YAGNI,因为我非常确定地知道我们不需要扩展其中的某些类。

这是我意思的一个具体示例:我有一个数据访问层,其中包装了一组存储过程,该过程使用具有基本CRUD功能的基本存储库样式模式。由于所有存储库类都需要几种方法,因此我为存储库创建了一个通用接口,称为IRepository。但是,然后我为每种类型的存储库(例如ICustomerRepository)创建了一个“标记”接口(即,没有添加任何新功能的接口),具体类实现了该接口。我已经通过Factory实现完成了同样的事情,以便从存储过程返回的DataReaders / DataSets中构建业务对象。我的存储库类的签名通常看起来像这样:

public class CustomerRepository : ICustomerRepository
{
    ICustomerFactory factory = null;

    public CustomerRepository() : this(new CustomerFactory() { }

    public CustomerRepository(ICustomerFactory factory) {
        this.factory = factory;
    }      

    public Customer Find(int customerID)
    {
        // data access stuff here
        return factory.Build(ds.Tables[0].Rows[0]);
    }
}

我在这里担心的是,我违反了YAGNI,因为我有99%的确定性知道,除了CustomerFactory向该存储库提供具体内容外,没有其他理由。因为我们没有单元测试,所以我不需要任何MockCustomerFactory类似的东西,并且拥有如此多的界面可能会使我的同事感到困惑。另一方面,使用工厂的具体实现方式似乎具有设计异味。

在适当的软件设计与不过度设计解决方案之间是否有折衷的好方法?我在问我是否需要所有“单一实现接口”,或者是否可以牺牲一些好的设计,而仅拥有例如基本接口和单个具体的接口,而不用担心对该接口进行编程接口(如果实现的话)将被使用。


17
您说“因为我们没有单元测试,所以我不需要MockX”,自然会导致“我不需要IX,我只需要X”。我想说的是,您没有单元测试这一事实凸显了您需要IX和MockX的事实,因为这些东西将帮助您进行单元测试。不要接受没有测试的现实,将其视为暂时的问题,可以在(很长一段时间)内解决。
安东尼·佩格拉姆

10
即使使用Google进行琐事,也应该有人提到YAGNI代表“您将不需要它”
thedaian 2011年

1
我认为,如果您正在编写像这样的新类,则需要添加一些单元测试。即使您的同事不会经营他们。至少以后您可以说:“看!当您破坏我的代码时,我的单元测试抓住了它!看看多么棒的单元测试!” 在这种情况下,使其可模拟可能在这里值得。(不过,如果可以在不定义接口的情况下
模拟

单元测试是否会迫使我创建(或使用)模拟框架,以免影响实时存储过程?这就是我倾向于不添加测试的主要原因-我们每个人都有要测试并编写代码的生产数据库的本地副本。
韦恩·莫利纳

3
@Anthony嘲笑是否总能证明它带来的额外复杂性开销?模拟是一个很好的工具,但其实用性也必须与成本进行权衡,有时过多的间接间接会影响规模。当然,有一些工具可以帮助解决额外的复杂性,但是它们并不能使复杂性消失。给定“不惜一切代价进行测试”的趋势似乎越来越大。我相信这是错误的。
康拉德·鲁道夫

Answers:


75

在适当的软件设计与不过度设计解决方案之间是否有折衷的好方法?

亚尼

我可以牺牲一些好的设计

错误的假设。

然后是基本界面,然后是单个混凝土,

那不是“牺牲”。那好的设计。


72
达到完美,不是在没有其他可添加的东西时,而是在没有其他东西可取的时候。Antoine De St-Exupery
Newtopian 2011年

5
@Newtopian:人们对苹果的最新产品有不同的看法。:)
罗伊·廷克

14
我对这种答案有很大的疑问。“ X是真实的,是因为Y说了,Y得到了社区的良好支持”吗?如果现实生活中有人反对YAGNI,您是否会向他展示这个答案作为论点?
vemv 2011年

2
@vemv。这里没有人反对YAGNI。S.Lott表示,OP认为两个值得实现的目标之间的矛盾是一个错误。顺便说一句,您知道在当今的软件世界中,有哪个活跃的开发人员反对 YAGNI吗?如果您正在从事真实的项目,您会知道需求在不断变化,并且用户和经理通常不知道自己在做什么或不想要什么,直到您将其摆在面前。实施必要的工作是充实的工作-为什么通过编写试图预测未来的代码来浪费时间,精力和金钱(或冒险工作)?
矢量

6
我的意思不是关于YAGNI,而是关于答案质量。我并不是说有人特别在抱怨,这是我推理的一部分。请再次阅读。
vemv 2011年

74

在大多数情况下,避免使用不需要的代码可以带来更好的设计。最易于维护和面向未来的设计是使用最少数量的满足要求的命名简单的代码。

最简单的设计是最容易演变的。没有什么比无用的,过度设计的抽象层杀死了可维护性。


8
我发现“避免使用YAGNI代码”非常含糊。这可能意味着您不需要的代码遵循YAGNI原则的代码。(请参见“ KISS代码”)
2011年

2
@sehe:完全没有歧义(尽管“坚持YAGNI原则”是完全自相矛盾的),如果您清楚地说明了这一点:“您不会需要它的代码”可能意味着什么,但是对它进行了编码你不需要吗?
Michael Borgwardt

1
我将以大字体打印此答案,并将其挂在所有宇航员都能阅读的位置。+1!
kirk.burleson 2011年

1
+1。我仍然记得我的第一个公司项目,该项目用于支持和添加一些新功能。我要做的第一件事是从40,000行程序中删除了32,000行无用的代码,而不会丢失任何功能。此后不久,原来的程序员被解雇了。
EJ Brennan

4
@vemv:将“无用”读为“当前不被使用,只有少量使用”,即YAGNI。“过度设计”比“糟糕”要具体得多。具体地说,它的意思是“由理论概念或想象的可能要求引起的复杂性,而不是具体的当前要求”。
Michael Borgwardt

62

YAGNI和SOLID(或任何其他设计方法)不是互相排斥的。但是,它们是相反的极性。您不必都坚持其中的100%,但是会有一些让步。您越是在一个位置上看到一个班级使用的高度抽象的模式,并说出YAGNI并将其简化,则设计变得越不牢固。反之亦然。在开发中很多次,设计都是“按信念”实现的。您看不到将如何使用它,但是您只有预感。这可能是正确的(并且随着您获得的经验越来越多,也越来越可能是正确的),但是它也可能使您承担像轻率的“轻而易举”的方法一样多的技术负担。而不是DIL的“意大利面条代码”代码库,您可能会得到“烤宽面条代码”,拥有如此多的层,以至于只需添加一个方法或一个新的数据字段就变成了为期数天的过程,其中涉及的服务代理和松散耦合的依赖项仅涉及一个实现。否则,您可能最终会得到“馄饨代码”,它包含了很小的小块内容,因此体系结构中的上,下,左或右移动将带您通过50条方法,每条方法包含3行。

我已经在其他答案中说过了,但是这里是:在第一遍,使其生效。在第二遍,使其优雅。在第三遍,将其设为实心。

分解:

当您第一次编写一行代码时,它只是必须工作。在这一点上,就您所知,这是一次性的。因此,构建“象牙塔”体系结构以添加2和2并没有任何风格点。做您必须做的事情,并假设您再也看不到它了。

下次您的光标进入该代码行时,您现在就不再像首次编写那样就驳斥了这一假设。您正在重新访问该代码,可能会将其扩展或在其他地方使用它,因此它不是一次性的。现在,应该实现一些基本原理,例如DRY(不再重复)和其他一些简单的代码设计规则。为重复的代码提取方法和/或形成循环,为常见的文字或表达式提取变量,也许添加一些注释,但是总体上您的代码应自行记录。现在,您的代码井井有条,甚至可能是紧密耦合的,其他任何查看它的人都可以通过阅读代码轻松地了解您的工作,而不必逐行跟踪。

光标第三次输入该代码,可能很重要。您要么再次扩展它,要么在代码库中至少三个不同的地方变得有用。在这一点上,它是系统的关键(如果不是核心的话)元素,因此应该进行架构设计。在这一点上,您通常还了解到目前为止如何使用它,这将使您能够就如何设计设计以简化这些用法和任何新用法做出良好的设计决策。现在,SOLID规则应输入方程式;提取包含特定目的代码的类,为具有类似目的或功能的任何类定义通用接口,在类之间建立松耦合的依赖关系,并设计这些依赖关系,以便您可以轻松地添加,删除或交换它们。

从这一刻起,如果您需要进一步扩展,重新实现或重复使用此代码,则将它们很好地打包并以我们都知道并喜欢的“黑匣子”格式抽象;将其插入您需要的其他位置,或在主题上添加新的变体作为该接口的新实现,而不必更改该接口的用法。


我第二棒。
FilipDupanović2011年

2
是。当我以前进行重用/扩展设计时,我发现当我想重用或扩展它时,它将以与预期不同的方式进行。预测是困难的,尤其是关于未来。因此,我支持您的三击规则-到那时,您对如何重用/扩展它有了一个合理的认识。注意:如果您已经知道会如何(例如,来自先前的项目,领域知识或已经指定),则是一个例外。
11ren

在第四遍,使其超赞。
理查德·尼尔·伊拉根

@KeithS:约翰·卡马克(John Carmack)做了类似的事情:“ Quake II的源代码……将Quake 1,Quake World和QuakeGL统一为一个漂亮的代码体系结构。” fabiensanglard.net/quake2/index.php
13ren 2011年

+1我想我会给您起个规则:“实用重构” :)
Songo 2012年

31

我更喜欢WTSTWCDTUAWCROT,而不选择任何一个?

(我们能做的最简单的事情是什么,我们可以在星期四发布?)

简单的首字母缩略词是我要做的事情,但并不是优先考虑的事情。


8
该首字母缩写词违反了YAGNI原则:)
riwalk

2
我恰好用了我需要的字母-越来越多。这样,我有点像莫扎特。是的 我是首字母缩写的莫扎特。
Mike Sherrill'Cat Recall'11

4
我从来不知道莫扎特在缩写时会感到可怕。我每天都在SE网站上学到一些新东西。:P
卡梅伦·麦克法兰

3
@Mike Sherrill'CatRecall':也许应该将其扩展到WTSTWCDTUWCROTAWBOF =“最简单的事情是有用的,我们可以在周四发布,而在周五不会休息吗?” ;-)
乔治

1
@ Stargazer712否:)它违反了POLA。
v.oddou '16

25

YAGNI和良好的设计没有冲突。YAGNI即将(不)支持未来的需求。良好的设计是使透明的软件做什么,现在,以及如何做到这一点。

引入工厂会使您现有的代码更简单吗?如果没有,请不要添加它。如果这样做,例如在添加测试时(您应该这样做!),请添加它。

YAGNI将不增加复杂性以支持将来的功能。
好的设计旨在消除复杂性,同时仍支持所有当前功能。


15

他们没有冲突,您的目标是错误的。

您想完成什么?

您想要编写高质量的软件,并且要做到这一点,请确保代码量少而不会出现问题。

现在我们陷入了冲突,如果我们不编写不打算使用的案例,该如何掩盖所有案例?

这就是您的问题。

在此处输入图片说明 (任何有兴趣的人,这被称为蒸发云

那么,这是什么驱动的呢?

  1. 你不知道你不需要什么
  2. 您不想浪费时间和夸大您的代码

我们可以解决其中哪一项?好吧,看起来不想浪费时间和膨胀的代码是一个伟大的目标,而且很有意义。那第一个呢?我们可以找出需要编写什么代码吗?

我正在一个项目上工作,并希望向我的同事慢慢介绍好的代码标准(目前还没有,而且一切都只是被混在一起而没有押韵或理由)[...]我退后了一步并认为这违反了YAGNI,因为我非常确定地知道我们不需要扩展其中的某些类。

让我们重新说明一下

  • 没有代码标准
  • 没有正在进行的项目计划
  • 到处都是牛仔,他们在做自己该死的事情(而你正试图在荒野的西部扮演警长)。

在适当的软件设计与不过度设计解决方案之间是否有折衷的好方法?

您不需要妥协,您需要有人来管理有能力且对整个项目有远见的团队。您需要一个可以计划您需要的人,而不是每个人都投入您不需要的东西,因为您对未来如此不确定,因为……为什么?我会告诉你为什么,这是因为在你们所有人中间谁也没有该死的计划。您正在尝试引入代码标准来解决一个完全独立的问题。您需要解决的主要问题是明确的路线图和项目。一旦掌握了这一点,您就可以说“代码标准可以帮助我们更有效地实现团队目标”,这是绝对的事实,但不属于该问题的范围。

找一个可以做这些事情的项目/团队经理。如果您有一个,则需要问他们一张地图,并解释YAGNI问题,即没有地图。如果他们严重不称职,请自己编写计划并说:“这是我为您提供的关于我们需要的东西的报告,请仔细阅读并告知我们您的决定。”


不幸的是,我们没有一个。开发经理要么更关心快速完成工作,要么不关心标准/质量,要么制定标准实践,例如在从类直接返回的所有地方使用原始数据集,VB6样式编码以及所有代码中的所有内容,并复制和粘贴所有重复逻辑过度。
韦恩·莫利纳

很好,它会变慢一些,但是那时候您需要对他们的担心说话。解释这个YAGNI /无用代码问题是在浪费时间,建议路线图,给他一个路线图,并说明它将使工作更快。当他被收购时,就会遇到标准差对发展速度造成的问题,建议更好的问题。他们想完成项目,他们只是不知道该如何管理,您要么需要替换他,要么给他一个孩子……或者辞职。
隐身

嗯.....我的主要目标是“您想拥有一个好的,有价值的产品”?
sehe 2011年

@sehe那不是他的决定:p。
隐身

3
好答案。他正面临一场艰苦的战斗。如果没有管理层的支持,将很难完成它。您说对了,这个问题为时过早,不能解决真正的问题,这是正确的。
塞斯·斯皮尔曼

10

YAGNI绝不会涉及使用单元测试扩展代码,因为您需要它。但是,我不认为您对单一实现接口的设计更改实际上会提高代码的可测试性,因为CustomerFactory已经从接口继承,并且可以随时替换为MockCustomerFactory。


1
+1是有关实际问题的有用评论,而不仅仅是关于Good Design和YAGNI之间的宇宙之战和/或相爱。
psr

10

这个问题提出了一个错误的困境。正确应用YAGNI原则并非无关紧要。这是良好设计的一方面。SOLID原则中的每一个都是良好设计的方面。您不能总是在任何学科中都完全应用所有原则。现实中的问题对您的代码施加了很大的力量,其中一些力量朝相反的方向发展。设计原则必须解决所有这些问题,但是没有少数原则可以适合所有情况。

现在,让我们看一下每个原理,并理解尽管它们有时可能会朝着不同的方向发展,但它们绝不存在内在的冲突。

YAGNI旨在帮助开发人员避免一种特殊的返工:这是由于构建错误的东西而引起的。它通过指导我们避免基于对我们认为将来会改变或需要的东西的假设或预测来过早做出错误的决定,从而做到这一点。集体经验告诉我们,当我们这样做时,我们通常是错误的。例如,YAGNI会告诉你没有创建一个接口的可重用性的目的,除非你知道现在你需要多个执行者。同样,YAGNI会说不要创建“ ScreenManager”来管理应用程序中的单个表单,除非您现在知道将要拥有多个屏幕。

与许多人的想法相反,SOLID 与可重用性,通用性甚至抽象无关。SOLID旨在帮助您编写为更改做好准备的代码,而无需说明任何具体更改。SOLID的五项原则创建了一种构建代码的策略,该策略既灵活又不会过于通用,而又简单又不会太幼稚。正确应用SOLID代码会产生具有明确定义的角色和边界的小型,专注的类。实际结果是,对于任何所需的需求更改,都需要触摸最少数量的类。同样,对于任何代码更改,到其他类的“涟漪”的数量最少。

查看示例情况,让我们看看YAGNI和SOLID可能要说些什么。由于所有存储库从外部看起来都是一样的,因此您正在考虑使用通用的存储库接口。但是,通用接口的价值在于可以使用任何实现程序,而无需知道具体是哪个接口。除非您的应用程序中有某个地方是必要或有用的,否则YAGNI表示不要这样做。

有5种SOLID原则可供研究。S是单一责任。这没有说明接口,但是可能说明了具体的类。可能会争辩说,最好将数据访问本身的处理交给一个或多个其他类负责,而存储库的职责是从隐式上下文(CustomerRepository是对Customer实体隐式存储库)转换为对指定客户实体类型的通用数据访问API。

O是开闭的。这主要是关于继承。如果您尝试从实现通用功能的通用库中获取存储库,或者希望从其他存储库中进一步获取存储库,则适用此方法。但是你不是,所以不是。

L是里斯科夫可取代性。如果您打算通过公共存储库界面使用存储库,则适用此规则。它对接口和实现施加了限制,以确保一致性并避免对不同实现者进行特殊处理。原因是这种特殊处理破坏了接口的目的。考虑此原则可能很有用,因为它可能会警告您不要使用公共存储库界面。这与YAGNI的指导相吻合。

我是接口隔离。如果您开始向存储库添加其他查询操作,则可能适用。接口隔离适用于您可以将类的成员划分为两个子集的情况,其中一个将由某些使用者使用,而另一个由其他使用者使用,但是没有使用者可能会同时使用两个子集。指南是创建两个单独的接口,而不是一个通用的接口。在您的情况下,获取和保存单个实例的可能性不太可能被用于一般查询的同一代码占用,因此将它们分成两个接口可能会很有用。

D是依赖注入。在这里,我们回到与S相同的地方。如果您将对数据访问API的使用划分为一个单独的对象,则该原则表示,在创建对象时应将其传递给它,而不是仅更新该对象的实例。存储库。这样可以更轻松地控制数据访问组件的生命周期,从而可以在存储库之间共享对它的引用,而不必走单线之路。

需要注意的是,大多数SOLID原则不一定适用于应用程序开发的特定阶段。例如,是否应该中断数据访问取决于它的复杂程度,以及是否要在不访问数据库的情况下测试存储库逻辑。听起来这不太可能(不幸的是,我认为),因此可能没有必要。

因此,经过所有考虑之后,我们发现YAGNI和SOLID实际上确实提供了一条共同的,立即相关的可靠建议:创建通用的通用存储库接口可能没有必要。

所有这些仔细的思考对于学习活动都是非常有用的。随着学习的进行,这很耗时,但是随着时间的流逝,您会发展直觉并变得非常快。您会知道做对的事情,但是除非有人要求您解释原因,否则您无需考虑所有这些字眼。


我认为本页中的大多数讨论都忽略了GRASP原理的两个主要竞争者“低耦合”和“高凝聚力”。昂贵的设计决策源于“低耦合”原理。就像何时出于低耦合的目的“激活” SRP + ISP + DIP。示例:在MVC模式中,一类-> 3类。甚至更昂贵:拆分为.dll / .so模块/程序集。由于构建的影响,项目,makelist,构建服务器,源代码版本的文件的添加,这非常昂贵……
v.oddou

6

您似乎认为“好的设计”意味着遵循某种思想和一套正式规则,即使无用,也必须始终遵循这些规则。

海事组织这是不好的设计。YAGNI是良好设计的组成部分,从不与其矛盾。


2

在您的示例中,我想说YAGNI应该占上风。如果您以后需要添加接口,则不会花费那么多钱。顺便说一句,如果根本没有任何目标,按类提供一个接口真的是一种好的设计吗?

再想一想,也许有时候您所需要的不是好的设计而是足够的设计。这是有关该主题的非常有趣的帖子序列:


您的第一个和第三个链接将转到同一位置。
David Thornley

2

有人认为,接口名称不应以I开头。具体原因之一是,您实际上正在泄漏对给定类型是类还是接口的依赖性。

是什么使您一开始CustomerFactory不能成为类,后来又将其更改为由DefaultCustormerFactory或实现的接口UberMegaHappyCustomerPowerFactory3000?您唯一需要更改的就是实现实例化的地方。如果您的设计不太理想,那么最多就是几个地方。

重构是开发的一部分。与为每个单个类声明一个接口和一个类相比,最好只包含很少的代码(易于重构),这迫使您同时至少在两个位置更改每个方法名称。

使用接口的真正目的是实现模块化,这可能是良好设计的最重要支柱。但是请注意,模块不仅由其与外界的解耦定义(即使这是我们从外部角度来看的方式),也由其内部工作定义。
我要指出的是,将本质上属于一起的事物解耦并没有多大意义。从某种意义上来说,这就像一个橱柜,每个杯子都有一个单独的架子。

重要性在于将一个大而复杂的问题设计成更小,更简单的子问题。而且,您必须停下来,在不进一步细分的情况下它们变得足够简单,否则实际上它们将变得更加复杂。这可以看作是YAGNI的必然结果。这绝对意味着好的设计。

目的不是以某种方式通过单个存储库和单个工厂解决本地问题。目的是,此决定对您的其余应用程序不起作用。这就是模块化的意义所在。
您希望您的同事看着您的模块,看到带有几个不言自明的调用的外观,并感到自信,他们可以使用它们,而不必担心所有潜在的复杂内部管道。


0

您将interface在将来创建预期的多种实现。您最好I<class>在代码库中为每个类都提供一个。别。

只需根据YAGNI使用单个具体类即可。如果发现需要一个“模拟”对象来进行测试,请将原始类转换为具有两个实现的抽象类,一个具有原始的具体类,另一个具有模拟的实现。

显然,您将不得不更新原始类的所有实例化以实例化新的具体类。您可以通过在前面使用静态构造函数来解决此问题。

YAGNI表示在需要编写代码之前不要编写代码。

好的设计说要使用抽象。

您可以同时拥有。类是一个抽象。


0

为什么标记界面?令我惊讶的是,它所做的不过是“标记”。每个工厂类型都有不同的“标签”,这有什么意义?

接口的目的是使类具有“类似于”的行为,使它们具有“存储库能力”。因此,如果您所有具体的存储库类型的行为都类似于相同的IRepository(它们都实现了IRepository),则可以通过其他代码以完全相同的代码以相同的方式来处理它们。此时,您的设计是可扩展的。添加更多具体的存储库类型,所有类型都作为通用IRepository处理-相同的代码将所有具体类型作为“通用”存储库处理。

接口用于基于通用性处理事物。但是自定义标记界面a)不添加任何行为。b)强迫您处理其独特性。

在设计有用的接口的程度上,您将获得OO的好处,而不必编写专门的代码来处理与具体类具有1:1关联的专门类,类型或自定义标记接口。那是毫无意义的冗余。

例如,如果您需要一个由很多不同类组成的强类型集合,那么我可以从旁看到标记器接口。在集合中,它们都是“ ImarkerInterface”,但是当您将它们拉出时,必须将它们转换为适当的类型。


0

您现在可以写下一个模糊合理的ICustomerRepository吗?例如,(您的客户确实到过这里,可能是一个不好的例子)您的客户过去一直使用PayPal吗?还是公司中的每个人都对与阿里巴巴建立联系充满了敬意?如果是这样,您可能想立即使用更复杂的设计,并且对老板来说是有远见的。:-)

否则,请稍候。在拥有一个或两个实际实现之前,在接口上进行猜测通常会失败。换句话说,除非您有几个示例可以概括,否则不要概括/抽象/使用奇特的设计模式。

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.