设计模式与面向对象

学习设计模式的难点

很多人都想学习设计模式,但真正学会的并不多,能够把它用在工作当中改进代码质量的就更少。我总结,学习设计模式的主要难点有四个:

第一,它不是基本语法,学习之后也未必能立刻开始使用。设计模式都有适用的场景,非使用场景强行使用设计模式,只会事倍功半。所以开发人员需要比较丰富的编程经验、见过足够多的需求,才能深入理解设计模式,才能自如使用设计模式。

第二,比起一般的面向过程的代码,设计模式抽象层级更高,需要花费更多的心思去理解。

第三,设计模式关乎的是能不能写好程序,而不是能不能写出程序。并不是所有人都愿意花时间琢磨怎么样才把程序写得更好。

换言之,如果你想提高自己的业务水平,投入时间学习,比如,你正在认真阅读这篇文章,恭喜你,你已经强过很多人了。

第四,请注意,《设计模式》一书的完整标题是:“设计模式——可复用面向对象软件的基础”。设计模式针对的是面向对象编程,不是面向过程编程,所以,如果开发者不具备面向对象的思想,未能以对象为核心来架构代码,那么几乎不可能用得上设计模式。

所以归根结底,我们还是要从面向对象说起。

面向对象

考虑到有些同学可能不熟悉面向对象编程(简称 OOP),所以这里简单介绍一下面向对象的思维方式。

面向对象是软件设计的一种抽象方式,它与面向过程的不同,后者把程序当成一系列指令的组合,面向对象则认为,程序就是“一些东西一起来完成一项工作”。这里的“东西”,就是面向对象中的“对象”,它们都有特定的职责,能完成特定的功能。我们根据需要自己的设计,拆分功能,形成若干对象,然后交代这些对象来完成特定工作。

举个例子。比如说我们要开发公司内部使用的工具,给所有同事算工资。

如果以面向过程的方式来写,那么它大概是这样的:

  1. 取出所有同事的信息。
  2. 取出所有同事的考勤。
  3. 取出所有同事的绩效打分。
  4. 取出所有的事假、病假、年假,借款报销抵扣之类的东西。
  5. 合并以上所有信息进行计算,算出每个人的工资,返回用户。

看起来很简单,对吧?面向过程的代码在刚刚写出来的时候,通常符合逻辑、结构清晰、简单易懂。所以大部分同学一开始写的都是面向过程的代码,并且觉得自己写的还不错,既满足了需求,阅读起来也很轻松;如果更努力一些,在各个步骤当中加进去一些注释,简直就是非常好的代码了。

但是很快需求就产生了变化:

  1. 首先,大家的工资并不是每个月都一样,比如过年的时候,通常都会发年终奖;天热的时候,公司也可能给大家发一些降温费。
  2. 公司的业务出现了变化,有一部分同事的底薪下调,绩效比重增加。
  3. 不同岗位对到岗时间要求不一样,销售要拜访客户,其它部门都要准点上下班,考核方式不同。
  4. 除了纵向的岗位工资,还有横向的项目奖金,即使同属一个部门,大家的各种系数也不一样。
  5. ……

随着需求变化,代码不断调整,越来越臃肿。我们不得不加入各种 if ... else ...,代码行数不断增加,甚至上涨到几千行。此时的代码几乎不能维护,因为你根本不知道这里的一处改动会对后面造成什么样的影响。

有时候我们会尝试重构,加入一些函数,但基本无济于事,战略问题无法通过战术来弥补。

面向对象的解法

还是刚才那个需求,换用面向对象编程,应该怎么做呢?

第一步,当然还是取出所有员工的集合。

紧接着,我们可以先把其它数据都取出来备用,也可以等下再按需取用。除非你迫切希望减少连接次数,不然我推荐你选用后者。

然后就不太一样了:

  1. 我们会构建一个“员工”基类,包含基础信息,比如底薪、奖金、绩效、考勤等,然后赋予其最基础的计酬算法
  2. 针对不同的员工类型,构建诸如“开发”、“销售”等子类
  3. 子类覆写父类的计算方法,可以实现特殊的计算公式
  4. 我们甚至可以把加减款项抽象成别的计算基类和子类,以实现特殊的计算,或者从特定的数据源取数据

如此一来,一大段面向过程的代码会分解成十几个不同的类,类之间有不同的继承和嵌套关系,代码结构似乎变复杂了。但实际上,类里面的每个函数每个算法,都不会很复杂,二三十行而已,理解和维护都会变得很简单。假设需求变化,需要我们新增一种岗位类型,或者某个岗位的算法有所变化,我们也只要实现一个新类或者修改某个类的实现即可。维护成本大大降低。

面向对象编程就是如此,它并非来出自本能,而是需要我们先理解需求,设计抽象类型,然后进行实现。短期来看,从面向过程到面向对象,消耗的时间会增加;但是长期来看,因为程序设计逻辑简单清晰明了,代码复用大大增加,维护成本和二次开发效率远远优于面向对象。

设计模式

面向对象当然也有缺点,比如,最常见的,人与人对世界的认识不同,所以每个人设计的对象结构都不尽相同,于是阅读其他人的代码时,效率就会比较低。长时间面向对象编程之后,一些前辈达人总结出来能够更进一步改善代码结构的方式方法,便是设计模式。

理解和学会设计模式,对我们之后开发会有很大的帮助。

有些同学可能没有主动使用过设计模式,但实际上,设计模式离我们并不远。设计模式提出于1990年,距今将近30年,早已渗入各种流行软件的设计当中。以我们 Web 前端为例,除了本文将重点讲述的几个设计模式之外,DOM 事件机制就是观察者模式(Observer)的实现,而 JS 也是基于原型链模式(Prototype)打造的。

如果简历里写着“熟悉设计模式”,但是只能答出单例(Singleton)和观察者的候选人,在我看来都不合格。与诸君共勉。

如果您使用过现代 MVVM 框架,那么你多半也用过其中的状态管理工具,比如 Vuex。它是从中介者模式(Mediator)演化而来的。

综上所述,其实我们日常开发中已经在享受设计模式带来的便利性和高效率了。

如何学习设计模式

需要大家注意的是,时过境迁,现在的开发环境,与那个时候的开发环境相比,变化巨大。如今的语言和平台,无论在 API 设计、语法设计、抽象程度,都不可同日而语。所以大家在学习设计模式的时候,也千万要与时俱进。不要盲目的去照搬一些书上的设计,也不要照抄书上的示范代码。

比如原型链,是 JS 的基础设计,绝大多数时候,我们使用原生设计就足够了。ES6 里新增的 Proxy 和 Decorator,也提供原生的代理模式和装饰器模式。

比如单例模式,大多数的讲设计模式的书和文章,都会教你先声明一个类,然后声明一个构建方法,然后创建一个实例,保证在所有的地方都使用同一个实例,就叫单例模式。这就是典型的教条主义。因为 JS 开发场景完全不同,无论用 CommonJS 还是 ES Module,我们都可以 export 一个对象,它的行为模式完全等效于单例。

其次,每个设计模式都有自己适用的场景,脱离那个场景,它的价值就难以发挥,甚至给日后挖下大坑。所以,不要刻意使用设计模式。

要想正确地学习和使用设计模式,我建议:

  1. 先简单了解设计模式,知道它们能解决什么样子问题
  2. 遇到问题时想一想,是否有设计模式可以解决这个问题
  3. 如果有,回去认真学习那个设计模式:它是怎么做的、有哪些注意事项,等等
  4. 用这个设计模式解决问题

如此重复两三次,这个设计模式就会牢牢的印在你的脑海里,成为你的常规武器。比如前面提到的员工工资计算问题,用策略模式可以收获奇效。

如果有些设计模式你好几年都没有用到,也没关系。很可能因为你的业务、你的开发工具、开发体系、你的用户群体,都不太需要这个设计模式。

有些模式,你只要知道它们的优缺点,知道怎么用就可以了,比如说原型链模式、代理模式、装饰器模式,JS 里都有原生实现,我们只要会用就可以了。

还有,我们要学会从别人的代码中发现设计模式的用法和价值。比如,这篇文章会介绍那些帮助 jQuery 取得成功的设计模式。在其它库和框架里,常常也存在很好的使用范例,比如 Vuex 里用到中介模式和备忘录模式(Memonto)、比如 Express 中用到职责链模式(Chain of Responsibility)。

先知晓这些设计模式,然后从实际用例当中学会使用设计模式,最后学会实现这些设计模式,也是个很好的路径。

再总结一下前端同学学习设计模式的方法:

  1. 不要刻板照搬书上的实现方式
  2. 找个机会把所有的设计模式看一遍,在日常开发当中尽量使用
  3. 发现设计模式,使用设计模式
  4. 从别人的代码当中,学习使用设计模式。

OK,关于面向对象和设计模式,我要说的大概就说完了,接下来我们来看第一个设计模式:工厂模式。