避免使用的设计模式

111

许多人似乎都认为,单例模式存在一些缺点,有些人甚至建议完全避免使用该模式。在这里有一个很好的讨论。请向那个问题提出任何关于单例模式的评论。

我的问题:是否还有其他设计模式应该避免或者需要格外小心使用?


我只需要提醒你注意一下这个伟大的设计反模式列表 http://deviq.com/antipatterns/ - NoWar
@casperOne 为什么关闭了这个问题?这个问题是合法的。 - bleepzter
12个回答

152

Patterns are complex

所有的设计模式都应该谨慎使用。在我看来,您应该有一个有效的理由重构到模式中,而不是马上实现一种模式。使用模式的普遍问题在于它们增加了复杂性。过度使用模式会使给定的应用程序或系统在进一步开发和维护时变得繁琐。

大多数情况下,有一个简单的解决方案,你不需要应用任何特定的模式。一个好的经验法则是,在代码块需要经常被替换或需要经常更改时使用模式,并准备好接受使用模式时带来的复杂代码。

记住,您的目标应该是简单明了, 如果您在代码中看到需要支持更改实际需求,请使用模式。

原则胜于模式

如果模式显然会导致过度设计和复杂的解决方案,那么使用模式似乎是徒劳的。然而,对于程序员来说,阅读奠定大多数模式基础的设计技术和原则要比使用模式更有趣。事实上,我最喜欢的一本关于“设计模式”的书强调了这一点,重申了适用于相关模式的原则。与模式相关性相比,这些原则足够简单而有用。其中一些原则足以涵盖比面向对象编程(OOP)更广泛的范畴,例如里氏替换原则,只要你能构建代码模块。

有许多���计原则,但《设计模式》的第一章中所描述的原则是一个不错的起点。

  • 针对“接口”进行编程,而不是针对“实现”。(Gang of Four 1995:18)
  • 倡导“对象组合”而非“类继承”。(Gang of Four 1995:20)
让这些东西在你身上沉淀一段时间。需要注意的是,当GoF被编写时,接口意味着任何抽象的东西(也就是超类),不要与Java或C#中的类型接口混淆。第二个原则来自观察到的继承过度使用,可悲的是今天仍然很普遍
从那里,您可以阅读SOLID 原则,这是由Robert Cecil Martin (又名Uncle Bob)所知道的。Scott Hanselman在关于这些原则的播客中采访了Uncle Bob:
  • Single Responsibility Principle(单一职责原则)
  • Open Closed Principle(开放封闭原则)
  • Liskov Substitution Principle(里氏替换原则)
  • Interface Segregation Principle(接口隔离原则)
  • Dependency Inversion Principle(依赖倒置原则)
这些原则是一个很好的开始,可以与同行讨论和阅读。你可能会发现这些原则与关注点分离依赖注入等其他过程交织在一起。做了一段时间的测试驱动开发后,你也可能会发现这些原则在实践中自然而然地出现,因为你需要在某种程度上遵循它们以创建隔离可重复的单元测试。

7
非常好的答案。看起来今天的每位(新手)程序员都知道他们的设计模式,或者至少知道它们的存在。但是很多人从未听说过、更别提应用一些绝对必要的原则,比如单一职责原则,来管理他们代码的复杂性。 - eljenso

23

《设计模式》一书作者最担心的模式之一是"Visitor"(访问者)模式。

它是一种“必要的恶”,但常常被过度使用,需要使用它通常暴露出你的设计中更根本的缺陷。

"Visitor"模式的另一个名称是“多重派发”,因为当你希望使用单类型派发的面向对象语言基于两个或更多不同对象的类型选择要使用的代码时,就会得到Visitor模式。

其中经典的例子是两个形状的交集,但还有一个更简单的情况经常被忽略:比较两个异构对象的相等性。

总之,通常你会得到类似下面的东西:

interface IShape
{
    double intersectWith(Triangle t);
    double intersectWith(Rectangle r);
    double intersectWith(Circle c);
}

这样做的问题是将所有“IShape”的实现紧密耦合在一起。你暗示了,每当您想要将新形状添加到层次结构中时,您都需要更改所有其他“Shape”实现。
有时,这是正确的最小设计 - 但请考虑一下。您的设计是否真的需要在两种类型上进行分派?您愿意编写多方法的组合爆炸吗?
通常,通过引入另一个概念,您可以减少实际需要编写的组合数量:
interface IShape
{
    Area getArea();
}

class Area
{
    public double intersectWith(Area otherArea);
    ...
}

当然,这取决于情况 - 有时候你确实需要编写代码来处理所有这些不同的情况 - 但在采用访问者模式之前值得暂停和思考一下。这可能会在以后避免很多麻烦。


4
说到访问者,Bob叔叔“一直在使用它”。http://butunclebob.com/ArticleS.UncleBob.IuseVisitor - Spoike
4
@Paul Hollingsworth,您能提供一个引用来证明《设计模式》的作者们正在担心什么(以及他们为什么会感到担忧)吗? - Random42

16

单例模式 - 使用单例X类的依赖关系难以查看和难以分离进行测试。

它们经常被使用,因为它们方便且易于理解,但它们可能会使测试变得非常复杂。

参见单例是病态的骗子


1
它们还可以简化测试,因为它们可以为您提供一个单一的注入模拟对象的点。这完全取决于找到适当的平衡。 - Martin Brown
1
@Martin:如果你没有使用标准的单例实现,那么更改单例以进行测试当然是可能的,但这比在构造函数中传递测试实现更容易吗? - orip

14

我认为模板方法设计模式通常是一种非常危险的模式。

  • 很多时候,它会因为“错误的原因”而耗尽你的继承层次结构。
  • 基类往往会变得杂乱无章,包含各种不相关的代码。
  • 这迫使你在开发过程中往往早期锁定设计。(在许多情况下是过早的锁定)
  • 在以后的阶段更改将变得越来越困难。

2
我想补充一下,每当您使用模板方法时,最好使用策略模式。模板方法的问题在于基类和派生类之间存在重入,这通常过于耦合。 - Paul Hollingsworth
5
@Paul:当模板方法被正确使用时,即变化的部分需要了解不变的部分时,它非常棒。我的看法是,当基础代码仅调用自定义代码时,应该使用策略;而当自定义代码本质上需要了解基础代码时,则应该使用模板方法。 - dsimcha
是的,dsimcha,我同意......只要类设计师知道这一点。 - Paul Hollingsworth

9
我认为你不应该避免使用设计模式(DP),也不应该强迫自己在规划架构时使用DP。只有当它们自然地出现在我们的规划中,我们才应该使用DP。
如果我们从一开始就定义了要使用某个DP,那么我们未来的许多设计决策都将受到这个选择的影响,而没有保证我们选择的DP适合我们的需求。
我们还不应该将DP视为不可变的实体,而应该根据我们的需求调整模式。
因此,总结一下,我认为我们不应该避免DP,而是在它们已经在我们的架构中形成时拥抱它们。

7

这很简单...避免使用你不清楚或者你感觉不舒服的设计模式。

以下是一些例子...

有一些不实用的模式,例如:

  • 解释器
  • 享元

还有一些比较难以理解的模式,例如:

  • 抽象工厂 - 完整的抽象工厂模式和创建对象族的方式并不像看起来那么简单
  • 桥接 - 如果将抽象和实现分别划分到子树中,则可能会变得过于抽象,但在某些情况下,这是非常有用的模式
  • 访问者 - 必须真正理解双重调度机制

还有一些模式看起来非常简单,但由于与其原理或实现相关的各种原因,它们并不是很明显的选择

  • 单例 - 并不是完全不好的模式,只是被过度使用(经常出现在不适合的地方)
  • 观察者 - 很棒的模式...只是让代码变得更难阅读和调试
  • 原型 - 交换编译器检查的动态性(这可能是好事或坏事...取决于情况)
  • 责任链 - 太经常被强制/人为地推入设计中了

对于那些不实用的模式,在使用它们之前,你应该真正考虑一下,因为通常有更优雅的解决方案。

对于比较难以理解的模式,当它们在适当的地方使用并且实现得很好时,它们确实是非常有帮助的...但是如果使用不当,则会成为噩梦。

现在,下一步是什么...


享元模式是必须的,每当您多次使用资源(通常是图像)时。它不仅仅是一种模式,更是一种解决方案。 - Cengiz Kandemir

7
我认为Active Record是一种被过度使用的模式,它鼓励将业务逻辑与持久化代码混合在一起。它不能很好地隐藏存储实现细节,将模型层与数据库耦合在了一起。有许多替代方案(PoEAA中有描述),例如Table Data Gateway、Row Data Gateway和Data Mapper,它们通常提供更好的解决方案,并帮助提供更好的存储抽象。此外,您的模型不应该“需要”存储在数据库中;将它们存储为XML或使用Web服务访问它们怎么样?更改模型的存储机制会有多容易呢?
话虽如此,Active Record并非总是不好,对于较简单的应用程序来说,它是完美的选择,其他选项可能会过于复杂。

1
有一定的真实性,但在很大程度上取决于具体的实现方式。 - Mike Woodhouse

5

我希望我不会因此受到太多的打击。Christer Ericsson在他的实时碰撞检测博客中写了两篇关于设计模式的文章(one, two)。他的语气相当严厉,也许有点挑衅,但这个人知道他的东西,所以我不会把它看作是一个疯子的胡言乱语。


有趣的阅读材料。感谢提供链接! - Bombe
3
蠢人编写糟糕的代码。有模式的蠢人编写的代码是否比从未见过模式的蠢人编写的代码更糟糕?我认为不是。对于聪明人来说,模式提供了一个众所周知的词汇表,便于交流思想。解决方法:学习模式并只与聪明的程序员打交道。 - Martin Brown
我认为一个真正的白痴不可能使用任何工具制作出比这更糟糕的代码。 - 1800 INFORMATION
1
我认为他在大学考试的例子只证明了一件事,那就是对于自己所处领域的蔑视和不愿意花费超过一个周末几个小时来学习它的人,在尝试解决问题时会产生错误的答案。 - scriptocalypse

5

还有一点需要注意的是,有时候服务定位器是必要的。例如,在你无法对对象实例化进行适当控制时(例如在 C# 中使用非常量参数的属性)。但也可以在构造函数注入的情况下使用服务定位器。 - Sinaesthetic

2

我认为观察者模式有很多问题需要解决,它在一般情况下可以工作,但随着系统变得更加复杂,就会变成噩梦,需要 OnBefore()、OnAfter() 通知,并经常发布异步任务以避免重入。一个更好的解决方案是开发一种自动依赖分析系统,在计算过程中对所有对象访问(使用读取屏障)进行仪器化,并自动生成依赖图中的边缘。


4
我理解你的回答直到"A"这个词。 - 1800 INFORMATION
3
代理/事件是观察者模式的一种实现方式。 - orip
1
我个人对观察者模式的不满在于它可能会在垃圾回收语言中导致内存泄漏。当你完成一个对象的使用后,你需要记住这个对象不会被清理掉。 - Martin Brown
@orip:是的,这就是为什么要使用委托/事件的原因。 ;) - Spoike
@Spoike:我的建议解决方案非常难以实现,但只需完成一次。我们使用一个前端预处理器将对象引用转换为对“读取屏障”的调用,在该调用中,所涉及的对象会自动附加到框架中,并跟随实际的对象访问。当重新计算发生时,依赖图是基于脏状传播到图中的程度进行重新计算的。设置这样一个相当复杂的系统确实很困难,但是一旦设置完成,你就不必再担心观察者模式了!注意:如果您尝试,请务必进行彻底的测试。 - Jesse Pepper
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接