设计模式研究,java 23种设计模式

众所周知,我们在实际编写代码时,为了更好的组织代码,都会在代码中采用的一定的设计模式。一般我们在代码的设计中会遵循如下几大原则:
1. 开闭原则(OCP open closed Proiciple),一个类应该对扩展开放,对修改关闭,如果要修改代码,尽量用继承或组合的方式来扩展类的功能,而不是直接修改类的代码。开闭原则是最基础的设计原则,其它的五个设计原则都是开闭原则的具体形态,也就是说其它的五个设计原则是指导设计的工具和方法,而开闭原则才是其精神领袖。依照Java语言的称谓,开闭原则是抽象类,而其它的五个原则是具体的实现类。设计模式中模板方法模式和观察者模式、策略模式都是开闭原则的极好体现。
软件功能可以扩展,但是软件实体不可以修改

2. 单一职责原则(SRP single responsibility prociple),一个类,应该只有一个引起它变化的原因,应该将强相关的元素放在一个类里,弱相关的元素放在类的外边,保持类内的高内聚性
3. 依赖倒置原则(DIP depedence inversion principle),实际上就是要依赖于抽象,高层模块不应该依赖于底层模块,二者应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
依赖倒置的关键是接口所有权的倒置.
每个高层模块都为它所需要的服务声明一个抽象接口,而低层模块则实现这些抽象接口,高层模块通过抽象接口使用低层模块。 在我们常见的MVC编程中,service层依赖dao层,并不是直接依赖dao的具体实现而是依赖dao层提供的抽象接口,但是这并不是依赖倒置,依赖倒置中最终奥的是抽象时属于谁的抽象,应该是service定义出自己需要的接口,dao去实现它,二者看似一样,但是还是有差距的。依赖层次中,每一层的接口都被高层模块定义,由低层模块实现,高层模块完全不依赖低层模块,即使是低层模块的接口。这样,低层模块的改动不会影响高层模块,高层模块的复用也不会依赖低层模块。对于 Service 和 DAO 这个例子来说,就是 Service 定义接口,DAO 实现接口,这样才符合依赖倒置原则。
4 里氏替换原则(LSP liskov substitution principle) 只要父类能够出现的地方,子类都能够出现替换。一般来说,如果子类继承一个父类,不那么子类就拥有了父类可以继承的属性和方法,这个时候用子类替换父类,应该不会引起原来使用父类类型的程序出现错误,但是如果子类修改了父类型的某些属性或者覆盖了某些方法,那么原来父类型可以运行的程序,替换换为子类型之后可能会出现问题。从开闭原则的角度来说,里氏替换原则则是开闭原则实现方式之一,开闭原则要对扩展开放,对修改关闭,扩展的一个实现就是通过继承,里氏替换原则能够保证子类替换父类。
在具体实践中,如果继承一个类仅仅是为了复用其父类的方法的时候,那么可能已经离错误不远了,一个类如果不是为了继承而设计那么最好不要继承他,这时候应该用组合而不是继承,比如适配器模式
5. 接口隔离原则(ISP interface segreation principle) 客户端应该只依赖于他所需要的接口,不应该依赖不需要的接口。实际上就是需要接口尽量细化,接口中的方法尽量少,这个与单一职责看上去类似,但是二者角度不同,例如一个接口中10个方法,都是同一职责相关的,给不同模块使用,这符合单一职责原则,但是接口隔离原则是不允许的,因为接口隔离原则希望每个模块访问一个单一的接口,而不是多个模块公用一个接口,调用接口中的不同方法。一种方法是通过委托进行隔离(例如增加一个适配器),另外一种是多重继承
6. 最少知识原则(LKP least knowledge principle) 也称迪米特法则,指的是在设计系统构建代码的时候,应该尽量较少对象之间的交互,对象只和自己相关的部分交互,达到松散类之间的耦合,减少类之间的相互依赖。尽量减少对象之间的交互,从而减小类之间的耦合。在做系统设计时,不要让一个类依赖于太多其他的类外观模式模式、中介者模式

面向对象原则

  • 多用组合,少用继承
  • 针对接口编程,不针对实现编程
  • 为交互对象之间的松耦合设计而努力
  • 类应该对扩展开放,对修改关闭
  • 依赖抽象,不依赖具体类

在设计模式中,一般又分为:创建类模式,结构类模式,行为类模式

创建类模式

主要有:工厂方法模式、抽象工厂、单例模式、建造者模式、原型模式

工厂方法模式(Factory Method)

目标:定义一个创建对象的接口,让其子类决定实例化哪一个类,工厂方法是一个类的实例化延迟到子类。
其构件如下:

  • Product 工厂方法需要创建的对象德吉接口
  • ConcreteProduct 实现Product接口的具体对象
  • Provider 声明工厂方法,返回一个Product
  • ConcreteProvider 具体的工厂方法实现类,覆盖Factory中定义的工厂方法,返回具体的Product实例
    类结构关系如下:
    在这里插入图片描述

工厂方法模式实际上是为了解耦对象的实例,一般我们都是自己手动来通过new关键字来获得一个对象的实例化,但是如果这个实例化的对象有一个共有特征(能够抽象为一个共有父类),并且在可预期的未来,可能还会有扩展,这个时候我们可以通过工厂方法模式,将该类的实例化步骤提取出来内聚到一起,或者说在实例化的时候需要基于不同的条件实例化不同类型,我们可以将这部分提取出来,通过工厂方法模式来提供统一的访问。

优点
  • 良好的封装,客户端需要一个对象并不需要知道这个对象具体的创建过程,只需要知道这个产品的相关信息,交由工厂去创建,降低了模块之间的耦合
  • 在不改变产品类型的情况下,可以很方便的实现扩展。工厂方法在不改变产品类型时,扩展只需要实现实现Provider 提供的方法即可
缺点
  • 如上所示,工厂方法模式的缺点主要在于具体的产品需要和工厂耦合。
抽象工厂模式(Abstract Factory)

目标:为创建一组相关或相互依赖的对象提供一个接口,而无需指定他们的具体类。这里可以这样简单理解,工厂方法是为了创建某个类型产品,而抽象工厂模式,则是为了创建一个产品族。
拿我们常见的生产汽车来举例:

  • 对于工厂方法模式来说,关心的是生产的车,比如生成奔驰车,宝马车
  • 对于抽象工厂模式,则关注的是生产汽车中的各个部件,比如:轮胎,发动机,变速箱
    一般抽象工厂模式包含构件如下:
  • AbstractFactory:抽象工厂,定义创建一些产品的接口,如汽车工厂,定义 创建轮胎、发动机、变速箱的创建接口
  • ConcreteFactory:具体的工厂,实现抽象工厂定义的方法,实现一系列产品的创建,如创建奔驰汽车工厂,需要实现创建奔驰汽车需要的轮胎、发动机、变速箱;宝马汽车工厂,需要实现创建宝马汽车需要的轮胎、发动机、变速箱
  • Product: 定义一类产品对象的接口
  • ConcreteProduct:具体产品对象

类结构关系如下:
在这里插入图片描述

优点
  • 分离了抽象和实现。客户端使用抽象工厂来创建需要的对象,但是客户端不需要知道具体的实现,客户端只需要知道这类产品的抽象,不用关心具体的实现,,实现了抽象和实现的分离
  • 切换产品簇变得容易。如果要获取奔驰车相关的产品,直接用奔驰车相关的抽象工厂,宝马这则切换到宝马车即可
缺点
  • 不容易扩展新的产品。如果要给产品簇增加一个新的产品,则需要修改抽象工厂,同时所有已经实现的工厂类都需要调整
使用场景
单例模式(Singleton)

目标:确保一个类仅有一个实例,并提供一个全局访问点

优点
  • 优化了资源,由于单例模式全局只存在一个实例,减少了资源和系统的性能开销
缺点
  • 单例模式一般没有接口,扩展比较困难
使用场景
建造者模式(Builder)

目标:将一个复杂对象的构建与他的表示分离,使同样的构建过程可以创建不同的表示

其构件如下:

  • Builder:生成器接口,定义一个创建Product对象实例所需的各个部件的操作
  • ConcreteBuilder:具体的生成器的实现,实现各个部件的创建,并组装成一个Product对象实例
  • Director:主要是使用Builder接口,确定构件过程中需要哪些部件怎么初始化这些部件最终获取到一个Product实例
  • Product: 需要构建的复杂对象,包含多个部件

类结构关系如下:
在这里插入图片描述

建造者模式实际上是将一个复杂对象的构建过程进行了拆解,让调用者可以随意组合来构建对象。可以理解为建造者模式将构建复杂对象的过程抽象为一个算法,通过建造者模式将复杂对象的实例化解耦出来。

使用场景
原型模式(Prototype)

目标:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
原型模式实际上就是通过一个对象克隆自身的接口,拷贝这个对象本身来创建一个新的实例,而无需通过new来创建,相当于是复制了一份这个对象。
原型模式的功能点主要在两个:

  • 通过调用克隆方法来创建新的实例
  • 克隆的实例复制原来实例的属性值
使用场景

结构类模式

结构类模式主要有:桥接模式、适配器模式、代理模式、装饰模式、组合模式、门面模式、享元模式

桥接模式(Bridge)

目标:将抽象和实现分离,使得二者可以独立的变化
构件如下:

  • Abstraction 定义的抽象部分的接口,一般在这个对象中会持有一个实现部分对象的引用,抽象接口的实现部分需要调用实现对象的方法来完成。抽象部分的接口一般是和具体业务相关的方法
  • RefinedAbstraction:扩展抽象部分的接口。定义跟业务方法,这些方法一般可能会使用Abstraction中定义的方法,也可能需要调用实现部分的对象来完成
  • Implementor:定义实现部分的接口,这里的接口不需要和Abstraction中的方法一致,一般这个对象中提供方法供Abstraction使用,相当于是一个低层次,而Abstraction是一个较高层次的调用
  • ConcreteImplmentor:实现Implementor中定义的方法

类结构关系如下:
在这里插入图片描述

桥接模式的主要目标就是分离抽象和实现部分,在java中比较明显的例子就是JDBC,java定义了JDBC的抽象接口,与具体数据无关,但是实际实现由各个数据库厂商自己实现,通过DriverManager来进行抽象和实现部分的桥接,将JDBC的接口与数据库的具体实现耦合到一起。桥接模式可以让两个不同维度的实现分离,各自发展,通过桥接将二者组合到一起,即所谓的桥。

优点
  • 将抽象和实现分离。这是桥接模式的主要目的,通过桥接模式,实现可以不受抽象的约束,不用和抽象绑定在同一个层次上面
  • 良好的扩展能力。由于抽象和接口进行了分离,如果对外接口层允许变化,那么抽象和实现可以独自随意扩展
  • 对客户是透明的。客户调用的是抽象部分的接口,不关注具体的实现细节
缺点
使用场景
  • 不希望或不适用通过继承来实现抽象。如:不希望抽象和实现部分采用固定的绑定关系(继承),可以通过桥接模式来分离抽象和实现;如果采用继承方式会产生很多子类,这时可以分析变化的原因,分离成不同维度,通过桥接模式分离不同维度,从而减少子类数目
  • 复用性较高的场景。设计的粒度越细,越有可能被重用,而采用继承的话会受到继承层次的限制。
适配器模式(Adapter)

目标:将一个借口转换成客户希望的例外一个接口,使得原本由于接口不兼容的类可以一起工作
主要构件如下:

  • Target:定义客户端期望使用的接口
  • Adaptee:需要被适配的接口,该接口已经存在并实现
  • Adapter:适配器,将Adaptedd转换成Target接口
  • Client:客户端,调用自己期望的Target接口

适配器模式有类适配器和对象适配器:

  • 类适配器。类适配器通过继承来实现接口适配,Adapter同时继承了Target和Adaptee,但是java里不能多继承,一般只能将Target为interfat类型,这样Adapter extends Adaptee implements Target
  • 对象适配器。对象适配器主要是Adapter持有一个Adaptee的引用,在Adapter直接使用Adaptee的实例引用

类适配器 类结构关系如下:
在这里插入图片描述
对象适配器模式 类结构关系如下::
在这里插入图片描述

适配器模式的主要目的是复用现有的功能,将现有功能进行转化能够匹配预期的目标接口。

优点
  • 提高了类的复用度
  • 可以让两个没有任何联系的类一起运行
使用场景
  • 当我们存在一个已有的功能,新的需求该功能接口已经实现了,但是其接口不符合需求,这时候可以使用适配器模式将已有的实现转换成需要的接口
  • 如果想建立一个可以服用的类,但是这个类需要和其他一些不兼容的类一起工作,这时候可以使用适配器模式
  • 如果想使用一些已经存在的子类,但是不可能对每一个子类都进行适配,这时候可以用对象适配器,直接适配父类即可
代理模式(Proxy)

目标:为其他对象提供一种代理以控制对这个对象的访问
主要构件如下:

  • Subject:目标接口,要代理访问的地方
  • RealSubject:目标接口实现,具体的目标对象
  • Proxy:代理对象,一般代理对象会实现Subject一样的接口,类似模拟Subject的子类,但是其会持有一个RealSubject的具体实现的引用,当调用Proxy接口的时候,会进行一定的控制处理,如果没有问题,最终会调用RealSubject的实现去处理

类结构关系如下:
在这里插入图片描述

代理模式是通过创建一个代理对象,用这个代理对象去模拟代表真实的对象,对于客户端而言,由于是面向接口编程,即客户端只知道Subject,对于Proxy的实现并不关心,而代理的实际实现是通过RealSubject去实现的,而代理实际上是中间的一个中转,这样的话,我们在代理上就可以实现一些额外的功能,比如判断权限,记录日志等。
在spring中常见的AOP编程就是通过代理实现的,如果类配置了相关切面,我们在程序中获得的实际上并不是该类,而是其生成的一个代理类
代理模式的本质其实就是控制对象访问。引入代理之后,在客户端和目标对象之间引入了一个中间层,在这个中间层中加入自己的逻辑

优点
缺点
使用场景
装饰模式(Decorator)

目标:动态的给一个对象添加额外的职责,单就增加功能来说,装饰模式比生成子类更灵活
其构件主要如下:

  • Componet:抽象模块,定义接口
  • ConcreteComponet:具体实现,被装饰的类
  • Decorator:抽象装饰器,继承Componet,并在内部持有一个具体的Componet引用
  • ConcreteCompone:具体的装饰器对象,实现给Componet接口添加功能

类结构关系如下:
在这里插入图片描述

优点
  • 装饰模式可以动态的扩展一个类的功能
  • 可以视为继承的一个替代方案
  • 装饰类和被装饰类可以独立发展
缺点

多层的装饰比较繁杂

使用场景
  • 需要扩展一个类的功能,或者给一类增加附件功能
  • 动态的给一个对象增加功能,这些功能可以动态的撤销
组合模式(Composite)

目标:将对象组合成竖向结构以表示部分-整体的层次结构,使用户对单个对象的访问和组合对象的访问具有一致性
其构件如下:

  • Component:抽象构件角色,定义外部访问的结构
  • Leaf:叶子节点对象,地应以和实现叶子对象的行为,不在包含其他的子节点对象,是最小的单位
  • Composite:组合对象(也称树枝节点),组合树枝节点和叶子结点

类结构关系如下:
在这里插入图片描述

组合模式是为了让客户端不区分操作的是组合对象还是叶子对象,而是用一个统一的方式来操作,通过抽象一个抽象构件角色来代表组合对象和叶子对象,对客户端而言,访问的就是抽象构件角色

优点
  • 对外提供了统一的视图,客户端调用不管是叶子节点还是树枝节点,都统一视为一个Component
  • 可以自由增加节点,使用组合模式,增加树枝节点和树叶节点都很方便
缺点

树枝节点和树叶节点是直接实现的,不是面向接口编程的

使用场景
外观模式(Facade)也称门面模式

目标:为子系统中的一组接口提供一个一致的界面,外观模式提供一个高层次的接口,使得子系统更加容易使用
其构件如下:

  • Facade:定义子系统中多个模块对外的高层接口
  • subsystem:子系统,可以同时拥有一个或多个子系统,每一个子系统都不是一个单独类,子系统并不知道Facade的存在,Facade实时它的一个客户端

外观模式并不是给子系统添加新的功能接口,而是为了让外部系统减少与子系统内的多个模块交互,松散耦合

优点
  • 松散耦合,外观模式松散了外部客户端和子系统内部的多个模块,减少了客户端和子系统各个模块之间的依赖,客户端只和Facade产生依赖
  • 简单易用,外观模式是得客户端只需要关心Facade,不需要关心内部各个模块之间的调用关系
缺点

外观模式中Facade不符合开闭原则,对修改关闭对扩展开放

使用场景
  • 为一个复杂的子系统或模块对外提供一个统一的访问接口
  • 子系统相对独立,外界对系统的访问字需要访问Facade即可
享元模式(Flyweight)

目标:使用共享技术有效地支持大量的细粒度对象
主要构件如下:

  • Flyweight:抽象享元角色,定义享元的接口
  • ConcreteFlyweight:具体享元角色,实现享元接口
  • unsharedConcreteFlyweight:不可共享的享元角色
  • FlyweightFactory:享元工厂,创建并管理共享的享元对象,职责非常简单,构造一个池容器,同时从池中获得对象的的方法

类结构关系如下:
在这里插入图片描述

享元模式的主要运用共享技术,是一些对象可以共享,是池化技术的来源,在系统中如果有多个相同的对象,只需要共享一份即可,而不必实例化每一个对象。

优点
缺点
使用场景

行为类模式

行为类模式主要有:责任链模式、观察者模式、策略模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、状态模式、模板方法模式、访问者模式

观察者模式(Observer)

目标:定义对象之间一种一对多的依赖关系,让一个或多个观察者对象监察一个主题对象,这样当一个主题对象状态发生改变的时候,所有观察者能够得到通知并被自动更新。
主要构件如下:

  • Subject:抽象主题角色,主题角色会将所有的观察者对象的引用保存在一个地方,同时可以添加或者移除观察者
  • Observer:抽象观察者角色,为所有观察者定义一个更新接口,给主题调用,当主题更新的时候调用观察者的更新接口,让其更新自己
  • ConcreteSubject:具体实现主题对象,维护主题状态,当主题状态发生改变时,通知所有观察者对象
  • ConcreteObserver:观察者对象的具体实现,接收主题对象的通知,并进行相应的处理

UML类结构关系如下:
在这里插入图片描述

优点
  • 观察者模式是在主题与观察者之间建立了一个抽象的耦合,并没有耦合到具体实现,因此这两个可以独立发展实现,抽象主题只知道抽象观察者,并不知道具体的观察实现
  • 观察者模式能够支持广播通信
缺点
  • 如果观察者太多,则广播可能会花费一定的时间
  • 如果主题之前有循环依赖,可能造成循环调用
使用场景
策略模式(Stragety)

目标:定义一组算法,并将其一个个封装起来,且可以相关替换,使得算法部分可以独立变化
其主要构件如下:

  • Context:上下文,负责与具体的策略交互
  • Strategy:策略接口,用来约束一系列具体算法,Context使用这个接口来调用具体的策略
  • ConcreteStrategy:具体的策略实现,即具体算法实现

类结构关系如下:
在这里插入图片描述

策略模式实际上是将解决问题的一类算法,抽象提取共有部分,分离算法,选择实现。

优点
  • 策略模式实际上是将解决同一个问题的不同算法进行了抽象,只要一个算法实现了抽象的策略算法,那么他就是这一组算法中的一个,这一组算法的每个算法可以自由切换
  • 避免了多重条件判断语句
缺点
  • 必须了解每种算法的不同
  • 增加了对象数目
  • 只适合扁平的算法结构
使用场景
命令模式(Command)

目标:将一个请求封装为一个对象(Command),这样我们可以用不同的请求对用户进行参数化,这样的话我们就能够对请求进行排队或者记录请求的日志以及可以支持撤销操作
其主要构件如下:

  • Command:定义命令的接口,声明执行的方法
  • ConcreteCommand:具体命令实现者,但注意这是一个虚的实现,通常会持有一个接受者,由接受者来完成实际要执行的操作
  • Receiver:接受者,真正执行命令的地方
  • Invoker:要求命令对象执行请求,一般会持有命令对象,可以持有很多的命令对象,相当于使用命令对象的入口
    命令模式实际上是将请求封装成了一个对象,定义了统一执行操作的接口,将请求和处理进行了解耦,本质就是封装请求。命令模式实际上是从界面开发中演化而来

类结构关系如下:
在这里插入图片描述

优点
  • 松散耦合。命令模式将用户和具体的执行对象(接受者)进行了解耦
  • 更加动态的控制。命令模式将请求封装起来,可以动态的参数化
缺点
使用场景

命令模式主要是将请求的调用者和具体实现者分离、解耦,

解释器模式(Interpreter)

目标:按照规定语法进行解析,给定一个语言,定义它的文法的一种表示,定义一个解释器,该解释器使用这个表示来解释语言中句子。
其主要构件如下:

  • AbstractExpression:抽象解释器,定义解释器的接口
  • TerminalExpression: 终结符表达式,实现解释器接口,实现语法规则中和终结符相关的操作,不在包含其他的解释器
  • NoneterminalExpression:非终结解释器,实现语法规则中非终结相关操作
  • Context:上下文
迭代器模式(Iterator)

目标:提供一个中方法访问一个容器中各个对象元素而又无需暴露该对象内部细节
其主要构件如下:

  • Iterator:抽象迭代器
  • ConcreteIterator:具体迭代器
  • Container:抽象容器角色
  • ConcreteContainer:具体容器角色

类结构关系如下:
在这里插入图片描述

优点
缺点
使用场景
中介者模式(Mediator)

目标:用一个中介对象来封装一系列对象的交互,使得个对象不需要显示的相互引用他,从而松散耦合,并且可以独立地改变他们之间的交互
其主要构件如下:

  • Mediator:中介者接口,定义各个同事对象之间交互需要的方法
  • ConcreteMediator:具体中介者实现对象,维护各个同事对象,并负责协调各个同事对象之间的交互关系
  • Colleague:同事类,每一个同事对象都知道中介者角色,并且与其他同事交互的时候一定要通过中介者协作
  • ConcreteColleague:具体的同事类,实现自己的业务,在需要与其他同事交互时与持有的中介者通信,由中介者负责与其他同事交互

中介者模式其实就是封装了对象之间的交互,以前可能一个同事类要与多个同事类交互,现在只需要与中介者交互即可,将原本直接相互依赖的对象分解,加入中介者这个角色,使得两头的对象只与中介者交互
在这里插入图片描述

优点
  • 松散耦合,将多个同事对象之间的交互封装到中介者中,使得同事对象之间松散耦合,不互相依赖,各自可以独立变化
  • 集中控制,同事类之间的交互都在中介者中,便于集中管理
缺点
  • 过度集中化
使用场景
备忘录模式(Memento)

目标:不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到原先保存的状态

其主要构件如下:

  • Originator:原发器,记录当前时刻的内部状态,负责定义哪些属于备份范围的状态,负责创建和恢复备忘录数据,
  • CareTaker:备忘录管理者,负责保存备忘录对象,不能检察或操作备忘录的内容
  • Memento:备忘录,将原发器的内部状态存储起来,但是具体要储存哪些数据由原发器对象来决定,另外备忘录只能由原发器对象来访问它内部的数据,原发器外部对象不应该访问到备忘录对象的内部数据
    通俗点来说,备忘录模式就是保存了一个对象的备份,本质是保存和恢复内部状态
优点
缺点
使用场景
  • 一个类需要保存它的对象的状态
状态模式(State)

目标:允许一个对象再起内部状态改变时改变它的行为,看起来这个对象似乎修改了它的类
其主要构件如下:

  • Context:上线文环境,定义客户端需要的接口,持有一个具体状态类的实例,并负责具体状态的转换
  • State:抽象状态角色,定义一个接口,用来封装与上下文的一个特定状态对应的行为
  • ConcreteState:具体实现状态处理的类

类结构关系如下:
在这里插入图片描述

状态模式主要是根据状态来分离和选择行为,这里的状态一般指的是对象的一个属性,行为指的就是对象的功能,通过维护状态的变化,来调用不同状态对应的不同功能,状态和行为是相互关联的。

优点
  • 分离了状态和行为,避免了为了判断状态而产生的if else判断,将对象的行为交给状态类维护之后,对于上层模块来说,只需要维护状态之间的转换规则
缺点
  • 某些场景下会导致有过多具体状态类
使用场景

状态模式跟策略模式有点类似,不同的是,策略模式是需要别人调用执行,而而状态模式则是自己。没有用状态模式,则是多个if else 语句判断,在调用context执行对应方法,而状态模式则将context给了状态,让状态自己选择应该调用context执行什么逻辑

模板方法模式(Template Method)

目标:定义一个操作中算法的骨架,而降一些步骤延迟到子类中,模板方法是得子类可以在不改变一个算法的机构即可重新定义该算法的某些特定步骤
其主要构件如下:

  • AbstractClass:抽象类,定义算法骨架
  • ConcreteClass:具体实现类,用来实现算法骨架中某些步骤,完成与特定子类相关的功能
    模板方法在于固定了算法的骨架,而让具体的算法实现可扩展
优点
  • 实现了代码的复用,将公用部分提取出来放到父类的模板中实现
缺点
  • 算法骨架定义下来后不好升级
使用场景
访问者模式(Visitor)

目标:在不改变已有程序结构的前提下,通过添加额外的访问者来对现有代码功能实现提升
其主要构件如下:

  • Visitor:访问者接口,为所有访问者对象生命一个visit方法,该接口的名字和参数表示了发送访问请求给具体访问者的具体元素角色,这样访问者角色酒而已通过该元素角色的特定接口直接访问它
  • Concrete Visitor:具体访问者,实现真正要被添加到对象结构中的功能
  • Element:抽象的元素对象,定义接受访问的操作
  • ConcreteElement:具体元素对象,对象结构中具体的对象,即被访问的对象,通常会回调访问者的真实功能,同时开放自身的数据供访问者使用
  • ObjectStructure:对象结构,包含多个被访问的对象,可以遍历多个被访问的对象,也可以让访问者访问它的元素
    类结构关系如下:
    在这里插入图片描述
优点
  • 访问者模式可以给一系列对象透明地添加功能,增加新功能非常方便
缺点
  • 增加新的节点类很困难
  • 破坏了封装,访问者模式要求访问者能够访问并调用每一个节点对象的操作,暴露了节点的内部状态和操作
使用场景
职责链(Chain of Responsibility)

目标:避免请求的发送者和接收者之间耦合,使多个对象都有机会处理请求,将这些对象连成一条链,沿着这条链传递该请求,知道有对象处理它为止
其主要构件如下:

  • Handler:定义职责的接口,也可以在这里实现后继链
  • ConcreteHandler:实现职责接口,可以选择将请求处理掉,也可以将请求传给下家
    类结构关系如下:
    在这里插入图片描述
优点
  • 请求者和接受者松散耦合,在职责链模式中请求者并不知道接收者是谁,只是向职责链发出请求即可
  • 动态组合职责
缺点
  • 产生很多细粒度对象
  • 请求不一定能够被处理
使用场景
评判好代码的标准:
  • 可维护性。落实到编码开发,所谓的“维护”无外乎就是修改 bug、修改老的代码、添加新的代码之类的工作。所谓“代码易维护”就是指,在不破坏原有代码设计、不引入新的 bug 的情况下,能够快速地修改或者添加代码。所谓“代码不易维护”就是指,修改或者添加代码需要冒着极大的引入新 bug 的风险,并且需要花费很长的时间才能完成。我们知道,对于一个项目来说,维护代码的时间远远大于编写代码的时间。工程师大部分的时间可能都是花在修修 bug、改改老的功能逻辑、添加一些新的功能逻辑之类的工作上。所以,代码的可维护性就显得格外重要。维护、易维护、不易维护这三个概念不难理解。不过,对于实际的软件开发来说,更重要的是搞清楚,如何来判断代码可维护性的好坏。实际上,可维护性也是一个很难量化、偏向对代码整体的评价标准,它有点类似之前提到的“好”“坏”“优雅”之类的笼统评价。代码的可维护性是由很多因素协同作用的结果。代码的可读性好、简洁、可扩展性好,就会使得代码易维护;相反,就会使得代码不易维护。更细化地讲,如果代码分层清晰、模块化好、高内聚低耦合、遵从基于接口而非实现编程的设计原则等等,那就可能意味着代码易维护。除此之外,代码的易维护性还跟项目代码量的多少、业务的复杂程度、利用到的技术的复杂程度、文档是否全面、团队成员的开发水平等诸多因素有关。所以,从正面去分析一个代码是否易维护稍微有点难度。不过,我们可以从侧面上给出一个比较主观但又比较准确的感受。如果 bug 容易修复,修改、添加功能能够轻松完成,那我们就可以主观地认为代码对我们来说易维护。相反,如果修改一个 bug,修改、添加一个功能,需要花费很长的时间,那我们就可以主观地认为代码对我们来说不易维护。你可能会说,这样的评价方式也太主观了吧?没错,是否易维护本来就是针对维护的人来说的。不同水平的人对于同一份代码的维护能力并不是相同的。对于同样一个系统,熟悉它的资深工程师会觉得代码的可维护性还不错,而一些新人因为不熟悉代码,修改 bug、修改添加代码要花费很长的时间,就有可能会觉得代码的可维护性不那么好。这实际上也印证了我们之前的观点:代码质量的评价有很强的主观性。
  • 可读性。们又该如何评价一段代码的可读性呢?我们需要看代码是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合等等。你应该也能感觉到,从正面上,我们很难给出一个覆盖所有评价指标的列表。这也是我们无法量化可读性的原因。实际上,code review 是一个很好的测验代码可读性的手段。如果你的同事可以轻松地读懂你写的代码,那说明你的代码可读性很好;如果同事在读你的代码时,有很多疑问,那就说明你的代码可读性有待提高了。
  • 可扩展性。它表示我们的代码应对未来需求变化的能力。跟可读性一样,代码是否易扩展也很大程度上决定代码是否易维护。那到底什么是代码的可扩展性呢?代码的可扩展性表示,我们在不修改或少量修改原有代码的情况下,通过扩展的方式添加新的功能代码。说直白点就是,代码预留了一些功能扩展点,你可以把新功能代码,直接插到扩展点上,而不需要因为要添加一个功能而大动干戈,改动大量的原始代码。关于代码的扩展性,在后面讲到“对修改关闭,对扩展开放”这条设计原则的时候,我会来详细讲解,今天我们只需要知道,代码的可扩展性是评价代码质量非常重要的标准就可以了。- 灵活性
  • 灵活性。尽管有很多人用这个词汇来描述代码的质量。但实际上,灵活性是一个挺抽象的评价标准,要给灵活性下个定义也是挺难的。不过,我们可以想一下,什么情况下我们才会说代码写得好灵活呢?我这里罗列了几个场景,希望能引发你自己对什么是灵活性的思考。当我们添加一个新的功能代码的时候,原有的代码已经预留好了扩展点,我们不需要修改原有的代码,只要在扩展点上添加新的代码即可。这个时候,我们除了可以说代码易扩展,还可以说代码写得好灵活。当我们要实现一个功能的时候,发现原有代码中,已经抽象出了很多底层可以复用的模块、类等代码,我们可以拿来直接使用。这个时候,我们除了可以说代码易复用之外,还可以说代码写得好灵活。当我们使用某组接口的时候,如果这组接口可以应对各种使用场景,满足各种不同的需求,我们除了可以说接口易用之外,还可以说这个接口设计得好灵活或者代码写得好灵活。从刚刚举的场景来看,如果一段代码易扩展、易复用或者易用,我们都可以称这段代码写得比较灵活。所以,灵活这个词的含义非常宽泛,很多场景下都可以使用。
  • 简洁性。有一条非常著名的设计原则,你一定听过,那就是 KISS 原则:“Keep It Simple,Stupid”。这个原则说的意思就是,尽量保持代码简单。代码简单、逻辑清晰,也就意味着易读、易维护。我们在编写代码的时候,往往也会把简单、清晰放到首位。
  • 可复用性。代码的可复用性可以简单地理解为,尽量减少重复代码的编写,复用已有的代码。在后面的很多章节中,我们都会经常提到“可复用性”这一代码评价标准。比如,当讲到面向对象特性的时候,我们会讲到继承、多态存在的目的之一,就是为了提高代码的可复用性;当讲到设计原则的时候,我们会讲到单一职责原则也跟代码的可复用性相关;当讲到重构技巧的时候,我们会讲到解耦、高内聚、模块化等都能提高代码的可复用性。可见,可复用性也是一个非常重要的代码评价标准,是很多设计原则、思想、模式等所要达到的最终效果。实际上,代码可复用性跟 DRY(Don’t Repeat Yourself)这条设计原则的关系挺紧密的,所以,在后面的章节中,当我们讲到 DRY 设计原则的时候,我还会讲更多代码复用相关的知识,比如,“有哪些编程方法可以提高代码的复用性”等。
  • 可测试性。 代码可测试性的好坏,能从侧面上非常准确地反应代码质量的好坏。代码的可测试性差,比较难写单元测试,那基本上就能说明代码设计得有问题。

可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。

如何解耦
  1. 封装与抽象,听过封装和抽象隐藏实现的细节,隔离实现的易变性,给相关的依赖提供稳定的接口
  2. 中间层,通过引入中间层简化模块或类之间的依赖关系
  3. 模块化。将系统划分不同模块,模块之间通过接口交互,模块内部高内聚,实现模块间的低耦合
  4. 单一职责原则、面向接口编程、多用组合少用继承、迪米特法则、
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值