刚才在园子里闲逛, 看见这么一篇博文在首页:
有感于四人帮那套书对广大的编程人员误导之严重, 决定写一个小系列,专门说这个. 此文权当第一篇, 为什么工厂模式是不必要的?
做一件事之前,要想的不是怎么做,而是为什么要做,工厂到底要解决什么问题?
其实归根结底就是为了不必在创建时显式指定要创建的类型,因为几个工厂其实本质是一样的, 抽象工厂是完整的, 普通工厂是化简了, 简单工厂方法又再化简一次. 如果连抽象工厂这个最复杂的都是没必要存在的, 那么另外两个就更没存在的意义了.所以这里就对着抽象工厂来开刀.
按照四人帮最早原文说的抽象工厂存在的意义是为了: Create related objects without specifying concrete class at point of creation.
那我们来看看这件事到底需要不需要引入"工厂". 典型的工厂模式的用例(不要再用工厂生产自行车和汽车做例子了....这是在偷概念, 让factory同时扮演业务对象和程序引入的不必要的噪音,这掩盖了问题) 做一个跨平台的UI库, 支持Windows,Mac,Linux等很多系统, 有按钮,滚动条等很多不同控件. 这是典型的抽象工厂案例,按照四人帮的思路, 应该有3个工厂分别给这3个平台, 如果有第4个平台就再加一个工厂, 然后还要一个抽象的工厂的父类, 用来给调用方使用(这样调用方就不需要关心到底现在在哪个系统), 然后每个平台有自己的控件的基类, 比如WindowsControl, LinuxCintrol....这些控件基类又是从一个通用的控件类继承, 然后不同的滚动条,按钮什么的再从各自平台的Control继承... ..这样就形成庞大的一大堆类和对象...
对于同一个控件, 比如button,需要一堆各种平台的工厂, 还有一堆各种平台各自的按钮. 各种平台各自的按钮是客观上业务需要(毕竟Windows和Linux显然有不同底层接口..这种分别是客观存在的), 但是那些Factory真的有必要存在么?
为什么我们不能让控件类自己来管自己的创建过程呢?
也许有人说, 我们刚才要的不就是要创建的时候不需要知道创建的是什么吗, 如果用控件类自己来直接new,那就必须要确定要new的是什么类. 而抽象类是无法new出来对象的.
等等, 为什么只能new,不能像工厂那样 create ?
比如 Button 类(这是抽象类)
1 public abstract class Button 2 { 3 public static Button CreateButton(......) 4 { 5 //这里可以跟工厂里一样,根据参数或者配置,new出不同的子类. 6 } 7 } 8 9 public class WindowsButton : Button {...} 10 public class LinuxButton : Button {...} 11 12 //客户端调用跟使用工厂没有区别...对应于抽象工厂的每个子工厂,都这么做
最终结果就是少了一大堆工厂类, 但是目的一样达成, 对维护没有什么影响,因为你增加一种控件要干的事情是一样的,只是把改工厂变成改父类了,而且改的一样是一处地方而已.
对于新添加一个平台, 可以比抽象工厂少加一个类.
类更少,代码更少,出bug的机会也就更少, 而且代码更加内聚, 本来问题里面就没工厂什么事情. 代码应该只表达需要表达的逻辑, 任何额外的附加都是应该避免的(但是有时候因为编程语言不够强大而无法避免).
事实上, "在创建对象时不需要指定具体创建的类",这个需求如果是很罕见的话, 那么不应该作为一种设计模式,因为模式应该是常见的可复用的模板和方法. 反过来如果这是一件常见的事情, 那么就应该是语言的 编译器 要做的事情, 如果一门语言, 要做这件事需求求助于 像 抽象工厂这样复杂的结构,(这个结构是实现技术叠加给它的,并不是问题本身有的) 那只能说明这门语言的设计者语法设计得有问题,不够完善,以至于程序员遇到一个"常见"的问题的时候, 需要 自己写一大堆类和代码来 完成本该编译器完成的工作(也就是程序员当人肉编译器了....)
C#的语法设计上允许一个抽象类有静态的方法,这就让factory不必要存在了.
PS: 如果有人要说,万一父类是系统库里面的,我改不了呢? 那不就需要个工厂了吗? 其实依然不需要工厂, 不过,这就涉及到另外一个设计模式遇到的问题, 你需要给一个类添加方法, 而这个类的代码你无法修改. 四人帮的书里用的方法是装饰器. 这依然可以是个不必要的的模式..本文后面会简单说一些不需要的模式.
事实上, 四人帮写书之前, 已经有 设计模式 这个词, 当时的含义跟现在完全不同. 是很泛化的, 比如汇编语言不支持函数调用语法, 于是形成了模式, 就是你要进入一个子程序, 就把你要传的数据(参数) 按顺序 push 入栈, 然后再进入子程序, 子程序里如果还有子程序, 再里面再push, 然后每退出一层就pop出来... 这就是最早的模式, 但是当像C这样的支持函数调用的语言里, 这个模式是不需要的. 因为入栈出栈的操作, 编译器帮你生成了.
其实所有的所谓的模式, 都是因为语言设计的不完善, 才使得程序员需要使用 "模式" 来解决问题, (另外一个角度,因为语法完美的语言可遇不可求,所以设计模式一直流行着...). 语言语法越强大,模式越不需要.
再举个例子,单件模式, 为了产生一个全局的访问点. 其实产生就产生吧,为什么需要搞个模式呢? 因为C++不能保证产生过程是线程安全的, 如果两个线程同时进入, 有可能会产生出两个对象,这样"全局访问点"的目的就落空了, 因此需要使用加锁的机制, 加锁影响性能, 因为其实创建只有第一次的时候需要发生以后都是读取访问应该是不需要锁的, 于是, 在外面加了个if 来跳过锁,但是又因为C++里这两个没法保证原子性,于是又需要使用 " 双检锁" 策略...于是一个简单的new操作就变成了 3层嵌套...那么有必要把这个逻辑单独抽出来, 于是单件模式就诞生了. 在C#里这一系列问题都不复存在, 虽然可以模仿C++写出一样的代码,但是你写来干嘛? 如果原始目的是要全局访问点, 静态类已经完全保证了全局访问点并绝不会创建出第2个....
还有刚才上面说的装饰器模式, 装饰器的目的是什么? C#有了 扩展方法, 还要装饰器干什么?
最后,C#语法经过这些年微软式的快速更新换代,已经很复杂很强大了, 但是它不是完美的, 它无法完整地解决上面说的那几个...比如扩展方法不允许实现接口(也许以后哪个版本可以...), 所以上面那种情况没法通过扩展方法去解决工厂的问题.... 静态类因为继承的问题也带来一些制约...
不过,确实有语言完整地解决这些问题, 比如 Clojure语言的 "协议" 语法(跟C#的接口类似, 但是不需要在类定义的时候指定这个类实现什么协议..., 可以在以后任何时候需要的话给一个类或者对象附加一个协议并实现上去....并且可以不破坏封装性, 而且只在特定模块可见....用C#的话说就是你可以给String加上一个Interface并实现那些方法,但是这个实现只对某个namespace 可见, 所以你不怕会影响微软的那些使用String类的方法,因为对他们来说String并没有这些方法和接口...他们在跟你不同的namespace里 )完整解决了装饰器模式的问题.
至于上面那个工厂的问题, 很多动态语言比如javascript或者lisp等,因为动态的特性, 可以完全避免, 比如某些动态语言可以直接 make(抽象基类,参数1,参数2) 来构建出子类对象而不需要指定子类, 那么工厂相关方法就真的完全不需要了...
在Dylan或者Lisp里, 四人帮23种设计模式里面完全不需要的模式达到有16种之多...所以设计模式其实应该跟语言相关的, 不是说你把C++的东西改下语法就是java的就是C#的,很多用其他语言写的设计模式的书,就是照把四人帮的内容照着搬一次然后代码改成XXX语言, 这么些年下去,让很多人迷失在设计模式里而把简单的东西复杂化.
写了很多年代码以后,
11年前的一天, 我第一次知道有设计模式这种东西的时候,觉得太无聊了,把东西弄复杂了.
9年前在一个大型项目中因为需求变更代码一团糟的时候,我才终于知道设计模式是干什么的,有什么用,于是开始研究和迷恋设计模式.
到了大约5年前,我已经可以不管什么模式不模式, 随便写一段代码就带着这种或那种模式的变体,组合各种模式来创建出所谓 柔软的代码. 手中无剑而心中有剑.
但是后来在stackoverflow我认识了一个网友,一个老外,他说设计模式是语言的BUG, 这些BUG需要人肉修复,于是就有了模式. 我仔细思考了很久, 越来越发现他说的有道理. 当我因为工作需要而接触了LISP系语言以后, 发现那个世界里没人把那23种当事情, 而解决那些问题的代码都是又短又漂亮而优雅.
这个文章是突然看见那个上面链接的文章而有感而发,没打草稿,一气敲出,所以可能有手误的地方,.... 而且我此文是针对的"设计模式" 这个东西,不是针对连接所指的文章,那个文章写的还是挺细致的.......
刚才发现我上篇博客还是在8年前,多年未写,文字有些混乱, 欢迎有疑惑或异议者留言探讨.