美文网首页
你真的理解设计模式吗?

你真的理解设计模式吗?

作者: _水蓝 | 来源:发表于2019-03-13 16:28 被阅读0次
    序言

    做开发久一点的都知道设计模式,你很多人看了很多书籍和文章,但因用了设计模式而收益匪浅的人可能不是很多。这又是为什么呢?

    下面我们以工厂模式为切入点,循序渐进的来理解设计模式

    1. what is it??
    2. How do we use it?
    3. What does it can bring to us?
    工厂模式

    分三种:简单工厂模式、工厂方法模式、抽象工厂模式

    1、普通工厂模式
    描述:如果你有一个接口,想new一个实现类的对象,不要直接new,而是通过另外一个类来获得这个对象,另外的这个类就叫工厂类。

    代码举例(假设有A和B两个同事合作完成这个工程):

    // 同事A的代码
    /** 定义一个“移动电话”接口 */
    public interface MobilePhone {
        public void call(String phoneNumber);
    }
     
    /** 主程序中调用 */
    public class MyTest {
        private MobilePhone androidPhone;
        private MobilePhone iPhone;
        private MobilePhone windowsPhone;
        
        public static void main(String[] args) {
            // 等同事B写完工厂类再把下面三行创建对象的代码加进来
            // 通过传参来创建对象,参数一定不能写错
            androidPhone = PhoneFactory.createMobilePhone("Android");
            iPhone = PhoneFactory.createMobilePhone("iPhone");
            windowsPhone = PhoneFactory.createMobilePhone("Windows");
            
            if (androidPhone != null) androidPhone.call("13900000000");
            if (iPhone != null) iPhone.call("13900000000");
            if (windowsPhone != null) windowsPhone.call("13900000000");
        }
    }
     
    // 同事B的代码
    /** 创建工厂类 */
    public class PhoneFactory {
        public static MobilePhone createMobilePhone(String phoneKind) {
            if (phoneKind.equalsIgnoreCase("Android")) {
                return new AndroidPhone();
            } else if (phoneKind.equalsIgnoreCase("iPhone")) {
                return new IPhone();
            } else if (phoneKind.equalsIgnoreCase("Windows")) {
                return new WindowsPhone();
            } 
            
            return null;
        }
    }
     
    /** 手机的实现类 */
    public class AndroidPhone implements MobilePhone {
        public void call(String phoneNumber) {
            System.out.println("Dial phoneNumber " + phoneNumber + "use AndroidPhone");
        }
    }
     
    public class IPhone implements MobilePhone {
        public void call(String phoneNumber) {
            System.out.println("Dial phoneNumber " + phoneNumber + "use iPhone");
        }
    }
     
    public class WindowsPhone implements MobilePhone {
        public void call(String phoneNumber) {
            System.out.println("Dial phoneNumber " + phoneNumber + "use WindowsPhone");
        }
    }
    

    作用:如果你跟另外一个同事合作写代码,你负责调用MobilePhone接口中的方法,他负责写MobilePhone实现类,就可以考虑用工厂模式,当有一天你的那个同事想改一成另外一个实现类,他不用通知你,只需要新写一个实现类再改一下工厂类就可以了。

    缺点:你要等你同事写好工厂类之后再开始开发,没有做到解耦。

    2、工厂方法模式
    描述:前边讲的简单工厂模式是你通过传参告诉工厂类你想要什么产品,工厂类帮你生产,因为这个工厂可以生产很多种产品。

    而工厂方法模式是:有很多工厂,每个工厂只能生产单一的一种产品,你想要哪种产品,就直接去找生产那种产品的工厂就好,不用传参了。

    当然这一大堆工厂有一个统一的工厂接口,这一大堆产品也有一个统一的产品接口。

    代码举例:

    // 同事A的代码
    /** 定义一个“移动电话”接口 */
    public interface MobilePhone {
        public void call(String phoneNumber);
    }
     
    /** 创建工厂类接口 */
    public interface PhoneFactory {
        public static MobilePhone createMobilePhone();
    }
     
    /** 主程序中调用 */
    public class MyTest {
        private MobilePhone androidPhone;
        private MobilePhone iPhone;
        private MobilePhone windowsPhone;
        
        // 要创建多个工厂对象
        private PhoneFactory androidPhoneFactory;
        private PhoneFactory iPhoneFactory;
        private PhoneFactory windowsPhoneFactory;
        
        public static void main(String[] args) {
            // 等同事B写完工厂类再把下面几行创建对象的代码加进来
            androidPhoneFactory = new AndroidPhoneFactory();
            iPhoneFactory = new IPhoneFactory();
            windowsPhoneFactory = new WindowsPhoneFactory();
            
            // 现在就不用传参了
            androidPhone = androidPhoneFactory.createMobilePhone();
            iPhone = iPhoneFactory.createMobilePhone();
            windowsPhone = windowsPhoneFactory.createMobilePhone();
            
            if (androidPhone != null) androidPhone.call("13900000000");
            if (iPhone != null) iPhone.call("13900000000");
            if (windowsPhone != null) windowsPhone.call("13900000000");
        }
    }
     
    // 同事B的代码
     
    /** 创建工厂类的实现类 */
    public class AndroidPhoneFactory implements PhoneFactory {
        public MobilePhone createMobilePhone() {
            return new AndroidPhone();
        }
    }
     
    public IPhoneFactory implements PhoneFactory {
        public static MobilePhone createMobilePhone() {
            return new IPhone();
        }
    }
     
    public WindowsPhoneFactory implements PhoneFactory {
        public static MobilePhone createMobilePhone() {
            return new WindowsPhone();
        }
    }
     
    /** 手机的实现类 */
    public class AndroidPhone implements MobilePhone {
        public void call(String phoneNumber) {
            System.out.println("Dial phoneNumber " + phoneNumber + "use AndroidPhone");
        }
    }
     
    public class IPhone implements MobilePhone {
        public void call(String phoneNumber) {
            System.out.println("Dial phoneNumber " + phoneNumber + "use iPhone");
        }
    }
     
    public class WindowsPhone implements MobilePhone {
        public void call(String phoneNumber) {
            System.out.println("Dial phoneNumber " + phoneNumber + "use WindowsPhone");
        }
    }
    

    作用:不用像普通工厂模式那样总要通过传参来告诉工厂类我要什么产品了,万一参数写错一个字母呢?是吧,现在需要哪种产品就直接找生产那种产品的工厂即可。

    3、抽象工厂模式
    描述:前边讲的工厂方法模式是有多个工厂,每个工厂只生产一种产品。而抽象工厂模式是有一个工厂接口,但是这工厂不止能生产一种产品,而是能生产多个产品。

    作用:如果只是从抽象工厂模式自身出发来解释还是比难理解的,要了解抽象工厂模式其实最好是跟前边两种对比,你就会明白为什么要使用抽象工厂模式了。

    (1)最开始的简单工厂模式的缺点是你总要通过传参来创建对象,传参有点麻烦,参数也有可能传错;

    (2)工厂方法模式的缺点是你要先定义一大堆工厂接口的对象,用每个工厂接口对象创建一种产品,那么定义一大堆对象,然后去管理这一大堆对象显然也是比较麻烦的事情;

    (3)如果我即不想通过传参创建对象,又不想创建一大堆工厂对象要怎么做呢?并且这个工厂在其它同事还没有做出来时先以接口形式存在,让我们可以提前使用、并且将来可以随意替换它的实现类呢?抽象工厂模式就是针对这一点对工厂方法模式做了改进。

    // 同事A的代码
    /** 定义一个“移动电话”接口 */
    public interface MobilePhone {
        public void call(String phoneNumber);
    }
     
     
    /** 创建工厂类接口 */
    public interface PhoneFactory {
        public static MobilePhone createAndroidPhone();
        public static MobilePhone createIPhone();
        public static MobilePhone createWindowsPhone();
    }
     
    /** 主程序中调用 */
    public class MyTest {
        private MobilePhone androidPhone;
        private MobilePhone iPhone;
        private MobilePhone windowsPhone;
        
        // 现在只需要创建一个工厂对象
        private PhoneFactory phoneFactory;
        
        public static void main(String[] args) {
            // 将来需求改变时可以随意替换工厂类的实现类,其它代码不用改
            phoneFactory = new phoneFactoryImpl();
            
            // 不需要传参就能创建对象
            androidPhone = phoneFactory.createAndroidPhone();
            iPhone = phoneFactory.createIPhone();
            windowsPhone = phoneFactory.createWindowsPhone();
            
            if (androidPhone != null) androidPhone.call("13900000000");
            if (iPhone != null) iPhone.call("13900000000");
            if (windowsPhone != null) windowsPhone.call("13900000000");
        }
    }
     
    // 同事B的代码
     
    /** 创建工厂类的实现类 */
    public class PhoneFactoryImpl implements PhoneFactory {
        public static MobilePhone createAndroidPhone() {
            return new AndroidPhone();
        }
        
        public static MobilePhone createIPhone() {
            return new IPhone();
        }
        
        public static MobilePhone createWindowsPhone() {
            return new WindowsPhone();
        }
    }
     
    /** 手机的实现类 */
    public class AndroidPhone implements MobilePhone {
        public void call(String phoneNumber) {
            System.out.println("Dial phoneNumber " + phoneNumber + "use AndroidPhone");
        }
    }
     
    public class IPhone implements MobilePhone {
        public void call(String phoneNumber) {
            System.out.println("Dial phoneNumber " + phoneNumber + "use iPhone");
        }
    }
     
    public class WindowsPhone implements MobilePhone {
        public void call(String phoneNumber) {
            System.out.println("Dial phoneNumber " + phoneNumber + "use WindowsPhone");
        }
    }
    

    缺点:等你的同事写好工厂类接口的实现类之后,你还是要回过头来改你当初的代码,把他的实现类赋给你的接口,也就是说还是无法实现完全的解耦。

    好文章引用

    为什么工厂模式是不必要的?

    有感于四人帮那套书对广大的编程人员误导之严重, 决定写一个小系列,专门说这个. 此文权当第一篇, 为什么工厂模式是不必要的?

    做一件事之前,要想的不是怎么做,而是为什么要做,工厂到底要解决什么问题?

    其实归根结底就是为了不必在创建时显式指定要创建的类型,因为几个工厂其实本质是一样的, 抽象工厂是完整的, 普通工厂是化简了, 简单工厂方法又再化简一次. 如果连抽象工厂这个最复杂的都是没必要存在的, 那么另外两个就更没存在的意义了.所以这里就对着抽象工厂来开刀.

    按照四人帮最早原文说的抽象工厂存在的意义是为了: 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 类(这是抽象类)

    public abstract class Button
    {
        public static Button CreateButton(......)
        {
            //这里可以跟工厂里一样,根据参数或者配置,new出不同的子类.
        }  
    }
    
    public class WindowsButton : Button {...}
    public class LinuxButton : Button {...}
    
    //客户端调用跟使用工厂没有区别...对应于抽象工厂的每个子工厂,都这么做
    

    最终结果就是少了一大堆工厂类, 但是目的一样达成, 对维护没有什么影响,因为你增加一种控件要干的事情是一样的,只是把改工厂变成改父类了,而且改的一样是一处地方而已.

    对于新添加一个平台, 可以比抽象工厂少加一个类.

    类更少,代码更少,出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年前,多年未写,文字有些混乱, 欢迎有疑惑或异议者留言探讨.

    相关文章

      网友评论

          本文标题:你真的理解设计模式吗?

          本文链接:https://www.haomeiwen.com/subject/zjrtmqtx.html