我们应该避免在不断变化的项目中使用设计模式吗?


32

我的一个朋友正在一家小公司工作,每个开发人员都讨厌这个项目:他被迫尽快发布,他是唯一一个关心技术债务,客户没有技术背景的人。

他给我讲了一个故事,使我想到了这样的项目中设计模式的适当性。这是故事。

我们不得不在网站的不同位置展示产品。例如,内容管理者可以通过API查看产品,还可以查看最终用户或合作伙伴。

有时,产品中缺少信息:例如,刚创建产品时,其中一堆没有任何价格,但尚未指定价格。有些没有描述(描述是具有修改历史,本地化内容等的复杂对象)。一些缺乏货运信息。

受到最近关于设计模式的阅读的启发,我认为这是使用神奇的Null Object模式的绝佳机会。所以我做到了,一切都变得顺畅而干净。只需致电product.Price.ToString("c")显示价格或product.Description.Current显示说明即可;不需要有条件的东西。直到一天,涉众要求通过使用nullJSON 来以不同的方式在API中显示它。对于内容管理者,也显示“未指定价格[更改]”,这也有所不同。而且我不得不谋杀我心爱的Null Object模式,因为不再需要它了。

同样,我不得不删除一些抽象工厂和一些建筑商,最终我通过直接和丑陋的调用替换了我美丽的Facade模式,因为底层接口在三个月中每天两次更改,甚至Singleton也离开了我当需求告知相关对象必须根据上下文而不同时。

超过三周的工作包括添加设计模式,然后在一个月后将其撕裂,我的代码最终变得面目全非,甚至包括我自己在内的任何人都无法维护。最好首先不要使用这些模式,这会更好吗?

确实,我必须在那些要求不断变化的项目类型上工作,而这些项目实际上是由那些并不真正考虑产品的凝聚力或一致性的人所决定的。在这种情况下,无论您有多敏捷,都将为问题提供一个优雅的解决方案,当最终实现该问题时,您会发现需求发生了巨大变化,以致于您的优雅解决方案不适合不再。

在这种情况下,解决方案是什么?

  • 是否不使用任何设计模式,停止思考并直接编写代码?

    进行一次团队直接编写代码,而另一个团队在打字之前三思而后行的经历会很有趣,这冒着几天后不得不扔掉原始设计的风险:谁知道,也许两个团队都会拥有同样的技术债务。如果没有这样的数据,我只断言,它不觉得不对劲,恕不另行思维上有20人月的项目工作时输入代码。

  • 保留不再有意义的设计模式,并尝试为新创建的情况添加更多模式?

    这似乎也不正确。模式用于简化对代码的理解;放置过多的模式,代码将变得一团糟。

  • 开始考虑包含新要求的新设计,然后慢慢将旧设计重构为新设计?

    作为一名理论家和偏爱敏捷的人,我完全投入其中。在实践中,当您知道必须每周回到白板并重做以前的设计的大部分内容时,并且客户只是没有足够的资金来支付您的费用,也没有足够的时间等待,这可能行不通。

那么,有什么建议吗?


43
我认为这是一个错误的困境。您的朋友自己承认,该代码现在已成为无法维持的意大利面。这不是软件模式的错;您的朋友无法以增加可维护性而不是降低可维护性的方式正确使用这些模式当我能提出具体示例时,我将发布一个正确的答案。
罗伯特·哈维

5
同样,FWIW,任何对漂移成本没有一定容忍度的客户都可能不应该采用敏捷方法,除非在转变需求的成本中内置了津贴。
罗伯特·哈维

5
我怀疑如果没有设计模式,代码将很快变得无法维护
Steven A. Lowe 2014年

28
这个问题没有任何意义。“避免设计模式”的唯一方法是完全不编写任何软件。诸如“ XYZ模式”之类的名称只是通用编码策略的名称,使我们的程序员可以更方便地相互交流有关我们的代码结构和选择的信息和建议。您代码中的任何设计选择都可以命名,并称为“设计模式”,尽管不一定是众所周知的设计模式(除非,我想,您为自己的独特设计感到自豪,并有足够的动机为其命名和博客,它或某物)。
詹森·C

9
即,您可以避免将脚称为“脚”,但是您仍然拥有那只脚,如果您不称其为“脚”,那么与某人谈论它会更加困难。您可能会认为自己是在“避免使用设计模式”,但是如果您提出了一个不错的设计,然后退后一步看看,您可能会发现您的设计最终还是适合一种常见的命名模式,不管你是否这样称呼。
詹森C

Answers:


86

我在这个问题中看到一些错误的假设:

  • 具有设计模式的代码,尽管正确应用,但比没有那些模式的代码需要更多的时间来实现。

设计模式本身并不是目的,它们应该为您服务,反之亦然。如果一个设计模式不能够更方便的代码来实现,或者好至少演化(这意味着:更容易适应不断变化的需求),那么该模式错过它的目的。当它们不能使团队的“生活”变得轻松时,请不要应用模式。如果新的Null对象模式在您的朋友使用它时一直在使用,那么一切都很好。如果以后要消除它,那也可以。如果Null对象模式减慢了(正确)实现的速度,则其用法是错误的。请注意,到目前为止,还不能从故事的这一部分得出任何导致“意大利面条式代码”的原因。

  • 归咎于客户,因为他没有技术背景并且不关心产品的凝聚力或一致性

那既不是他的工作,也不是他的错!您的工作是关心凝聚力和连贯性。当需求每天更改两次时,您的解决方案不应是牺牲代码质量。只需告诉客户需要多长时间,如果您认为需要更多时间来使设计“正确”,则可以在任何估计中增加足够大的安全裕度。尤其是当您有客户想要给您施加压力时,请使用“斯科蒂原理”。而且,当与非技术客户争论有关工作时,请避免使用诸如“重构”,“单元测试”,“设计模式”或“代码文档”之类的术语,这些术语是他不理解的,可能被认为是“不必要的”废话”,因为他认为其中没有任何价值。 或至少对客户可以理解的(功能,子功能,行为更改,用户文档,错误修复,性能优化等)。

  • 快速变化的需求的解决方案是快速更改代码

老实说,如果“底层接口在三个月内每天两次更改”,则解决方案不应是每天两次更改代码来做出反应。真正的解决方案是询问为什么需求如此频繁地更改,以及是否有可能在流程的这一部分进行更改。也许进行一些前期分析会有所帮助。界面可能太宽,因为组件之间的边界选择错误。有时,它有助于寻求更多信息,以了解需求的哪些部分是稳定的,哪些仍在讨论中(并实际上推迟了所讨论事物的实施)。有时,有些人只是因为一天两次不改变主意而被“踢倒了”。


30
+1-您无法通过技术解决方案解决人员问题。
Telastyn 2014年

1
+1,但“或至少具有更好的可扩展性(这意味着:更容易适应不断变化的需求)”-我可以用合理变化的需求来对此进行限定,对吗?
Fuhrmanator 2014年

1
@Fuhrmanator:我认为这很难用一般术语来讨论。恕我直言,很明显,当您的第一个要求是“我们需要文字处理程序”并且更改为“我们需要飞行模拟器”时,没有任何设计模式可以为您提供帮助。对于不太剧烈的需求变更,要决定什么将帮助您保持软件的不断发展并不总是那么容易。最好的事情是恕我直言,不要使用太多的模式,而是要应用一些原则-主要是SOLID原则和YAGNI。我同意Telastyn的100%的建议,当需求变化太多时,这可能不是技术问题。
布朗

4
+1-“老实说,如果“基础接口每天两次更改,持续三个月”,那么解决方案不应是每天两次更改代码来做出反应。真正的解决方案是问为什么需求如此频繁地更改,以及可以在该过程的那部分进行更改。 “如果不断给您提供新的指导,最好与所有利益相关者坐下来,重申您的期望。找出您的差异,并希望不要通过给项目一个更清晰的目标而浪费每个人的时间和金钱。
krillgar

1
@Cornelius Doc Brown说,没有具体性很难。从文字处理器到飞行模拟器的要求是不合理的;没有设计模式会有所帮助。在他的示例之间有很多灰色区域,例如,在文字处理器的Save函数中添加了新的文件格式(这是非常合理的)。没有细节,很难讨论。另外,这并不是说不想改变。如果您已经根据需求尽早做出了设计选择,那么进行此类更改就很困难。文字处理程序vs.飞行模拟是早期选择的一个很好的例子。
Fuhrmanator 2014年

43

我的拙见是,您不应该避免或不避免使用设计模式。

设计模式只是众所周知的,针对一般问题的值得信赖的解决方案,已被命名。它们在技术上与您能想到的任何其他解决方案或设计没有什么不同。

我认为问题的根源可能是您的朋友从“应用或不应用设计模式”的角度思考,而不是“我能想到的最佳解决方案是什么,包括但不限于模式”我知道”。

也许这种方法使他在不属于他们的地方以部分人为或强迫的方式使用了模式。这就是造成混乱的原因。


13
+1 “设计模式是解决一般问题的众所周知且受信任的解决方案,这些解决方案已获得名称。它们在技术上与您能想到的任何其他解决方案或设计没有什么不同。” 究竟。人们被命名的设计模式所困扰,以至于他们忘记了这些,无非就是赋予各种策略的名称,从而使我们的程序员可以更轻松地就我们的代码和设计选择进行交流。这种态度是非常大的混乱-往往试图在没有一定有利于问题迫使不适当的“模式”相关联。
杰森C

14

在您使用Null Object模式的示例中,我相信它最终失败了,因为它满足了程序员的需求,而不是客户的需求。客户需要以适合于上下文的形式显示价格。程序员需要简化一些显示代码。

因此,当设计模式不符合要求时,我们是说所有设计模式都是在浪费时间,还是说我们需要其他设计模式?


9

看起来错误是更多的是删除模式对象,而不是使用它们。在最初的设计中,Null对象似乎为问题提供了解决方案。这可能不是最佳解决方案。

作为项目的唯一人员,您将有机会体验整个开发过程。最大的缺点是没有人担任您的导师。花时间学习和应用最佳或更好的做法很可能会很快得到回报。诀窍是确定何时学习哪种练习。

以product.Price.toString('c')形式链接引用违反了Demeter律。我已经看到了这种做法的各种问题,其中许多与null有关。诸如product.displayPrice('c')之类的方法可以在内部处理空价格。同样,product.Description.Current可以由product.displayDescription(),product.displayCurrentDescription()处理。或product.diplay('Current')。

处理经理和内容提供者的新要求需要通过响应上下文来处理。可以使用多种方法。工厂方法可以使用不同的产品类别,具体取决于将显示给它们的用户类别。对于产品类显示方法,另一种方法是为不同的用户构造不同的数据。

好消息是您的朋友意识到事情已经失控。希望他拥有修订控制中的代码。这将使他能够回避自己将始终做出的错误决定。学习的一部分是尝试不同的方法,其中一些方法会失败。如果他可以应付接下来的几个月,他可能会找到简化生活并清理意大利面的方法。他可以尝试每周修复一件事。


2
考虑到您“必须”违反Demeter定律,它还可以进一步表明表面使用的模型不合适。为什么“视图模型”(以宽松的方式使用)不仅仅具有描述要显示?(即,为什么在UI级别上不只是当前描述?)业务层可以根据UI层是否是管理者,为UI层准备适当填充的对象,该对象已经具有不同的内容。
Cornelius

7

这个问题在很多方面似乎是错误的。但是公然的是:

  • 对于您提到的Null对象模式,在更改需求之后,您需要更改一些代码。很好,但这并不意味着您“谋杀”了空对象模式(顺便说一句,请谨慎使用措辞,这听起来太极端了,有些人过于偏执根本不会觉得这很有趣)。

许多人正确地说,设计模式主要是关于标记和命名一种常见实践。因此,请考虑一下一件衬衫,一件衬衫有一个领子,由于某种原因,您要卸下领子或部分领子。命名和标签发生了变化,但本质上仍是一件衬衫。这里就是这种情况,细节上的细微变化并不意味着您已经“谋杀”了这种模式。(再次记住极端措辞)

  • 您谈论的设计是不好的,因为当需求变更只是次要的事情时,您会在各处进行大规模的战略设计变更。除非高层业务问题发生变化,否则您无法证明进行大规模设计更改是合理的。

根据我的经验,当次要需求到来时,您只需要更改代码库的一小部分。有些可能有点骇人听闻,但没有什么太严重的问题足以实质性地影响可维护性或可读性,并且通常有几句话可以解释骇客部分。这也是非常普遍的做法。


7

让我们暂停片刻,然后看一下这里的基本问题- 架构系统时,架构模型与系统中的低级功能过于耦合,从而导致架构在开发过程中频繁中断。

我认为我们必须记住,必须在适当的层次上使用与之相关的体系结构和设计模式,并且对什么是正确的层次进行分析并非无关紧要。一方面,您可能仅通过非常基本的约束(例如“ MVC”之类)就可以轻松地将系统的体系结构保持在较高的级别,这可能导致错过明确的指南和代码利用方面的机会,并且意大利面条式代码可以轻松地在其中在所有自由空间中蓬勃发展。

另一方面,您可能会像过度设置系统架构一样,在将约束设置为详细级别时,假设您可以依赖现实中比预期更不稳定的约束,从而不断打破约束并迫使您不断进行改型和重建,直到您开始绝望。

对系统需求的更改将始终或多或少地存在。而且使用架构和设计模式的潜在好处将永远存在,因此,是否使用设计模式并不是一个真正的问题,而是应该在什么级别上使用它们。

这不仅要求您了解拟议系统的当前要求,而且还需要确定可以将其哪些方面视为系统的稳定核心属性,以及在开发过程中可能会更改哪些属性。

如果您发现自己经常需要与无组织的意大利面条代码作斗争,则可能是您没有做足够的架构,或者没有达到较高的水平。如果发现体系结构经常崩溃,则可能是您做的体系结构过于详细。

使用体系结构和设计模式并不是您可以“涂装”系统的东西,就像您要喷涂桌子一样。这些技术应经过深思熟虑地应用,在这种情况下,您需要依靠的约束很有可能保持稳定,而这些技术实际上值得为架构建模和实现实际约束/架构/模式带来麻烦作为代码。

关于过于详细的架构问题,您也可以在架构没有太大价值的情况下付出很多努力。请参阅风险驱动的体系结构以供参考,我喜欢这本书- 足够的软件体系结构,也许您也会喜欢。

编辑

澄清我的答案,因为我意识到我经常表示自己是“太多的体系结构”,而我真正的意思是“太详细的体系结构”,这并不完全相同。过于详细的体系结构可能经常被视为“太多”的体系结构,但是即使您将体系结构保持在一个良好的水平上并创建人类有史以来最美丽的系统,如果优先级高的话,在体系结构上仍然可能会花费太多精力功能和上市时间。


+1这些是关于非常高水平的很好的想法,我认为您必须在系统中查看,但是就像您说的那样,它需要大量的软件设计经验。
塞缪尔

4

根据他的轶事,您的朋友似乎正面临着许多不利因素。那是不幸的,并且可能是一个非常艰苦的工作环境。尽管有困难,他仍在使用模式使自己的生活更轻松的正确道路上,而离开了那条道路实在可耻。意大利面条代码是最终结果。

由于存在两个不同的问题领域,即技术问题和人际问题,因此我将分别解决每个问题。

人际交往

您的朋友正面临着快速变化的需求而苦恼,这正如何影响他编写可维护代码的能力。我首先要说的是,每天如此长的时间内每天两次更改需求是一个更大的问题,并且具有不切实际的隐含期望。需求变化的速度超过了代码的变化速度。我们不能期望代码或程序员能跟上潮流。这种快速的变化步伐是更高层次上所需产品概念不完整的征兆。这是个问题。如果他们不知道自己真正想要的是什么,他们将浪费大量的时间和金钱,永远无法得到它。

为更改设置边界可能会很好。每两周将变更分组在一起,然后在实施期间将其冻结两周。在接下来的两个星期内建立新清单。我感到其中一些变化重叠或矛盾(例如,在两个选项之间来回摆动)。当变化迅速而激烈时,它们都是当务之急。如果您让他们积累到列表中,则可以与他们一起组织和确定最重要的事情,以最大程度地提高工作量和生产率。他们可能会发现他们的某些更改很愚蠢或不太重要,这给您的朋友提供了喘息的空间。

但是,这些问题不应阻止您编写好的代码。错误的代码会导致更严重的问题。从一种解决方案重构到另一种解决方案可能需要花费一些时间,但是有可能这样的事实表明,通过模式和原则,良好的编码实践会带来好处。

在频繁变化的环境中,技术债务在某个时候到期。与其付款,而不是先等它变得太大而无法克服,不如先付钱。如果模式不再有用,则将其重构,但不要再回到牛仔编码方式。

技术

您的朋友似乎对基本的设计模式有很好的了解。空对象是解决他所面临问题的好方法。实际上,这仍然是一个好方法。他似乎面临挑战的地方是理解模式背后的原理,原因。否则,我不相信他会放弃他的方法。

(下面是原始问题中并未要求的一组技术解决方案,但这些技术解决方案说明了我们如何能够遵循用于说明目的的模式。)

空对象背后的原理是封装变化的思想。我们隐藏了哪些更改,因此我们不必在其他任何地方都应对它。在这里,空对象将product.Price实例中的方差封装起来(我将其称为Price对象,空对象的价格为NullPrice)。Price是领域对象,一种业务概念。有时,根据我们的业务逻辑,我们还不知道价格。有时候是这样的。空对象的完美用例。Price有一个ToString输出价格的方法,如果不知道,则输出空字符串(或NullPrice#ToString返回一个空字符串)。这是合理的行为。然后需求就改变了。

我们必须将a输出null到API视图或将其他字符串输出到管理者视图。这如何影响我们的业务逻辑?好吧,事实并非如此。在以上声明中,我两次使用了“视图”一词。这个词可能没有明确说出来,但是我们必须训练自己以听取需求中隐藏的词。那么,“视图”为何如此重要?因为它告诉我们真正必须在哪里进行更改:在我们看来。

撇开:这里是否使用MVC框架无关紧要。虽然MVC对“视图”有非常特定的含义,但我将其用于一段演示代码的更一般(也许更适用)的含义。

因此,我们确实需要在视图中修复此问题。我们该怎么做?最简单的方法是if声明。我知道null对象旨在摆脱所有ifs,但我们必须务实。我们可以检查字符串是否为空,然后切换:

if(product.Price.ToString("c").Length == 0) { // one way of many
    writer.write("Price unspecified [Change]");
} else {
    writer.write(product.Price.ToString("c"));
}

这如何影响封装?这里最重要的部分是视图逻辑封装在视图中。这样,我们可以使业务逻辑/域对象与视图逻辑的变化完全隔离。很难看,但是可以用。但是,这不是唯一的选择。

我们可以说我们的业务逻辑发生了一点变化,因为如果没有设置价格,我们想输出默认字符串。我们可以对我们的Price#ToString方法进行一些细微的调整(实际上是创建一个重载的方法)。我们可以接受默认的返回值,如果未设置价格,则返回该值:

class Price {
    ...
    // A new ToString method
    public string ToString(string c, string default) {
        return ToString(c);
    }
    ...
}

class NullPrice {
    ...
    // A new ToString method
    public string ToString(string c, string default) {
        return default;
    }
    ...
}

现在,我们的视图代码变为:

writer.write(product.Price.ToString("c", "Price unspecified [Change]"));

条件不存在了。但是,这样做太多可能会使特殊情况下的方法激增到您的域对象中,因此,只有在只有少数情况下才有意义。

我们可以改用创建一个返回布尔值的IsSet方法Price

class Price {
    ...
    public bool IsSet() {
        return return true;
    }
    ...
}

class NullPrice {
    ...
    public bool IsSet() {
        return false;
    }
    ...
}

查看逻辑:

if(product.Price.IsSet()) {
    writer.write(product.Price.ToString("c"));
} else {
    writer.write("Price unspecified [Change]");
}

我们在视图中看到了有条件的返回,但是对于业务逻辑来说,确定价格是否已确定,情况就更强了。Price#IsSet现在我们可以在其他地方使用它了。

最后,我们可以封装将价格完全呈现在视图助手中的想法。这将隐藏条件,同时尽可能保留域对象:

class PriceStringHelper {
    public PriceStringHelper() {}

    public string PriceToString(Price price, string default) {
        if(price.IsSet()) { // or use string length to not change the Price class at all
           return price.ToString("c");
        } else {
            return default;
        }
    }
}

查看逻辑:

writer.write(new PriceStringHelper().PriceToString(product.Price, "Price unspecified [Change]"));

进行更改的方法还有很多(我们可以归纳PriceStringHelper为一个对象,如果字符串为空,则返回默认值),但是这些方法可以快速(大部分)保留模式原理,例如以及进行此类更改的实用方面。


3

如果原本应该解决的问题突然消失,那么设计模式的复杂性可能会给您带来麻烦。可悲的是,由于设计模式的热情和普及,这种风险很少被明确指出。朋友的轶事对显示模式如何不起作用大有帮助。Jeff Atwood 对这个话题有一些选择

记录需求中的变化点(它们是风险)

许多更复杂的设计模式(没有太多的零对象)包含受保护的变化的概念,即“确定预测的变化或不稳定性的点;分配职责以在它们周围创建稳定的接口”。适配器,访客,外观,层,观察者,策略,装饰器等都利用了这一原理。当需要在预期可变性的维度上扩展软件并且“稳定”的假设保持稳定时,它们会“得到回报”。

如果您的需求是如此不稳定,以至于您的“预测变化”总是错误的,那么您应用的模式将使您痛苦或充其量是不必要的复杂性。

克雷格·拉曼(Craig Larman)谈到了两种应用受保护的变体的机会:

  • 变更点 -在现有的当前系统或要求中,例如必须支持的多个接口,以及
  • 演变点 -现有需求中不存在的推测性变化点。

两者都应该由开发人员进行记录,但是您可能应该对变更点有客户的承诺。

为了管理风险,您可以说任何采用PV的设计模式都应追溯到客户签署的需求中的变化点。如果客户更改了需求的变更点,则您的设计可能必须进行根本性的变更(因为您可能会投资于设计[模式]以支持该变更)。无需解释内聚,耦合等。

例如,您的客户希望该软件与三种不同的旧库存系统一起使用。那是您设计的一个变体点。如果客户放弃了这一要求,那么您当然会拥有一堆无用的设计基础架构。客户需要知道变化点会花费一些钱。

CONSTANTS 在源代码中是PV的一种简单形式

您的问题的另一个类推是询问CONSTANTS在源代码中使用是否是一个好主意。参考此示例,假设客户放弃了密码需求。因此,MAX_PASSWORD_SIZE作为在代码中不断传播的代码将变得毫无用处,甚至成为维护和易读性的障碍。您会责怪使用CONSTANTS作为原因吗?


2

我认为这至少部分取决于您情况的性质。

您提到了不断变化的需求。如果客户说“我希望这个养蜂应用程序也可以与黄蜂一起工作”,那么这似乎是一种情况,在这种情况下,精心设计将有助于进步而不是阻碍进步(尤其是当您考虑到将来她可能希望也要保持果蝇。)

另一方面,如果更改的性质更像是“我希望这个养蜂应用程序来管理我的自助洗衣店集团的工资单”,那么没有任何代码可以使您摆脱困境。

设计模式本质上没有任何好处。它们是与其他工具一样的工具-我们仅在中长期使用它们来使我们的工作更轻松。如果使用其他工具(例如交流研究)更有用,那么我们可以使用它。

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.