美文网首页
[笔记]设计模式简要总结

[笔记]设计模式简要总结

作者: 蓝灰_q | 来源:发表于2017-10-14 00:30 被阅读30次

    用途

    软件工程中总会遇到重复出现的问题,某些可复用的成功经验,就可以抽象为模式,在设计方面,就是设计模式。
    设计模式不仅是针对某个需求的,更多是为了应对需求变更的,在修改代码以满足需求变更时,如何能提高可读性、降低工作量、不干扰无关业务等,就是设计模式的目标。
    没有万能的设计模式,只有特定条件下为一些重复问题提供的合理解决方案。

    6个设计原则

    迪米特法则:最少知道法则,一个对象应该对其他对象有最少的了解
    以及5个常被统称为SOLID原则:
    单一职责:类的职责单一,不要上帝类
    开放封闭:允许扩展,不允许修改
    里氏替换:子类可以替换父类,所以我们用基类书写逻辑,在运行时再确定子类
    接口隔离:不要大接口,用多个接口,且多个接口间应该是各自独立的角色
    依赖倒置:就是面向接口编程,不应依赖具体而应依赖抽象,尽量用接口和抽象类解耦合
    此外,还有一种合成复用原则:尽量组合对象,少用类的继承,减少耦合,维护封装

    23种设计模式

    Java 中一般认为有23 种设计模式,总体来说设计模式分为三大类:
    创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式
    原型模式。
    单例模式主要考虑线程安全性和懒加载的问题,现在主要用静态内部类实现。
    建造者模式主要依靠Director类持有的Builder类实例来返回Product对象,Product和Director也可以是一个类。
    原型模式本质是通过clone对象内存复制一个对象实例出来,所以他与工厂模式是互斥的。
    结构型模式,共七种:适配器模式、桥接模式、装饰器模式、代理模式、外观模式、组合模式、享元模式。
    适配器是在不修改原类的前提下,让它能在新的接口中使用。
    适配器主要分为类适配器(extends A implements B)和对象适配器(implements B),前者靠继承,后者靠构造注入对象
    适配器里还有一种接口适配器,主要作用是可以只实现接口中的一部分函数,比如AnimatorListenerAdapter就可以用来避免实现AnimatorListener的所有函数。
    对象适配器和桥接模式很相似,但适配器是为了让两个接口配合,而桥接是为了拆分成不同纬度
    装饰和静态代理很相似,但意图和语义是相反的,装饰是为了动态增加功能;代理则是为了控制访问。
    外观和代理的区别是外观代表一个子系统,而不是一个单一对象。
    装饰模式与代理模式的区别(转载)
    装饰者,适配器,代理和外观模式的区别
    行为型模式,共十一种:策略模式、模板方法模式、命令模式、访问者模式、状态模式、观察者模式、迭代器模式、责任链模式、备忘录模式、中介者模式、解释器模式。
    策略模式容易联想起装饰者模式,但是装饰者模式要求构造传参的是自己的兄弟类实例,策略模式是其他的业务类实例
    策略模式和模板模式都是更换算法,但策略是整个更换,模板是保留算法框架,只更换部分细节函数。
    策略模式和访问者模式很相似,不过策略是通过构造注入或属性注入传入业务的策略对象,访问者是在函数调用时传入业务的访问者对象,而且访问者会再回来调用元素。
    策略模式和状态模式很相似,不过策略模式里是由调用者更换策略,而状态模式是在业务处理过程中,由状态类(或环境类)里自动切换状态的。
    命令模式和状态模式很相似,但命令是把发送者和接收者解耦,接收者之间没有关系,有时候不要接收者,直接在命令里做业务;状态模式是为了切换状态,状态之间是可以切换的,而且最终还是在环境类里做业务。
    命令模式像建造者模式中Director类和Product类分开的情况,访问者模式像Director类和Product类是一个类的情况。不过命令模式和访问者模式里处理的是抽象类。

    *5种创建型模式

    工厂模式

    工厂模式也分为简单工厂(用if else提供多产品)、工厂(每种产品一个工厂)、抽象工厂(每个工厂提供多个流水线,流水线提供最终产品)

    单例模式

    单例模式主要考虑这几个因素:
    1.延迟加载
    2.线程安全
    3.性能损耗
    所以,现在比较好的写法是使用静态内部类或枚举来实现
    单例模式的几种写法和各自优势

    建造者模式

    用一个Director去引用基础Builder对象,在扩展的ConcreteBuilder中实现建造方法,最终得到一个Product对象。
    AlertDialog、Glide、Retrofit等都使用了建造者模式
    优点
    建造过程和使用过程是分离的
    很容易修改Product
    缺点
    建造过程没有封闭,在调用者那里是可见的

    原型模式

    原型模式主要是在内存中直接复制一个对象

    abstract class ProtoType implements Cloneable{
        String a1;
        int a2;
        public abstract ProtoType clone();
    }
    

    其实就是通过实现Cloneable接口,实现clone内存对象。
    原型模式对于基本类型容易实现,但对于复杂的引用类型,还需要设法实现深拷贝。

    *7种结构性模式

    适配器

    适配器可以在两个对象之间扩展出一种联系,调用对象A的方法a,实际执行的是对象B的方法b,同时A和B都不需要做改动。
    适配器的优势是开放封闭原则好,原始类不需要改动
    缺点是代码可读性差,看代码是A接口,实际实现却是B的方法,系统结构零乱,不容易理解。
    适配器分为类适配器和对象适配器。
    类适配器
    对象A和B中,有一方为接口的情况下,可以利用类的继承关系,适配器继承类B,扩展接口A,在初始化A对象时,返回一个Adapter实例,该实例执行A的方法a时,在a内部调用父类B的方法b,实现适配器。
    对象适配器
    对象A和B中,均为类的情况下,因为类不能多继承,就需要利用引用关系,适配器继承类A,引用一个类B的实例对象,在初始化A对象时,返回一个Adapter实例,该实例执行A的方法a时,在a内部通过类B的实例对象调用方法b,实现适配器。

    桥接模式

    在抽象类里引用另外一个业务维度的接口,好像一座桥把两个业务维度连接起来,这样可以实现多继承。

    abstract class IShape {
        protected IColor color;
        public IShape(IColor color) {
            this.color = color;
        }
        abstract void operation();
    }
    

    优先
    满足单一职责的同时,部分实现多继承
    把抽象和实现分离开,两个维度中任意扩展都不需要修改原系统
    缺点
    设计人员需要准确抓出两个独立维度,这不容易做到
    在抽象层实现的聚合,不利于可读性和设计难度

    装饰器模式

    Decorator实际上是一层层的引用嵌套,其实就是在继承了同一个基类/接口的两个类中,把其中一个基础类的实例封装为基类/接口,(构造传参)传给另一个装饰类的实例,另一个类通过引用基础类,在接口函数中调用基础类的函数,再增加一些装饰功能。都要作为基类/接口来调用函数。
    java.io就是典型的装饰者模式,InputStreamReader和BufferReader都是Reader,但是BufferReader通过装饰InputStreamReader扩展了功能。
    Android里的Context也是一种典型的装饰者模式,所有的contextwrapper都是用contextimpl去做context业务的。
    优点
    用引用代替继承,类的关系更加灵活自由,不会出现子类爆炸
    可以在运行过程中动态装饰,不用修改代码
    缺点
    装饰层次太深的话,会影响效率
    适合改变外部表现,不适合改变内部,如果要改变内部,应该用策略模式

    代理模式

    代理分为动态代理和静态代理两种。
    静态代理 以类为单位进行操作
    我有一个接口IBiz,业务类BizA实现了接口函数,如果业务变动,希望在函数前后增加点逻辑,为了不修改业务类BizA,我们就做一个BizB的类,实现IBiz接口,并引用BizA,在实现的接口函数里,调用BizA的函数,并且在BizA的函数前后增加逻辑处理。
    在调用的时候,我只要IBiz的实例,实际上创建一个BizB的实例,并给BizB传一个BizA的实例作为参数,这样,我用IBiz去发起调用,实际执行的是BizB的函数,在BizB执行函数过程中,会执行BizA的函数。实际上是BizB代理了BizA的业务,这就是代理模式。
    因为BizB对BizA的代理是写死在BizB的代码里的,所以叫静态代理。
    静态代理的问题在于,对每一个要扩展的业务类,虽然扩展内容一样,但是对每个类都要做一个对应的代理类,用工厂模式去生产。
    静态代理和装饰模式的代码结构是一样的,但是使用目的不同。
    JDK动态代理 以函数为单位进行操作
    为了在代码中根据业务需要,动态地为函数添加逻辑,Java提供了动态代理机制。
    动态代理是借助反射来实现的,核心是用java.lang.reflect的Proxy.newProxyInstance动态创建代理类,再用InvocationHandler去重写invoke函数,它其实分为四步:
    1.定义好调用处理器
    实现InvocationHandler接口,做一个函数调用处理器,编写要扩展的逻辑。因为要对被代理类进行扩展,所以需要把被代理类封装为Object传进来(构造函数或bind函数传入)
    2.创建动态代理类
    用反射的Proxy创建动态代理,需要被代理类的ClassLoader,以及这个被代理类的所有接口。(所以,要使用动态代理,就必须有对应的接口定义)
    3.创建对应的构造函数
    用反射获取动态代理类的Constructor构造函数,需要用InvocationHandler作为参数。
    4.构造出动态代理类的实例
    用构造函数new一个实例,把第1步中的函数调用处理器作为参数传入。
    生成的动态代理类实现接口,接口函数实际上调用了函数调用处理器的invoke方法,在invoke方法中利用反射调用了被代理类的方法。
    2、3、4步其实在Proxy中是newProxyInstance一个函数。

    final Star star = new SuperStar();
            Star st = (Star) Proxy.newProxyInstance(Star.class.getClassLoader(), star.getClass().getInterfaces(), new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args)
                        throws Throwable {
                    System.out.println("before---------");
                    Object object = method.invoke(star, args);
                    System.out.println("after---------");
                    return object;
                }
            });
            st.singSong();
    

    CGLib动态代理 修改字节码的方式
    CGLibs在代码编写上和InterceptionHandler的实现类很像,它使用MethodInterceptor,并在intercept中实现逻辑扩展,不同的是,CGLib不需要通过接口实现。
    但是Android不支持CGLib动态代理,因为android类文件和jdk类文件不一样(CGLib针对.class文件,Android的是优化后的.dex文件)
    Java 动态代理作用是什么
    彻底理解JAVA动态代理

    外观模式

    为复杂的子系统提供一个简单的接口,调用者直接访问Facade外观角色,Facade去调用子系统,但同时子系统也无需知道Facade外观角色。
    优势
    调用者不用关心子系统,而且用起来更容易
    调用者和子系统互相不需要知道,松耦合
    大型项目不再受子系统的编译、修改、移植的影响
    缺点
    用户仍可以直接访问子系统,不能屏蔽
    子系统修改时,Facade和调用者有可能都要修改,违反开闭原则

    组合模式

    典型案例就是Android中View+ViewGroup的结构
    优点
    分层次的复杂对象可以清晰的定义和组合出来
    仅靠leaf和composit两种构件,就可以递归组合成复杂树形结构
    维护旧构件、扩展新构件很方便,调用方不用修改代码
    调用者使用简单,整个组合结构或单个对象都可以一致地使用
    缺点
    对设计人员来说,如果业务规则很复杂,就很难实现,特别是composit构件很难focus在对leaf的处理

    享元模式

    为了减少对象的使用,FlyWeight享元模式可以共享相同或相似的对象,实现重用。
    用一个Factory去提供享元对象,Factory里用Map管理和提供所有对象,如果要的对象在Map里已有,就提供重复对象。
    享元模式里除了作为父类的抽象对象抽象享元,有单纯享元和复合享元两种:
    单纯享元
    享元全是内部状态,整个都可以共享。
    复合享元
    对象是composite复合对象,不能共享,需要从中剥离出单纯享元,仅对单纯享元共享。

    //复合对象本身不能共享
    class ConcreteCompositeFlyweight extends Flywight {
        Map<String, Flywight> list = new HashMap<>();//单纯享元,可以共享
        public void add(String state, Flywight flywight) {
            list.put(state, flywight);
        }
        @Override
        void operation(String externalState) {
            for (Map.Entry<String, Flywight> data : list.entrySet()) {
                data.getValue().operation(externalState);
            }
        }
    }
    
    class FlyweightFactory {
        HashMap<String, Flywight> map = new HashMap<>();
        public Flywight getFlyweight(List<String> states) {
            ConcreteCompositeFlyweight ccFlyweight = new ConcreteCompositeFlyweight();
            for (String state : states)
                ccFlyweight.add(state, getFlyweight(state));//复合对象中的单纯享元,使用享元模式
            return ccFlyweight;
        }
        public Flywight getFlyweight(String state) {
            Flywight flywight = map.get(state);
            if (flywight == null) {
                flywight = new ConcreteFlyweight(state);
                map.put(state, flywight);
            }
            return flywight;
        }
    }
    

    优点
    大幅降低内存中重复对象的数量
    缺点
    系统更复杂
    不能共享的外部状态会导致一定的性能损耗

    *11种行为型模式

    策略模式

    实现算法的封装与切换

    class Context {  
        private AbstractStrategy strategy; //维持一个对抽象策略类的引用  
        public void setStrategy(AbstractStrategy strategy) { //注入策略实例
            this.strategy= strategy;  
        }  
        public void algorithm() {  
            strategy.algorithm();  //调用策略类中的算法  
        }  
    } 
    

    其实就是通过各种方式(构造函数注入、set注入、反射+配置文件)等方式,动态调整策略实例
    优点
    满足开闭原则
    可以替换继承关系,更灵活
    避免长长的if else
    缺点
    会产生很多细小的策略类
    调用者必须知道所有的策略
    无法同时使用多个策略

    模板方法

    模板方法其实使用了多态的特性,通过具体方法和抽象方法的混合使用,在父类中处理算法的主要框架,但是用到的某些细节函数不在父类中实现,而是在子类中实现。
    结合配置文件+反射,就能实现动态配置算法模板。
    钩子方法
    有一些特殊的模板方法,是在父类中做一些返回boolean值的函数,子类通过修改这些函数的返回值,就能控制业务逻辑的分支,这种做法叫做钩子方法。
    优点
    算法代码复用
    不会改变算法模板的框架,执行次序都是模板决定的。
    通过更换子类更换具体实现,符号单一职责和开闭。
    采用钩子方法,能实现子类对父类的反向控制
    缺点
    如果模板中可更换的点很多,会导致子类出现很多,有时候要考虑改成桥接模式,减少子类。

    命令模式

    其实就是把请求封装起来,实现在发送者和接收者之间解耦,命令引用接收者,发送者引用命令,所以发送者和接收者之间是解耦的

    Receiver receiver = new Receiver();//接收者真正处理业务,但谁也不引用
    Command command = new ConcreteCommand(receiver);//命令引用接收者
    Invoker invoker = new Invoker(command);//发送者引用命令
    invoker.doInvokerAction();//trigger
    

    请求命令作为对象,有很多可以操作的点。
    可以把命令参数化注入,为发送者的参数注入不同的具体命令,就能让不同的接收者来处理
    可以把命令排入队列,实现缓存和排序处理
    可以撤销命令,就是在请求命令中执行一个相反的操作,如果使用命令集合,就能多次撤销。
    可以序列化后储存起来,反序列化后再次执行操作
    组合命令,命令模式和组合模式混用
    就是在外层命令里维护一个命令列表,外层命令的一次执行,会遍历执行命令列表,实现对命令的批处理
    优点
    发送者与接收者解耦
    容易增加新命令
    容易实现队列、撤销、日志等操作
    缺点
    不能减少类的数量
    可能出现大量具体命令类

    访问者模式

    其实就是在待处理的元素中,选择一个访问者,作为处理自己的业务类,这样可以通过更换访问者,更换对元素的处理方式

    abstract class Visitor{  //访问者访问不同方法
        public abstract void visit(ConcreteElementA elementA);  
        public abstract void visit(ConcreteElementB elementB); 
    ...
    class ConcreteElementA implements Element{ 
        public void accept(Visitor visitor)  {  //元素里选择不同的访问者
            visitor.visit(this);  //由访问者处理自己
        } 
    

    访问者模式一般是在一个数据集合中遍历处理元素时使用。
    双重分派
    首先给元素的accept函数传参visitor
    然后,给visitor的visit函数传参Element元素自己
    最后,在visit函数里,还可以调用Element元素自己的函数
    这就是双重分派
    组合访问者
    一般在处理元素时,使用迭代器来遍历地处理元素,如果元素分为两种,一种是leaf,一种是composite,在composite里继续遍历,就是访问者+组合模式
    优点
    对元素的处理抽象、封装为访问者,不是散落在元素里,单一职责,而且元素的复用性更好
    扩展元素操作时,容易通过增加新的访问者,实现对元素的新的处理方法,符合开闭原则
    缺点
    难以增加新元素,不符合开闭,因为抽象访问者类要增加对应的方法
    破坏迪米特,访问者需要知道元素的一些内部操作

    状态模式

    可以在环境类里动态更换状态类,环境类写的业务函数,是在状态类中调用的,所以动态更换状态类,就是动态更换业务行为
    环境类引用状态类,抽象状态类引用环境类,当环境类做操作时,调用状态类的操作,状态类一方面反调环境类的业务操作,另一方面检查状态变化,为环境类更换状态类(或者不在状态类切换,而是在环境类切换)

    //环境类
    class Account {  
        private AccountState state; //维持一个对抽象状态对象的引用  
        public void withdraw(double amount) {  
            state.withdraw(amount); //调用状态对象的业务方法
        public void setBalance(int i){//真正的业务方法
            setState(new OtherState()) //(1.在环境类里切换状态)
    ...
    //抽象状态类  
    abstract class AccountState {  
        protected Account acc; 
    ...
    //正常状态:具体状态类  
    class NormalState extends AccountState {  
        public NormalState(Account acc) {  
        public void withdraw(double amount) {  //状态类的业务方法
            acc.setBalance(acc.getBalance() - amount); //仍然用环境类的方法
            stateCheck(); //检查状态变换(2.或者在状态类里切换状态)
        }   
        public void stateCheck() {  //状态转换
            if (acc.getBalance() > -2000 && acc.getBalance() <= 0) {  
                acc.setState(new OverdraftState(this));  
            } 
    

    共享状态
    如果同一个环境类的多个不同实例要共享状态,可以把多个状态定义为static对象,一个环境实例更换状态,其他实例也全部更换(更换时要使用环境类里的静态对象)
    优点
    把状态转换的规则抽离、封装、集中管理
    状态行为全放进状态类里,通过设置不同状态,就能设置环境类的行为
    避免使用庞大的条件语句
    共享状态,减少对象
    缺点
    增加了类和对象
    提升了系统复杂度,容易混乱
    不符合开闭原则,增加新状态类,需要修改状态转换代码;修改旧状态类,需要修改状态类代码

    观察者模式

    核心思想是封装一批Observer对象,放进Observable对象的订阅队列里(注册),当Subject对象发生变化时,在notify函数里遍历地调用所有Observer对象的update方法(通知)。
    Android中ListView在更新数据时刷新界面,就是使用了观察者模式,setAdapter时,会为传入的Adapter注册一个DataSetObserver(是一个AdapterView的子类AdapterDataSetObserver),当Adapter的数据变化时,这个observer就会去更新数据,更新当前显示列表项,更新焦点、刷新layout等。
    基于观察者的委派事件模型
    Java事件处理有三个要素:事件源对象、事件对象和事件处理对象。
    把事件处理对象(eventlistener)注册到事件源对象,事件源对象把event事件委派给listener,实际上是个一对一的观察者模式
    优势
    在观察者和被观察者之间解耦(只维持抽象集合)
    可以借此分离表示层和业务逻辑层
    可以建立一个触发链
    缺点
    通知所有订阅者,有性能损耗
    update函数会传递Observable对象,如果观察者借此引用了被观察者的某些特性,就可能需要修改观察者的代码,破坏开闭原则。

    迭代器模式

    其实就是把一批聚合对象封装为Iterator对象,迭代器iterator里用cursor游标实现对聚合对象的依次访问,然后在一个聚合类里把原始聚合数据如List列表变成Iterator对象。有时候迭代器是聚合类的内部类。

    public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {  
        private class Itr implements Iterator<E> {  
            int cursor = 0;  
            ......  
    } 
    

    优点
    遍历从聚合类中剥离,很容易更换聚合类
    只要实现Collection接口,就能实现迭代器模式,简化聚合类的开发
    通过抽象层实现新的迭代器和聚合类,符合开闭原则
    缺点
    抽象迭代器很重要,但很难平衡性能和功能
    新的聚合类不能自己实现遍历,可能需要新的迭代器,多需要一个类

    责任链模式

    其实就是一串Handler组成一个链,每个决定自己处理或者传递给下一个角色处理,责任链的每一环只能处理或不处理,不允许处理一部分再继续传递。
    优点
    可以动态地组织和分配责任
    简化对象的相互连接,只要记住下一个,不需要记住全链路
    缺点
    不追踪处理结果,可能没有处理
    链过长会影响性能,且调试不便

    备忘录模式

    其实就是在一个数据管理类中,管理Memento备忘数据(保存对象的历史状态),发起人只引用了Memento类,但不管理备忘数据,而是通过CareTaker数据管理类实现保存和恢复

    caretaker.setMemento(originator.createMemento()); //向备忘录存数据
    originator.restoreMemento(caretaker.getMemento()); //从备忘录取数据
    

    如果要实现多状态多备份,就在Memento里维护一个map实现多状态,在CareTaker里维护一个map实现多数据。
    优点
    发起人不需要管理备份状态
    缺点
    备忘录对象和备忘录管理对象会消耗大量内存

    中介者模式

    其实就是把网状的交互抽象为星型的,让Mediator中介来统一负责交互,从多对多变成一对多,从而符合迪米特法则。
    Mediator会引用每个Component组件,每个Component组件也引用Mediator中介,这样组件通过调用中介,中介再与其他组件交互。
    优点
    网状对象解耦
    简化对象交互
    在改变行为时,改变一个中介类,优于扩展大量的组件类
    缺点
    组件过多时,中介类会演变的非常复杂

    解释器模式

    就是用来创建和扩展自己的文法规则的,它是用类来解析文法的,解析基本分两部分interpret解释命令+execute执行命令
    解释器模式在解析XML文件、正则表达式等场景中使用较多

    String text = "LOOP 2 PRINT 杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉";  //重复处理的特定规则的语法文本
    Context context = new Context(text);  //语法文本放进环境类,逐个解释处理
    Node node = new ExpressionNode();  //把语法元素都转换为Node,遍历地+递归地解释处理
    node.interpret(context);//解释为命令
    node.execute(); //执行命令
    

    其实就是用一个环境类,把要解析的文法放到环境类里,逐个解析文法中的内容,能逐个处理语法文本中的元素。
    然后用一组文法解析类去处理,比如首先用一个ExpressionNode,把文法中的每个内容都包装为CommandNode,相当于把原始语言加载为解释器认识的Node列表。
    然后每个CommandNode中分别处理递归命令和具体命令,比如语法中的Loop循环就交给专门的LoopNode处理(Loop中递归地交给ExpressionNode处理),具体命令交给PrimitiveNode处理
    优点
    能够方便地实现简单的语言
    增加语言规则时,增加对应的表达式类即可,符合开闭原则
    缺点
    需要先把重复出现的问题抽象为语言,并建立抽象语法树
    对于复杂文法,每条规则一个类,会类爆炸
    大量循环和递归,导致效率低

    引用

    从Android代码中来记忆23种设计模式
    史上最全设计模式导学目录(完整版)
    设计模式汇总:创建型模式
    设计模式汇总:结构型模型(上)
    设计模式汇总:结构型模型(下)
    设计模式总结之行为型模式

    相关文章

      网友评论

          本文标题:[笔记]设计模式简要总结

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