为什么我们在设计模式中需要这么多的类?


208

我是大四学生中的初级开发人员,并且在理解他们的思维和推理方面付出了很多努力。

我正在阅读域驱动设计(DDD),无法理解为什么我们需要创建这么多的类。如果采用这种设计软件的方法,我们最终将得到20-30个类,最多可以替换为两个文件和3-4个函数。是的,这可能很麻烦,但是它更具可维护性和可读性。

每当我想查看某种EntityTransformationServiceImpl功能时,都需要遵循许多类,接口,它们的函数调用,构造函数,它们的创建等等。

简单的数学:

  • 60行伪代码与10类X 10(假设我们有完全不同的逻辑)= 600行杂乱代码与100类+更多包装和管理它们;不要忘记添加依赖项注入。
  • 读取600行凌乱代码=一天
  • 100节课=一周,仍然忘记哪个课什么时候

每个人都说它易于维护,但是为什么呢?每次添加新功能时,都会添加五个带有工厂,实体,服务和值的类。我觉得这种代码的运行速度比凌乱的代码要慢得多。

假设,如果您在一个月内编写了50K LOC混乱代码,则DDD内容需要进行大量审查和更改(我都不介意在两种情况下进行测试)。一个简单的添加可能需要数周,甚至更多。

一年之内,您编写了很多混乱的代码,甚至可以多次重写它,但是使用DDD样式,您仍然没有足够的功能来与混乱的代码竞争。

请解释。为什么我们需要这种DDD样式和许多模式?

UPD 1:我收到了很多不错的答案,请大家在某处添加评论或使用阅读列表的链接编辑您的答案(不确定从哪开始,DDD,设计模式,UML,代码完成,重构,实用性等。) ..那么多好书),当然还有顺序,这样我就可以像你们中的一些人一样开始理解并成为高级。


81
我认为这里有一个很好的问题,但它隐藏在夸张和沮丧的背后。@ user1318496,稍微改一下您可能会受益。
MetaFight

48
由于您的语言很烂,因此有踩脚的危险。不要仅仅因为它而已:出于多种原因,容易上瘾的语言通常是正确的技术选择,但这并不能使它变得不容易上瘾。至于您的实际问题,正确性比可读性更重要。或换句话说:可读性在使正确性推理成为可能的范围内很重要。那么,您的100个课程还是整体的上帝课程哪个更容易测试?我下注100个简单的类。
贾里德·史密斯

87
“是的,这可能很麻烦,但是它更具可维护性和可读性。” 如果杂乱无章,如何可读性和可维护性?
Polygnome

37
@Polygnome:我正要输入完全相同的评论。初级开发人员的另一个选择注释特征是“我觉得这种代码的运行速度比混乱的代码要慢得多”。如果您花费一半的时间撞墙,那么就无法以最快的速度运行!慢是光滑的,光滑是快速的。
埃里克·利珀特

40
除非您正在使用Java,否则您不会这么做。当您只有Java时,一切看起来都像一类。
hobbs

Answers:


335

这是一个优化问题

优秀的工程师理解,没有目标就没有优化问题。您不仅可以优化,还必须针对某些事物进行优化。例如,您的编译器选项包括:优化速度和优化代码大小;等等。这些有时是相反的目标。

我想告诉我的妻子,我的办公桌经过优化,可以增加。这只是一堆,添加内容非常容易。如果我为检索进行了优化,我的妻子会更喜欢它,例如,对我的东西进行一些整理,以便我可以找到东西。当然,这使得添加起来更加困难。

软件是相同的方式。您当然可以为产品创建进行优化- 尽快生成大量的整体代码,而不必担心组织它。正如您已经注意到的那样,这可能非常非常快。替代方案是优化维护-使创建过程更加困难,但使修改更容易或风险更低。这就是结构化代码的目的。

我建议一个成功的软件产品只能创建一次,却可以修改很多次。经验丰富的工程师已经看到,非结构化代码库会自行生存,成为产品,其规模和复杂性都会不断增长,直到即使不进行巨大风险也很难进行很小的更改。如果代码是结构化的,则可以控制风险。这就是为什么我们要处理所有这些麻烦。

复杂性来自关系,而不是要素

我在您的分析中注意到,您正在查看数量-代码量,类数等。虽然这些很有趣,但真正的影响来自元素之间的关系,这些关系组合爆炸。例如,如果您有10个功能,而又不知道哪个取决于哪个,则您有90个可能的关系(依赖项)需要担心-十个功能中的每个都可能取决于其他九个功能中的任何一个,而9 x 10 =90。您可能不知道哪个函数会修改哪些变量或如何传递数据,因此编码人员在解决任何特定问题时会担心很多事情。相反,如果您有30个类别,但它们排列得很巧妙,则它们最多可以具有29个关系,例如,如果它们是分层的或成堆排列的。

这如何影响您的团队的吞吐量?好了,依赖更少了,问题更容易解决了;编码人员在进行更改时不必费神费力。因此,最小化依赖关系可以极大地提高您胜任推理问题的能力。这就是为什么我们将事物划分为类或模块,并尽可能严格地限制变量的范围,并使用SOLID原理的原因。


103
但是,我要指出的是,过多的类和间接寻址对理解NOR维护都无济于事。复杂的控制流(也称为回调地狱)也不会。
Matthieu M.

9
@JohnWu这种解释是我阅读过的最好,最容易理解的方法。
Adriano Repetti

13
写得很好,但是为什么“一次创建但修改了很多次”为何重要呢?(如果所有代码都EntityTransformationServiceImpl包含在内,则必须先了解整个过程,然后才能对其进行修复-但是,例如,如果格式存在问题并且Formatter类可以解决该问题,则只需了解该部分的工作原理。在大约3个月后阅读自己的代码就像阅读一个陌生人的代码。)我觉得我们大多数人下意识地想到了这一点,但这可能是因为经验。对此不是100%肯定。
R. Schmitz

17
对于组合,我将使用完整的10×10 = 100:这些函数很可能是递归的,并且在特别糟糕的代码中,显然不一定如此。
KRyan

32
“替代方案是优化维护-使创建工作变得更加困难,但使修改更容易或风险更低。这就是结构化代码的目的。” 我对隐含的观念表示怀疑,即更多的类=更多的可维护性。尤其是,将逻辑分散在许多类上通常会使很难在代码中找到总体逻辑概念(尤其是没有非常有意识地确保概念非常清晰可见),这极大地降低了可维护性。
jpmc26

66

好吧,首先,易读性和可维护性通常在情人眼中。

您可读的内容可能不是您的邻居。

可维护性通常归结为可发现性(在代码库中发现行为或概念的难易程度),可发现性是另一种主观的东西。

DDD

DDD帮助开发人员团队的方法之一是建议一种组织代码概念和行为的特定方法(但仍是主观的)。此约定使发现事情变得更加容易,因此更易于维护应用程序。

  • 领域概念被编码为实体和集合
  • 域行为驻留在实体或域服务中
  • 聚合根确保了一致性
  • 持久性问题由存储库处理

这种安排从客观上来说并不容易维护。它,然而,可测量更容易,当每个人都了解他们在DDD情境中操作维护。

班级

类是可维护性,可读性,可发现性等方面的帮助,因为它们是众所周知的约定

在面向对象的设置中,类通常用于对密切相关的行为进行分组并封装需要仔细控制的状态。

我知道这听起来很抽象,但是您可以这样考虑:

使用类,您不必知道它们中的代码是如何工作的。您只需要知道班级负责什么

通过类,您可以根据定义良好的组件之间的交互作用来推理应用程序。

这可以减少推理应用程序时的认知负担。不必记住600行代码可以完成什么,您可以考虑30个组件如何交互。

而且,考虑到这30个组件可能跨越了应用程序的3层,您可能每个人一次只需要推理大约10个组件。

这似乎很容易处理。

摘要

本质上,您所看到的高级开发人员所做的是:

他们将应用程序分解为易于推理的类。

然后,他们将它们组织成易于推理的层次。

他们之所以这样做,是因为他们知道,随着应用程序的增长,从整体上对其进行推理变得越来越困难。将其分解为“层和类”意味着它们不必对整个应用程序进行推理。他们只需要推理其中的一小部分。


5
此外,较小的类更易于替换/重构。
Zalomon

12
过度设计并不能提供更具可维护性或可理解性的代码-相反,尽管您主张如此。谁需要一个AddressServiceRepositoryProxyInjectorResolver您何时可以更优雅地解决问题?为了设计模式而设计模式只会导致不必要的复杂性和冗长。
vikingsteve

27
是的,过度设计是不好的。杀死幼犬也是如此。这里没有人提倡任何一个。 logicallyfallacious.com/tools/lp/Bo/LogicalFallacies/169/...
MetaFight

5
同样,软件工程环境中的“优雅”通常是一个狡猾的词。 en.wikipedia.org/wiki/Weasel_word
MetaFight

11
对于任何组织代码的方法都可以这样说。一切都取决于维护团队,以了解为什么按原样组织代码。这就是使用公认的和易于理解的约定的全部意义。
MetaFight

29

请向我解释一下,为什么我们需要这种DDD样式和许多模式?

首先,请注意:DDD的重要部分不是模式,而是开发工作与业务的一致性。格雷格•扬(Greg Young)表示,蓝皮书的章节顺序错误

但是对于您的特定问题:类的数量往往比您预期的要多,(a)因为要努力将域行为与管道区别开来,(b)因为要付出额外的努力来确保概念在领域模型中明确表示。

坦率地说,如果您在域中有两个不同的概念,那么即使它们恰好在内存表示中共享相同的概念,它们在模型中应该是不同的。

实际上,您正在构建一种领域特定的语言,该语言以业务语言描述您的模型,从而使领域专家应该能够查看它并发现错误。

此外,您会发现更多关注点是关注点分离。以及将某些功能的使用者与实现细节隔离开来的概念。参见DL Parnas。清晰的边界使您能够更改或扩展实施,而不会在整个解决方案中造成影响。

这里的动机是:对于作为企业核心竞争力的一部分的应用程序(意味着它可以产生竞争优势的地方),您将希望能够轻松,廉价地替换具有更好变化的域行为。实际上,您有部分程序要快速发展(状态如何随时间变化),而其他部分则要缓慢变化(状态如何存储)。额外的抽象层有助于避免无意间将彼此耦合。

公平地讲:其中一些也是面向对象的脑损伤。Evans最初描述的模式基于他15年前参与的Java项目。状态和行为紧密地联系在一起,从而导致您可能宁愿避免的并发症;请参见Stuart Halloway的《感知与行动》或John Carmack的《内联代码》

无论您使用哪种语言,以功能风格进行编程都会带来好处。您应该在方便时执行此操作,而在不方便时应认真考虑该决定。 卡马克,2012年


2
在阅读本文之前,我将添加自己的答案。我将在第一段中强调,其目的是在软件中对业务概念进行建模,以使软件与业务需求保持一致。公平地讲,驱动项目的分析人员还应说明交付驱动程序所需的时间,并相应地调整代码/测试质量或完整性。
Sentinel

1
约翰·卡马克(John Carmack)是IMO的一个很好的例子,因为他以编写干净且易于理解的代码而著称,同时还表现出色。当您使用先进的技术跟踪他的代码时,这一点尤其明显-具有更好的CPU和内存,您可以看到很多事情,从“这需要非常简洁”变为“这需要非常清晰/隔离/ ...” ”。我知道的最实用的程序员之一:)
Luaan

我认为这是最好的答案。尽管我绝不出售DDD,但该答案至少可以解释其真正含义和动机,而不是诉诸于普通的陈词滥调。
jpmc26

29

其他答案中有很多优点,但我认为它们错过或不强调您犯的重要概念错误:

您正在比较了解整个程序的工作量。

对于大多数程序来说,这不是一个现实的任务。即使简单的程序也包含大量代码,以至在任何给定时间都不可能在头上管理所有这些代码。您唯一的机会是找到与手头任务相关的程序部分(修复错误,实现新功能)并进行处理。

如果您的程序包含庞大的函数/方法/类,这几乎是不可能的。您必须了解数百行代码,才能确定这部分代码是否与您的问题有关。根据您给出的估计,花费一周的时间来查找所需的代码变得很容易。

将此代码与具有小的函数/方法/类的代码库进行比较,这些函数/方法/类被命名并组织到程序包/名称空间中,从而可以很明显地找到/放置给定的逻辑。如果在许多情况下正确完成操作,您可以直接跳到正确的位置来解决问题,或者至少跳到调试器启动将使您跳到正确位置的位置。

我曾在两种系统中工作过。对于可比较的任务和可比较的系统大小,性能上的差异很容易达到两个数量级。

这对其他活动有影响:

  • 使用较小的单元,测试变得更加容易
  • 更少的合并冲突,因为两个开发人员处理同一段代码的机会较小。
  • 减少重复,因为更容易重用片段(并首先找到片段)。

19

因为测试代码比编写代码难

从开发人员的角度来看,许多答案都给出了充分的理由-可以减少维护,但首先要使代码更费力地写。

但是,还有一个方面需要考虑- 测试只能作为原始代码进行细化。

如果将所有内容都写成一个整体,那么唯一可以写的有效测试就是“给定这些输入,输出正确吗?”。这意味着发现的所有错误都限于“在那堆巨大的代码中的某个地方”。

当然,然后,您可以让开发人员坐在调试器中,找到问题的根源并进行修复。这会占用大量资源,并且浪费了开发人员的时间。想象一下,您遇到的每一个小错误都会导致开发人员需要再次调试整个程序。

解决方案:许多较小的测试,每个测试都可以确定一个特定的潜在故障。

这些小型测试(例如单元测试)具有检查代码库特定区域并帮助发现有限范围内的错误的优势。这不仅可以在测试失败时加快调试速度,而且还意味着,如果所有小型测试都失败了,那么您可以在大型测试中更轻松地找到失败(即,如果它不在特定的测试功能中,则必须在交互中实现)它们之间)。

应当清楚,进行较小的测试意味着您的代码库需要拆分为较小的可测试块。在大型商业代码库上,这样做的方法通常会导致代码看起来像您正在使用的代码。

只是一个注:这并不是说人们不会“太过分”。但是有合理的理由将代码库分成较小/较少连接的部分-如果明智的话。


5
尽管您的答案仅涉及测试和可测试性,但这是最重要的单个问题,其他一些答案则完全忽略,因此为+1。
Skomisa

17

请向我解释一下,为什么我们需要这种DDD样式和许多模式?

我们中的很多人(大多数...)真的不需要它们。理论学家和非常有经验的程序员根据大量研究和丰富的经验编写了有关理论和方法的书,这并不意味着他们编写的所有内容都适用于每个程序员的日常实践。

作为初级开发人员,最好阅读您提到的一本书,以扩大您的观点,并使您了解某些问题。当您的高级同事使用您不熟悉的术语时,这还可以避免您感到尴尬和困惑。如果您发现一些非常困难的东西,并且似乎没有任何意义或有用,请不要为之沉迷-只需将其归档在脑海中,就可以想到存在这样的概念或方法。

在您的日常开发中,除非您是一名学者,否则您的工作就是找到可行,可维护的解决方案。如果您在书中发现的想法不能帮助您实现该目标,那么只要您的工作令人满意,就不要担心。

可能会有一段时间,您会发现您可以使用所读到的内容,但一开始并没有完全“了解”,也许没有。


12
从纯粹的角度来看,这是正确的,听起来像DDD,其他模式只是理论上的。他们不是。它们是实现可读和可维护代码的重要工具。
詹斯·肖德

9
@PeteKirkham OP的困境是过度使用设计模式。您是否真的需要一个AccountServiceFacadeInjectResolver(我刚刚在工作中的系统中找到的真实示例)-答案很可能不是。
vikingsteve

4
@vikingsteve OP不是评论设计模式普遍过度使用的编程大师。OP是一个学徒,认为他们在他的商店中被滥用。我知道,初学者对我今天编写的代码也会有同样的想法,但是初学者有很多个人项目失败,因为它们最终变得太复杂了。
R. Schmitz

3
@ R.Schmitz那就是说,初学者我有很多个人项目失败,因为他们太过努力使事情设计良好,组织合理,结构化,面向对象...这些都是工具。他们需要被正确理解和使用,您几乎不能责怪锤子拧不紧。令人惊讶的是,要理解这一简单的事情似乎需要大量的见识和经验:)
Luaan

3
@Luaan如果您已经关心设计良好,组织有序,结构化的OOP,我几乎不称呼它为初学者-但过度使用是不好的。但是,问题更多是关于OP如何真正不了解这些问题解决了哪些问题。如果您不知道原因,似乎没有原因-但实际上,您还不知道。
R. Schmitz

8

由于您的问题涉及很多领域,有很多假设,因此我将提出您问题的主题:

为什么我们在设计模式中需要这么多的类

我们不。没有公认的规则说设计模式中必须有许多类。

有两个主要指南可用于确定将代码放置在何处以及如何将任务切成不同的代码单元:

  • 内聚性:任何代码单元(包,文件,类或方法)都应该属于同一类。即,任何特定的方法都应该完成一项任务,并且做到这一点。任何课程都应负责一个较大的主题(无论可能是什么)。我们想要高凝聚力。
  • 耦合:任何两个代码单元之间的依赖关系应尽可能少-特别是不应存在循环依赖关系。我们想要低耦合。

为什么这两个很重要?

  • 内聚性:一种可以完成很多事情的方法(例如,在很长的一堆代码中就可以完成GUI,逻辑,数据库访问等的老式CGI脚本)变得笨拙。在撰写本文时,很容易将您的思路付诸实践。这行得通,很容易演示,您可以完成。麻烦稍后出现:几个月后,您可能会忘记所做的事情。顶部的一行代码可能与底部的一行有几个屏幕。很容易忘记所有细节。方法中任何位置的任何更改都可能破坏复杂事物的任何行为。重构或重用该方法的某些部分非常麻烦。等等。
  • 耦合:每次更改代码单元时,都有可能破坏依赖于它的所有其他单元。在像Java这样的严格语言中,您可能会在编译时得到提示(即,有关参数,声明的异常等)。但是许多变化并不会触发这种变化(即行为变化),而其他更具动态性的语言则没有这种可能性。耦合度越高,更改任何内容的难度就越大,您可能会停顿下来,在这种情况下,必须完全重写才能实现某些目标。

这两个方面是任何编程语言和范式(不仅是OO)中任何“将内容放置在何处”选择的基础“驱动程序”。并不是每个人都清楚地意识到它们,而且要花很多时间(通常是数年)才能真正了解这些因素如何影响软件。

显然,这两个概念并没有告诉您实际要做什么。有些人会犯错误,而有些人会犯错误。某些语言(在这里是Java,在您看来是Java)倾向于使用许多类,因为该语言本身具有极其静态和腐的特性(这不是值声明,而是它的本质)。当您将其与动态和更具表达能力的语言(例如Ruby)进行比较时,这一点尤其明显。

另一个方面是,有些人订阅只写代码是必要的敏捷方法,现在,和重后,当需要重构。在这种开发方式中,interface当您只有一个实现类时就不会创建一个。您只需实现具体类。如果以后需要第二类,则可以重构。

有些人根本不这样工作。它们为任何可以更普遍使用的东西创建接口(或更一般地,抽象基类)。这很快导致班级爆炸。

再次,有赞成和反对的论据,无论是我还是您更喜欢哪个。在您作为软件开发人员的一生中,您将遇到各种极端情况,从漫长的意大利细面条方法到开明的,足够大的类设计,再到令人难以置信的过度设计的过度计划。随着经验的增加,您将越来越多地担任“建筑”角色,并可以朝着您希望的方向开始对此产生影响。您会发现自己的黄金中间点,无论您做什么,仍然会发现很多人不同意您的看法。

因此,在这里保持开放的态度是最重要的一点,这将是我对您的主要建议,因为从您的其余问题来看,您似乎对这个问题感到非常痛苦...


7

经验丰富的编码人员已学会:

  • 因为那些较小的程序和分类开始增长,尤其是成功的程序和分类。在简单级别上起作用的简单模式无法扩展。
  • 因为每次添加/更改都必须添加/更改多个工件似乎很麻烦,但是您知道要添加的内容并且这样做很容易。3分钟的打字比3小时的巧妙编码节拍。
  • 很多小类不是“混乱”,只是因为您不了解为什么开销实际上是一个好主意
  • 如果没有添加领域知识,“对我而言显而易见”的代码通常对我的队友来说是神秘的……再加上对我的未来。
  • 部族的知识可能使项目难以置信,难以使团队成员容易地增加团队成员的工作效率。
  • 命名是计算中仍然存在的两个难题之一,许多类和方法通常是命名中的一个激烈的练习,许多人发现这是一个很大的好处。

5
但是开销是否必要?在我看到的许多情况下,设计模式被过度使用。过于复杂的结果是增加了理解,维护,测试和调试代码的难度。当您围绕一个简单的服务类拥有12个类和4个maven模块时(我刚刚看到的一个真实示例),它很快变得比您想象的要难管理。
vikingsteve

4
@vikingsteve您一直在引用一个有缺陷的实现的示例,希望它能证明结构化代码通常是个坏主意。那只是不追踪。
MetaFight

2
@MetaFight OP不在谈论结构代码。他说的是太多的抽象。我认为,如果您有太多的抽象,您很快就会得到非常非结构化的代码和很多几乎相同的代码,这仅仅是因为人们无法应付他们必须处理的大量工作。
清晰的

4
@Clearer我很确定这里的每个人都同意,结构化代码的假设错误使用是一件坏事。OP说他们是初级。这就是为什么人们认为他/她不熟悉结构化代码的优点并重申它们的原因。
MetaFight

2

到目前为止,所有答案都是好的,都是从合理的假设开始的,即询问者遗漏了一些东西,而询问者也承认这一点。质询者也有可能基本正确,值得讨论这种情况如何发生。

经验和实践是有力的,如果前辈在大型,复杂的项目中获得了经验,而只有这样才能使事情得到控制EntityTransformationServiceImpl,所以他们很快就会习惯设计模式并严格遵守DDD。使用轻量级方法,即使对于小型程序,它们的效率也将大大降低。作为一个奇怪的人,你应该适应它,这将是一个很棒的学习经验。

在进行调整的同时,您应该将其作为在深度学习单一方法,可以使其在任何地方都可以工作,保持通用性以及知道哪些工具可用而不必成为任何一种专家的平衡之间的平衡。两者都有优势,世界都需要。


-4

按用途使用更多的类和函数将是一种以特定方式解决问题的好方法,该方法有助于将来解决任何问题。

多个类有助于确定其基本工作,并且可以随时调用任何类。

但是,如果您从它们的可获取名称中获得了许多类和函数,则它们很容易调用和管理。这称为干净代码。


9
对不起,你在说什么呢?
Deduplicator

@Deduplicator让我们以Java为例,如果我们设计使用jframes线程和类进行继承。从仅一个类中,我们就无法使用Java的某些作品进行设计,因此我们需要创建更多类
Sanjeev Chauhan

2
该答案需要表达其意图以更清晰的英语表达的想法。答案的核心思想似乎是,每个小类都有一个明确定义的功能,这是一个好主意,但是可怕的语法几乎使它无法理解。同样,仅凭此断言可能还不够。
talonx
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.