无用的设计模式-上篇

提到设计模式,有一个非常有意思的现象:
理论学习中,几乎所有的开发人员都认为它非常有用很重要。
工作实践中,绝大部分开发人员在项目中找不到合适的应用场景。
设计模式学了一遍又一遍,却毫无用武之地。大概设计模式最好的归宿,就是存在程序员的深深的脑海里。

难道设计模式真的没有用了吗?

关于本文

本文目的,通过对设计模式的本质进行探讨剖析,建立起更为高效的认知模式。最终可以灵活运用设计模式到日常工作中,产出稳定、高效、灵活的业务实现。

内容上分为上下两篇,上篇为理论篇,讲述了设计模式一些共识、原则的思考理解等。下篇为实践篇,通过一个完整的系统设计实例,对上篇的理论进行验证和实践。

一、设计模式到底是什么?

从因果规律来看,任何事物的发展与诞生,必然有其背景原因。想要摸清设计模式的底细,就要明白设计模式是怎么来的。

1.1 设计模式诞生背景

image

时间回到20世纪80年代,当时的软件行业正处于第二次软件危机中。根本原因是,随着软件规模和复杂度的快速增长,如何高效高质的构建和维护这样大规模的软件成为了一大难题。

软件复用被认为是解决这一危机的一条可行路径,而面向对象的思想则很好的解决了复用问题。设计模式正是在这样的背景下,伴随着面向对象编程的兴起出现的。

1.2 设计模式的前世

image

设计模式这个术语,其实并非源自于软件工程领域。它最早起源于建筑学领域,是由哈佛建筑学博士Alexander在1977年提出的。

他跟团队在研究了大量的建筑结构后发现,为了解决同一类建筑问题而设计出的高质量建筑结构具有某些共性,于是使用“模式语言”来描述它。

他定义每个模式均包含前提条件(适用场景)、目标问题、解决方案三个部分。

创建模式的目的是,复用那些已经实践成功的建筑解决方案。

1.3 设计模式的今生

image

1994年,以四人组(GoF)自称的四位资深软件工程学者,借鉴了这种思想,将模式的概念引入软件工程领域。GoF总结了23种在开发过程中使用频率较高的设计模式,合著出版了《Design Patterns - Elements of Reusable Object-Oriented Software》一书。设计模式正式诞生。

**软件领域的设计模式,本质上是基于面向对象设计经验的总结。**它不是解决所有的问题的银弹,更多的是一种解决问题的思路。

二、我们需要设计模式吗?

简单了解了设计模式后,对于文章开头的问题,你有答案了吗?设计模式真的没有用了吗?我们可以提出一些合理的猜测:

**设计模式所代表的经验已经过时了。**距设计模式的提出已经过去了20多年,软件领域已经有了长足的发展,有可能经验已经不适用了,所以日常开发中运用设计模式的场景比较少。

**业务软件开发不需要设计模式。**随着软件领域的发展,软件开发的分工,慢慢朝着细致化、精致化方向靠拢,技术本身开始独立于业务发展。我们用上了各种各样的功能强大的工具框架,填好模板代码就能完成业务,似乎业务已经不需要过多设计了。

接下来,我们围绕这两个问题一起讨论下。

2.1 经验之谈,依然有效

不妨先来回忆一下,我们接触到设计模式最多的地方是在哪里?

大部分应该都是在框架(工具类库等)源码里吧。那为什么在框架里会大量使用设计模式呢?

很多人会说,因为框架需要覆盖的场景多,不仅要考虑现有功能的持续迭代,还要考虑到后续可能的功能扩展。代码结构既要足够稳定,保持可维护性。又要有一定的灵活性,保持可扩展性。

所以,框架通常需要一个比较好的设计。而设计最终体现在代码中,就是对设计模式的应用。

这说明设计模式所代表的经验,依然是有效的。

2.2 业务需要设计模式

近10年来,软件生产效率有了巨大的提升。抛开硬件的因素,很大程度上是因为面向对象的普及,以及配套工具体系(例如框架)的完善。以至于在日常开发中,很多人会认为框架解决了所有问题,业务不过是CRUD。

所以在讨论业务需不需要设计模式之前,我们有必要先讨论下框架是否解决了业务开发的难点?

软件工程学上,将软件开发过程中的难题,分为本质困难和非本质困难。

本质困难是,如何抽象出实体,准确地描述现实业务中复杂的概念结构。非本质困难是,如何通过技术落地实现这些概念结构。

框架(编程语言也是)所解决的是非本质困难,是技术落地实现的效率问题。对于业务自身的复杂性这类本质困难没有帮助。

所以,业务上需要一些方法去对抗自身复杂性,以实现软件的可复用性、可维护性和可扩展性。而设计模式正是一种被验证过的有效方法。

当然,并不是所有业务都需要设计模式。如果业务复杂度在预期的时间段内是可控,可接受的,那么过度的设计,反而会降低软件的可维护性。

但是,在SaaS领域,业务复杂度的增速一般是非常快的,这跟SaaS软件的特性不无关系。回想这几年经历的SaaS行业,SaaS软件具备的区别于其他领域软件的几个明显特性:

  • 行业领域的专业性

  • 商家场景的多样性

  • 业务规则的不一致性

  • 个性需求的不确定性

  • 需求难以协调的刚性

这些特性,无一不在向我们表明着,SaaS软件是非常注重可维护性,可扩展性,甚至更多。

业务上我们是非常需要设计模式的。

三、怎么学习设计模式?

至此,我们已经探讨了一部分意识上的问题,下面我们探讨一些方法上的问题。如果追问工作中为什么没有应用设计模式的经验,归结其原因,分为这么两类:

  • 不会用,对设计模式不熟悉,不知道该怎么应用。

  • 用不上,业务中一直找不到合适的场景,不知道用在哪。

其原因在于,对设计模式的学习方法以及认知上存在偏差。例如,不知道设计模式的核心关注点在哪。或者认为设计模式都是割裂存在的,仅适用在单一场景下的解决方案的集合。

3.1 解构设计模式

设计模式的本质是经验的总结。我们可以对设计模式进行抽象,更精确地表达它:

image

  • 名称:模式名称是帮助我们理解记忆,方便沟通交流的标识。它是场景的简称,场景描述了问题产生的背景。

  • 问题:它是场景中想要达成的目标与现状之间的落差。通常一个模式中的问题,代表的是一类问题,不特指某一个具体的问题。

  • 方案:针对模式中的问题,存在已经被反复实践验证过的最佳解决方案。解决方案并不描述一个特定而具体的设计或实现,具有一定普适性。

那么你认为最应该关注设计模式的哪一部分?

**是名称吗?**每种设计模式的名字或者问题场景,你都非常清楚,但你可以熟练应用吗?

**是解决方案吗?**解决方案固然重要,但是如果你不清楚方案要解决的问题,结果只能是拔剑四顾心茫然。

如果将设计模式的关注点放在问题场景(名称)或者解决方案上,大脑就会驱动我们以场景为触发点,去匹配模式。

image

通常会出现以下问题:

  • 割裂看待各个模式,用熟悉的场景去套用模式

  • 有创建对象场景,立即会想到用工厂模式

  • 本地引用远程服务场景,想到代理模式

  • String/Integer常量池,线程池,连接池,想到享元模式

  • 全部心思都在解决方案本身上,陷入细节不可自拔,各种方案越看越迷茫

  • 适配器、装饰模式的区别

  • 策略、桥接模式也有相似之处

应用的场景数不胜数,场景背后的问题却是殊途同归。单个场景不难识别,但是实际业务开发中,往往是复合场景,识别难度大。这也是工作中难以应用设计模式的主要原因。

**是问题吗?**问题是模式存在的前提,从使用方的角度看,问题是模式的唯一使用标识。所以说问题才是设计模式的核心。

模式基于问题提出,问题依托于场景存在。以问题为出发点,去匹配模式。

image

一个场景中可能包含多个问题,以创建对象场景为例。我们使用 问题=>模式 的思路来分析下在这个过程中可能会遇到的问题:

  • 创建实例过程太复杂?

  • 为什么复杂?是依赖对象太多?有办法解耦吗?

  • 是对象本身属性多?可以按需初始化吗? [建造者模式]

  • 是要创建不同类型实例的逻辑太复杂?有办法将创建行为统一起来吗? [工厂模式]

  • 创建实例成本太高?

  • 为什么太高?有优化空间吗?复制对象替代创建新行为可行吗? [原型模式]

  • 延迟创建可以吗?单例可行吗? 实例共享可行吗? [单例、享元模式]

3.2 小结

一连串问题下来,你会发现,设计模式是伴随着问题的解决而被引入的(这里随着问题的细化确认,标出了几个比较明显的模式作为示例),这远比从场景套用模式来的更清晰自然。

image

总结起来就是,以具体场景为切入点,以遇到的问题为核心,匹配并组合模式,最终形成解决方案。

四、面向对象设计原则及共识

面向对象编程、设计原则、设计模式之间关系,恰似利剑、心法、剑法。手中有剑,心法为纲,方能知剑招,悟剑意。

4.1 面向对象编程3大特性

封装 / 继承 / 多态是面向对象的3大基本特征。我们从设计模式的角度,该怎么去理解呢?

1.封装 :本质目的是将类实现者与使用者分离,从类内部来看,只包含自己的属性,尽量不依赖其他类,只暴露必要的行为。我们经常提到的高内聚,低耦合是对它最佳的体现。耦合强度由高到低排序,泛化( is-a ) = 实现( like-a ) > 组合( part-a ) > 聚合( contains-a ) > 关联( has-a ) > 依赖( use-a )。

  • 泛化,比较好理解,是类与类之间或者接口与接口之间的继承关系。
  • 实现,也比较常见,类与接口之间的关系。
  • 组合,是一种强关联,强调部分是整体不可分割的一部分,具有相同的生命周期。例如手是身体的一部分。
  • 聚合,是一种组合稍弱的强关联,强调个体相对于集体的独立性,个体组成了集体,两者生命周期是独立的。例如独立个体的人,聚在一起,形成了各种团体组织。
  • 关联,强调的是拥有关系,是实际存在的逻辑关联。例如夫妻双方,互相为对方的配偶,两者之间关系为关联关系。 - 依赖,是一种耦合度较低的关系,这种关系一般是偶然性的、临时性的。例如我们使用手机打游戏,我们依赖了手机,本质上我们跟手机是不存在逻辑联系的。

2.继承 :本质目的是抽象,是类与类之间的联系,表示的是 is-a 的静态关系。继承同时也具备复用属性与行为的能力。

3.多态 :本质目的是复用,但是只能复用行为,表示的是 like-a 的动态关系,在运行时体现出不同。

设计模式是基于对面向对象特性的充分理解,以及对类与对象之间相互关系的应用体现。

4.2 面向对象设计7大原则

设计实现一个系统时,我们一般先按功能划分好模块,以模块中核心类为起点,根据功能逐步向周边延展设计其它类。

设计模式在这个过程中可以帮助我们进行高质量的代码设计。但是模式是有限的,这些优秀的设计模式背后有没有什么通用的指导原则呢?

  1. 依赖倒置原则:面向接口编程,不要针对实现编程。实现意味着应对变化的能力下降,尽量延迟到调用时再具体化。
  2. 开闭原则:对扩展开放,对修改关闭。比较好理解,扩展新增引入的风险相对修改更可控一些。修改往往意味着,系统扩展性不够。
  3. 里氏替换原则:继承父类的目的是为了复用。高质量的继承关系,是衍生类可以完全替换掉基类,并且系统的行为不受到影响。如果子类不能完全替换父类,说明继承是不彻底的,复用的目的就没有达到。
  4. 单一职责原则:一个类应该只承担一个职责。承担的职责过多,职责之间可能会相互耦合。这里最难的就是划分职责,职责必须恰如其分地表现实体的行为。比如用户账号可以修改基础信息,会员可以持有会员卡。如果不加以区分,只抽象一个用户实体包括所有的行为,显然是不合适的。
  5. 接口隔离原则:适度细化接口,接口的行为尽量少。分治的思想,降低复杂性,系统更可控。
  6. 迪米特法则:一个类对依赖的类知道的越少越好。本质目的是将复杂度控制在一定范围内。
  7. 组合/聚合复用原则 :复用即可以通过继承实现,也可以通过组合 / 聚合实现。区别在于,继承表达 is-a 的逻辑关联,目的在描述结构,而不是复用。

五、总结

本文围绕设计模式在业务中难以落地实践的现象,总结了一些可能的原因。跟随问题探讨了设计模式的本质以及对设计模式存在的一些错误认知,给出了以应用设计模式为目的,需要以问题为核心的学习方式。最后讲解了设计模式背后的通用原则及共识,帮助我们更好的理解设计模式。

转自有赞Coder公众号