设计模式(Design pattern)
设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验、错误和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱。可以理解为“套路”。
JAVA一共有23种设计模式。
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模
式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、
命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
具体如下:
创建型
一、Singleton,单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问点。
二、Abstract Factory,抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们的具体类。
三、Factory Method,工厂方法:定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method使一个类的实例化延迟到了子类。
四、Builder,建造模式:将一个复杂对象的构建与他的表示相分离,使得同样的构建过程可以创建不同的表示。
五、Prototype,原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。
行为型
六、Iterator,迭代器模式:提供一个方法顺序访问一个聚合对象的各个元素,而又不需要暴露该对象的内部表示。
七、Observer,观察者模式:定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知自动更新。
八、Template Method,模板方法:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,TemplateMethod使得子类可以不改变一个算法的结构即可以重定义该算法得某些特定步骤。
九、Command,命令模式:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队和记录请求日志,以及支持可撤销的操作。
十、State,状态模式:允许对象在其内部状态改变时改变他的行为。对象看起来似乎改变了他的类。
十一、Strategy,策略模式:定义一系列的算法,把他们一个个封装起来,并使他们可以互相替换,本模式使得算法可以独立于使用它们的客户。
十二、China of Responsibility,职责链模式:使多个对象都有机会处理请求,从而避免请求的送发者和接收者之间的耦合关系。
十三、Mediator,中介者模式:用一个中介对象封装一些列的对象交互。
十四、Visitor,访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这个元素的新操作。
十五、Interpreter,解释器模式:给定一个语言,定义他的文法的一个表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
十六、Memento,备忘录模式:在不破坏对象的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
结构型
十七、Composite,组合模式:将对象组合成树形结构以表示部分整体的关系,Composite使得用户对单个对象和组合对象的使用具有一致性。
十八、Facade,外观模式:为子系统中的一组接口提供一致的界面,fa?ade提供了一高层接口,这个接口使得子系统更容易使用。
十九、Proxy,代理模式:为其他对象提供一种代理以控制对这个对象的访问。
二十、Adapter,适配器模式:将一类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。
二十一、Decrator,装饰模式:动态地给一个对象增加一些额外的职责,就增加的功能来说,Decorator模式相比生成子类更加灵活。
二十二、Bridge,桥模式:将抽象部分与它的实现部分相分离,使他们可以独立的变化。
二十三、Flyweight,享元模式
其实还有两类:并发型模式和线程池模式。用一个图片来整体描述一下:
面向对象的设计模式的六大原则
OCP 开闭原则(Open-Closed Principle)
一个软件的实体应当对扩展开放,对修改关闭。
当我们写完的代码,不能因为需求变化就修改。我们可以通过新增代码的方式来解决变化的需求。如果每次需求变动都去修改原有的代码,那原有的代码就存在被修改错误的风险,当然这其中存在有意和无意的修改,都会导致原有正常运行的功能失效的风险,这样很有可能会展开可怕的蝴蝶效应,使维护工作剧增。
说到底,开闭原则除了表面上的可扩展性强以外,在企业中更看重的是维护成本。所以,开闭原则是设计模式的第一大原则,它的潜台词是:控制需求变动风险,缩小维护成本。
LSP 里氏代换原则(Liskov Substitution Principle)
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换
原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP 是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
ISP 接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。
DIP 依赖倒转原则(Dependence Inversion Principle)
要针对接口编程,不要针对实现编程。
如果 A 中关联 B,那么尽量使得 B 实现某个接口,然后 A 与接口发生关系,不与 B 实现类发生关联关系。
依赖倒置的潜台词是:面向抽象编程,解耦调用和被调用者。
LOD 迪米特法则(Law Of Demeter)
只与你直接的朋友通信,而避免和陌生人通信。
要求尽量的封装,尽量的独立,尽量的使用低级别的访问修饰符。这是封装特性的典型体现。
一个类如果暴露太多私用的方法和字段,会让调用者很茫然。并且会给类造成不必要的判断代码。所以,我们使用尽量低的访问修饰符,让外界不知道我们的内部。这也是面向对象的基本思路。这是迪米特原则的一个特性,无法了解类更多的私有信息。
另外,迪米特原则要求类之间的直接联系尽量的少,两个类的访问,通过第三个中介类来实现。
迪米特原则的潜台词是:不和陌生人说话,有事去中介。
CRP 合成复用原则(Composite Reuse Principle)
原则是尽量使用合成/聚合的方式,而不是使用继承。
单例设计模式 (Singleton)
· 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
· 如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,这样,就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。
· 单例模式可以说是大多数开发人员在实际中使用最多的,常见的Spring默认创建的bean就是单例模式的。
· 单例模式有很多好处,比如可节约系统内存空间,控制资源的使用。其中单例模式最重要的是确保对象只有一个。
· RunTime就是典型的单例设计,我们通过对RunTime类的分析,一窥究竟。
public class Runtime {
//1.创建静态的全局唯一的对象
private static Runtime currentRuntime = new Runtime();
//2.私有化构造方法,不让外部来调用
/** Don't let anyone else instantiate this class */
private Runtime() {}
//3.通过自定义的静态方法获取实例
public static Runtime getRuntime() {
return currentRuntime;
}
}
通过分析,底层的实现思路一共分为了3个步骤:
- 对本类构造方法私有化,防止外部调用本类的构造方法创建对象
- 创建本类对象(全局唯一)且私有–为了防止外部多次获取本类对象
- 通过自定义的公共方法将创建好的对象返回(类似封装属性后的getXxx() )
单例设计模式-饿汉式
class Singleton {
// 1.私有化构造器
private Singleton() {
}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single = new Singleton();
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
return single;
}
}
Tips:
构造方法和对象私有化后,通过公共的访问点来获取对象,那外界如何调用这个公共方法呢?
- 之前我们都是在外部创建本类对象并进行方法调用,但是现在单例程序中外部无法直接创建本类对象。
解决方案:我们可以利用之前学习的静态的概念,将方法修饰成静态的,就可以通过类名直接调用。
(静态只能调用静态,所以静态方法中返回的对象也需用静态修饰)
单例设计模式-懒汉式
class Singleton {
// 1.私有化构造器
private Singleton() {
}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single;
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
if(single == null) {
single = new Singleton();
}
return single;
}
}
这样写的懒汉式暂时还存在线程安全问题,可修复(若在多线程程序中 + 有共享数据 + 多条语句操作共享数据就一定会存在线程安全问题。)
- 解决方案1:同步代码块[加锁,范围是操作共享资源的所有代码]
- 解决方案2:同步方法[如果方法中的所有代码都需要被同步,那么这个方法可以修饰成同步方法]
注意事项:锁对象在静态方法中,不可以使用this,因为静态资源优先于对象加载锁对象。
可以使用外部创建好的唯一的锁对象 static Object o = new Objec(); 或者字节码文件 类名.class 但请注意,静态只能调用静态。
单例设计模式-懒汉式(线程安全)
public class SingletonTest{
public static void main(String[] args){
Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
System.out.println(s1==s2);
} }
单例模式的优点:
· 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
单例设计模式-应用场景
- 网站的计数器,一般也是单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
- 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取。
- Application 也是单例的典型应用。
- Windows的Task Manager (任务管理器)就是很典型的单例模式。
- Windows的Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
模板方法设计模式(TemplateMethod)
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
解决的问题
- 当功能内部一部分实现是确定的,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。
- 换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。
例一:
计算一个方法的执行完毕的用时
abstract class Template {
public final void getTime() {
long start = System.currentTimeMillis();
code();
long end = System.currentTimeMillis();
System.out.println("执行时间是:" + (end - start));
}
public abstract void code();
}
class SubTemplate extends Template {
public void code() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
}
}
}
例二:
package com.atguigu.java;
//抽象类的应用:模板方法的设计模式
public class TemplateMethodTest {
public static void main(String[] args) {
BankTemplateMethod btm = new DrawMoney();
btm.process();//调用父类BankTemplateMethod 中的方法
BankTemplateMethod btm2 = new ManageMoney();
btm2.process();//调用父类BankTemplateMethod 中的方法
}
}
abstract class BankTemplateMethod {
// 具体方法
public void takeNumber() {
System.out.println("取号排队");
}
public abstract void transact(); // 办理具体的业务 //钩子方法
public void evaluate() {
System.out.println("反馈评分");
}
// 模板方法,把基本操作组合到一起,子类一般不能重写
public final void process() {
this.takeNumber();//取号排队
this.transact();// 像个钩子,具体执行时,挂哪个子类,就执行哪个子类的实现代码
this.evaluate();//反馈评分
}
}
class DrawMoney extends BankTemplateMethod {
public void transact() {
System.out.println("我要取款!!!");
}
}
class ManageMoney extends BankTemplateMethod {
public void transact() {
System.out.println("我要理财!我这里有2000万美元!!");
}
}
模板方法设计模式-应用场景
模板方法设计模式是编程中经常用得到的模式。各个框架、类库中都有他的影子,比如常见的有:
- 数据库访问的封装
- Junit单元测试
- JavaWeb的Servlet中关于doGet/doPost方法调用
- Hibernate中模板程序
- Spring中JDBCTemlate、HibernateTemplate等
代理设计模式(Proxy)
现实生活中经常听到一个词“代理”,比如某某酒品牌什么什么省总代理;还有像现在的明星都有自己的经纪人,有事需要找他们的时候就要先找他们的经纪人,经纪人也相当于是一个代理;再比如打官司都需要找一个律师,有什么问题直接由律师去沟通解决,那律师就是自己的一个代理。
生活中的代理随处可见,Java中也存在一种设计模式,就叫代理模式,并且使用非常普遍,很多著名框架和开源项目的源码中都有大量使用代理模式,比如比较流行的Spring、MyBatis等,都有用到代理模式。特别是MyBatis,代理模式随处可见。那到底什么是代理模式呢?
代理模式也叫做委托模式,是指为其他对象提供一种代理以控制对这个对象的访问。代理模式可以分为静态代理和动态代理,动态代理又分为JDK提供的动态代理和CGLIB动态代理,具体实现及原理下面会进行详细介绍。
代理模式的三个角色
代理模式中存在三个角色:
-
抽象角色:通过接口或抽象类声明真实角色实现的业务方法。
-
代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。
-
真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。
这三个角色之间的关系可以用一张简单的类图来表示:
静态代理(静态定义代理类)
静态代理很简单,代理角色和真实角色继承同一个父类或实现相同的接口,使用时只需要把真实角色传递给代理角色,代理角色调用真实角色的方法就可以了。
package com.atguigu.java;
public class StaticProxyTest {
public static void main(String[] args) {
Star s = new Proxy(new RealStar());
s.confer();
s.signContract();
s.bookTicket();
s.sing();
s.collectMoney();
}
}
interface Star {
void confer();// 面谈
void signContract();// 签合同
void bookTicket();// 订票
void sing();// 唱歌
void collectMoney();// 收钱
}
class RealStar implements Star {
public void confer() {
}
public void signContract() {
}
public void bookTicket() {
}
public void sing() {//唱歌,自己唱,其他交给代理类
System.out.println("明星:歌唱~~~");
}
public void collectMoney() {
}
}
class Proxy implements Star {
private Star real;//被代理类对象
public Proxy(Star real) {
this.real = real;
}
public void confer() {
System.out.println("经纪人面谈");
}
public void signContract() {
System.out.println("经纪人签合同");
}
public void bookTicket() {
System.out.println("经纪人订票");
}
public void sing() {
real.sing();
}
public void collectMoney() {
System.out.println("经纪人收钱");
}
}
优点:
-
真实角色类(业务类)只需要关注业务逻辑本身,这是代理模式的共有优点;
-
代理类可以在调用业务类的处理方法之前和之后做一些增强性的操作,比如记录日志、管理事务等,这也是代理模式共有的优点。
缺点:
-
代理类和业务类实现了相同的接口,并且实现了相同的方法,代码冗余。如果接口增加一个方法,所有的代理类和所有的实现类都需要增加这个方法的实现,不易维护;
-
代理对象只能代理同一种类型的对象,如果要对多种类型的对象进行代理,就要写多个代理类,这就会大大增加类文件的数量,不适合在大规模程序中使用。
针对静态代理的这些缺点,JDK 提供了动态代理。
动态代理(动态生成代理类)
- JDK自带的动态代理,需要反射等知识
动态代理的代理类是在程序运行的时候由Java反射机制动态生成的。我们在上面静态代理的例子中,代理类GamePlayerProxy是自己定义好的,在程序运行之前就已经编译完成。我们先来看下如何实现JDK的动态代理。
Java的 java.lang.reflect 包下提供了一个Proxy类和一个InvocationHandler接口,JDK动态代理的实现主要依靠这两个对象实现。
这个类中持有一个被代理对象的实例 target,还有一个 invoke 方法,这个方法中通过反射机制调用被代理对象target的method方法,所有执行代理对象的方法都会被替换成执行invoke方法。
CGLib动态代理
CGLib动态代理是代理类去继承目标类,然后重写其中目标类的方法。这是它与JDK动态代理的一个主要区别。
- 引入CGLib的依赖。
- 编写方法拦截器。这个拦截器实现了MethodInterceptor接口
- 创建被代理的目标类。
- 通过Enhancer对象创建代理类,执行被代理方法。
通过以上4步我们就完成了CGLib动态代理实现游戏代练的场景。
代理模式-应用场景
- 安全代理:屏蔽对真实角色的直接访问。
- 远程代理:通过代理类处理远程方法调用(RMI)
- 延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象比如你要开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有100MB,在打开文件时,不可能将所有的图片都显示出来,这样就可以使用代理模式,当需要查看图片时,用proxy来进行大图片的打开。
工厂设计模式(Factory Method)
工厂模式:实现了创建者与调用者的分离,即将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。其实设计模式和面向对象设计原则都是为了使得开发项目更加容易扩展和维护,解决方式就是一个“分工”。
社会的发展也是这样,分工越来越细。
- 原始社会的人:人什么都要会,自己种,自己打猎,自己织衣服,自己治病…
- 现在的人:可以只会一样,其他都不会,只会 Java 也能活,不会做饭,不会开车,不会…
工厂模式的分类
- 简单工厂模式:用来生产同一等级结构中的任意产品。(对于增加新的产品,需要修改已有代码)
- 工厂方法模式:用来生产同一等级结构中的固定产品。(支持增加任意产品)
- 抽象工厂模式:用来生产不同产品族的全部产品。(对于增加新的产品,无能为力;支持增加产品族)
GOF 在《设计模式》一书中将工厂模式分为两类:工厂方法模式(Factory Method)与抽象工厂模式(Abstract Factory)。将简单工厂模式(Simple Factory)看为工厂方法模式的一种特例,两者归为一类。
核心本质:
- 实例化对象,用工厂方法代替 new 操作。
- 将选择实现类、创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。
1、无工厂模式
package com.atguigu.pattern.factory.nofactory;
interface Car{
void run();
}
class Audi implements Car{
public void run() {
System.out.println("奥迪在跑");
}
}
class BYD implements Car{
public void run() {
System.out.println("比亚迪在跑");
}
}
public class Client01 {
public static void main(String[] args) {
Car a = new Audi();
Car b = new BYD();
a.run();
b.run();
}
}
2、简单工厂模式
简单工厂模式,从命名上就可以看出这个模式一定很简单。它存在的目的很简单:
定义一个用于创建对象的工厂类。
package com.atguigu.pattern.factory.simple;
interface Car {
void run();
}
class Audi implements Car {
public void run() {
System.out.println("奥迪在跑");
}
}
class BYD implements Car {
public void run() {
System.out.println("比亚迪在跑");
}
}
//工厂类
class CarFactory {
//方式一
public static Car getCar(String type) {
if ("奥迪".equals(type)) {
return new Audi();
} else if ("比亚迪".equals(type)) {
return new BYD();
} else {
return null;
}
}
//方式二
public static Car getAudi() {
return new Audi();
}
public static Car getByd() {
return new BYD();
}
}
public class Client02 {
public static void main(String[] args) {
Car a = CarFactory.getCar("奥迪");
a.run();
Car b = CarFactory.getCar("比亚迪");
b.run();
}
}
调用者只要知道他要什么,从哪里拿,如何创建,不需要知道。分工,多出了一个专门生产 Car 的实现类对象的工厂类。把调用者与创建者分离。
小结:
简单工厂模式也叫静态工厂模式,就是工厂类一般是使用静态方法,通过接收的参数的不同来返回不同的实例对象。
缺点:对于增加新产品,不修改代码的话,是无法扩展的。违反了开闭原则(对扩展开放;对修改封闭)。
3、工厂方法模式
为了避免简单工厂模式的缺点,不完全满足 OCP(对扩展开放,对修改关闭)。
工厂方法模式和简单工厂模式最大的不同在于,简单工厂模式只有一个(对于一个项目或者一个独立的模块而言)工厂类,而工厂方法模式有一组实现了相同接口的工厂类。这样在简单工厂模式里集中在工厂方法上的压力可以由工厂方法模式里不同的工厂子类来分担。
package com.atguigu.pattern.factory.method;
interface Car{
void run();
}
//两个实现类
class Audi implements Car{//实现接口方法
public void run() {
System.out.println("奥迪在跑");
}
}
class BYD implements Car{//实现接口方法
public void run() {
System.out.println("比亚迪在跑");
}
}
//工厂接口
interface Factory{
Car getCar();
}
//两个工厂类
class AudiFactory implements Factory{//实现接口方法的类
public Audi getCar(){
return new Audi();
}
}
class BydFactory implements Factory{//实现接口方法的类
public BYD getCar(){
return new BYD();
}
}
public class Client {
public static void main(String[] args) {
Car a = new AudiFactory().getCar();
Car b = new BydFactory().getCar();
a.run();
b.run();
}
}
总结:
简单工厂模式与工厂方法模式真正的避免了代码的改动了?
没有。在简单工厂模式中,新产品的加入要修改工厂角色中的判断语句;
而在工厂方法模式中,要么将判断逻辑留在抽象工厂角色中,要么在客户程序中将具体工厂角色写死(就像上面的例子一样)。而且产品对象创建条件的改变必然会引起工厂角色的修改。面对这种情况,Java 的反射机制与配置文件的巧妙结合突破了限制——这在Spring 中完美的体现了出来。
4、抽象工厂模式
抽象工厂模式和工厂方法模式的区别就在于需要创建对象的复杂程度上。而且抽象工厂模式是三个里面最为抽象、最具一般性的。
抽象工厂模式的用意为:给客户端提供一个接口,可以创建多个产品族中的产品对象。
而且使用抽象工厂模式还要满足一下条件:
- 系统中有多个产品族,而系统一次只可能消费其中一族产品。
- 同属于同一个产品族的产品以其使用。
工厂方法模式引入工厂等级结构,解决了简单工厂模式中工厂类职责过重的问题,但由于工厂方法模式中每个工厂只创建一类具体类的对象,这将会导致系统当中的工厂类过多,这势必会增加系统的开销。此时,我们可以考虑将一些相关的具体类组成一个“具体类族”,由同一个工厂来统一生产,这就是我们本文要说的“抽象工厂模式”的基本思想。
接着来看我们抽象方法模式一文中我们说的例子,我们现在有这么一个场景;现在用户的需求变多了,除了要求不同的形状之外,我们还要求创建不同颜色的形状,比如圆形,我们要求红色的圆形、赤橙黄绿青蓝紫等各种各样颜色的圆形。按照我们工厂方法模式的设计方案,那么我们就需要实现各种颜色的形状并实现这些颜色的形状对应的工厂类,这样一来我们系统中会增多非常多的工厂类。看起来如下:
//具体类的抽象接口
public interface Shape {
//接口方法
}
//具体类Circle
public class Circle implements Shape {
//接口方法实现
}
//具体类红色圆
public class RedCircle implements Shape {
//接口方法实现
}
//...各种颜色的圆...
//具体类Triangle
public class Triangle implements Shape {
//接口方法实现
}
//具体类红色Triangle
public class RedTriangle implements Shape {
//接口方法实现
}
//...各种颜色的Triangle...
//工厂接口
public interface ShapeFactory {
Shape getShape();
}
//CircleFactory
public class CircleFactory implements ShapeFactory {
//返回实例
}
//RedCircleFactory
public class RedCircleFactory implements ShapeFactory {
//返回实例
}
//...各种颜色的Circle的工厂...
//TriangleFactory
public class TriangleFactory implements ShapeFactory {
//返回实例
}
//RedTriangleFactory
public class RedTriangleFactory implements ShapeFactory {
//返回实例
}
//...各种颜色的Triangle的工厂...
那么我们有没有办法对当前的设计方案进行优化,减少一些工厂类的产生呢?我们下面对当前的系统做进一步抽象。以形状为类的级别、颜色为族来对当前系统作进一步改善。
首先我们要以形状为等级、颜色为族来对系统进行进一步抽象。
等级抽象
进行等级抽象我们需要将不同的形状声明为抽象类(等级划分)并实现公共的抽象接口(Shape),然后具体的实现类继承自对应的抽象类;
//形状公共接口
public interface Shape {
void draw();
}
//圆形抽象类Circle
public abstract class Circle implements Shape {
public abstract void draw();
}
//长方形抽象类Rectange
public abstract class Rectange implements Shape {
public abstract void draw();
}
//其他图形抽象类... ...
具体的实现类继承自对应的抽象类,继承自不同的抽象类就相当于将类划分为不同的等级,如:
//具体颜色的Circle实现
public class BlueCircle extends Circle {
@Override
public void draw() {
System.out.println("绘制蓝色的圆");
}
}
public class RedCircle extends Circle {
@Override
public void draw() {
System.out.println("绘制红色的圆");
}
}
//具体颜色的Rectange实现
public class RedRectange extends Rectange{
@Override
public void draw() {
System.out.println("绘制红色长方形");
}
}
public class BlueRectange extends Rectange {
@Override
public void draw() {
System.out.println("绘制蓝色长方形");
}
}
具体类族抽象
具体类族的划分我们以颜色为基础,不同类族的对象我们通过对应的具体工厂来创建。所以首先我们需要定义一个抽象工厂,具体工厂(族)实现抽象工厂的方法来生成一组具体对象。
//抽象工厂ShapeFactory
public interface ShapeFactory {
Shape getCircle();
Shape getRectange();
}
//RedShapeFactory(他所代表的是红色形状这一族)
public class RedShapeFactory implements ShapeFactory {
@Override
public Shape getCircle() {
return new RedCircle();
}
@Override
public Shape getRectange() {
return new RedRectange();
}
}
//BlueShapeFactory(他所代表的是兰色形状这一族)
public class BlueShapeFactory implements ShapeFactory {
@Override
public Shape getCircle() {
return new BlueCircle();
}
@Override
public Shape getRectange() {
return new BlueRectange();
}
}
//...其他族...
等级以及族类划分完成之后,我么的客户端在使用时只需要知道抽象工厂(ShapeFactory )以及具体工厂(如BlueShapeFactory )就可以获得指定族的所有形状。
public class TestDemo {
public static void main(String[] args) {
ShapeFactory redShapeFactory = new RedShapeFactory();
Shape circle = redShapeFactory.getCircle();
circle.draw();
Shape rectangle = redShapeFactory.getRectange();
rectangle.draw();
ShapeFactory greenShapeFactory = new GreenShapeFactory();
Shape greenCircle = greenShapeFactory.getCircle();
greenCircle.draw();
}
}
从上面的例子我们就可以知道,抽象工厂模式为创建一组对象提供了一种解决方案。 与工厂方法模式相比, 抽象工厂模式中的具体工厂不只是创建一种具体对象, 它负责创建一组(族)具体对象。
抽象工厂模式定义(Abstract Factory Pattern)
提供一个创建一系列相关或者相互依赖对象的接口,而无需知道他们的具体类,抽象工厂模式也称Kit模式,它属于类创建型模式。
在抽象工厂模式中,每个具体工厂都提供了多个工厂方法用于创建多种不同类型的具体对象,这些被创建的对象就构成一个族。
抽象工厂模式包含的角色:
-
抽象工厂:声明一组用于创建一族产品的方法,每个方法对应一种对象;在抽象工厂中声明了多个工厂方法, 用于创建不同类型的对象, 抽象工厂可以是接口, 也可以是抽象类或者具体类,具体实现可参考上例中的ShapeFactory;
-
具体工厂:具体工厂实现了抽象工厂,每个工厂方法返回一个具体对象,一个具体工厂所创建的具体对象构成一个族。具体实现可参考上例中的RedShapeFactory;
-
抽象类接口:提供一组所有类都具有的业务方法。
-
抽象类:用于实现抽象接口所定义的业务方法,但是该角色对于抽象接口定义的方法只做抽象实现,即所有实现都被定义为抽象方法,最终的具体实现全部交给具体类实现。引入该角色主要是为了根据声明不同的抽象类,将类区分为不同的等级结构。
-
具体类:该角色继承抽象类,主要用于实现抽象类中声明的抽象方法,完成不同等级结构,不同族的业务方法的具体实现。
根据我们上面对抽象工厂模式的描述,我们大致能够明白,如果我们在上例的基础上想要增加绿色或者其他颜色的图形时,我们只需要增加一个绿色产品工厂(GreenShapeFactory),同时继承不同的图形抽象类实现不同颜色的图形具体类即可,看起来如下:
//GreenShapeFactory
public class GreenShapeFactory implements ShapeFactory {
@Override
public Shape getCircle() {
return new GreenCircle();
}
@Override
public Shape getRectange() {
return new GreenRectange();
}
}
//GreenRectange
public class GreenRectange extends Rectange {
@Override
public void draw() {
System.out.println("绘制绿色长方形");
}
}
//GreenCircle
public class GreenCircle extends Circle{
@Override
public void draw() {
System.out.println("绘制绿色圆");
}
}
我们增加这些拓展,并不会影响到原来的代码,这看起来是符合“开闭原则”的;但是我们再深入思考一下,如果我们现在想增加一个新的图形,比如说三角形(Triangle)。
而对于我们目前所做的设计来讲,我们将三角形(Triangle)放到任何一个等级机构(即继承自任何一个图形抽象类,如Circle)都显得有些格格不入。但是我们如果在系统之中增加一个三角形(Triangle)的抽象类并实现不同颜色的三角形(Triangle)类之后我们会发现,我们需要修改所有的抽象工厂,这并不符合“开闭原则”。
即在抽象工厂模式中,我们只能够对族进行拓展,在对族进行拓展的情况下,时符合“开闭原则”的,如上例中增加不同颜色的指定图形;但是对类的等级结构做拓展,是不符合“开闭原则”的;
因此使用抽象工厂模式要求设计人员在设计之初就能够全面考虑, 不会在设计完成之后向系统中增加新的等级结构, 也不会删除已有的等级结构, 否则将会导致系统出现较大的修改, 为后续维护工作带来诸多麻烦。
小结
抽象工厂模式是工厂方法模式的进一步延伸, 由于它提供了功能更为强大的工厂类并且具备较好的可扩展性, 在软件开发中得以广泛应用, 尤其是在一些框架和API类库的设计中, 例如在Java语言的AWT( 抽象窗口工具包) 中就使用了抽象工厂模式, 它使用抽象工厂模式来实现在不同的操作系统中应用程序呈现与所在操作系统一致的外观界面。 抽象工厂模式也是在软件开发中最常用的设计模式之一。
优点
-
抽象工厂模式隔离了具体类的生成, 使得客户并不需要知道什么被创建。 由于这种隔离,更换一个具体工厂就变得相对容易, 所有的具体工厂都实现了抽象工厂中定义的那些公共接口, 因此只需改变具体工厂的实例, 就可以在某种程度上改变整个软件系统的行为。
-
当一个族中的多个对象被设计成一起工作时, 它能够保证客户端始终只使用同一个族中的对象。
-
增加新的族很方便, 无须修改已有系统, 符合“开闭原则”。
缺点
- 增加新的等级结构麻烦, 需要对原有系统进行较大的修改, 甚至需要修改抽象层代码,这显然会带来较大的不便, 违背了“开闭原则”。
使用场景
-
一个系统不应当依赖于具体类实例如何被创建、 组合和表达的细节, 这对于所有类型的工厂模式都是很重要的, 用户无须关心对象的创建过程, 将对象的创建和使用解耦;
-
系统中有多于一个的族, 而每次只使用其中某一族。 可以通过配置文件等方式来使得用户可以动态改变族, 也可以很方便地增加新的族。
-
属于同一个族的对象将在一起使用, 这一约束必须在系统的设计中体现出来。 同一个族中的对象可以是没有任何关系的对象, 但是它们都具有一些共同的约束, 如同一操作系统下的按钮和文本框, 按钮与文本框之间没有直接关系, 但它们都是属于某一操作系统的, 此时具有一个共同的约束条件: 操作系统的类型。
等级结构稳定, 设计完成之后, 不会向系统中增加新的等级结构或者删除已有的等级结构。
适配器设计模式(Adapter Pattern)
什么是适配器设计模式
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
适配器模式通过定义一个新的接口(对要实现的功能加以抽象),和一个实现该接口的Adapter(适配器)类来透明地调用外部组件。这样替换外部组件时,最多只要修改几个Adapter类就可以了,其他源代码都不会受到影响。
以手机为例子,每一种机型都自带有充电器,有一天自带充电器坏了,而且市场没有这类型充电器可买了,怎么办?万能充电器就可以解决,这个万能充电器就是适配器。
适配器模式有两种形式,一种是类的适配,另一种自然就是对象的适配。
为什么使用适配器设计模式
-
意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
-
主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
何时使用适配器设计模式
-
系统需要使用现有的类,而此类的接口不符合系统的需要。
-
想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
-
通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)
怎么使用适配器设计模式
如何解决:继承或依赖(推荐)。
关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。
类的适配
这种适配器由三种角色组成:
1、目标(Target)角色:这就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因此目标不可以是类。
2、源(Adapee)角色:现在需要适配的接口。
3、适配器(Adaper)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。
示例:
类适配器
故事情景:手机充电用手机充电器充电,耳机用耳机充电器充电,现在要实现给手机充电的同时也能给耳机充电。
首先来创建一个手机充电器接口:
/**
* 充电器接口
*/
public interface ICharge {
void charge();
}
再创建一个接口实现类,IPhone的充电器类(Adaptee角色):
/**
* (Adaptee角色) 现在需要适配的接口
* 苹果手机充电器(需要适配的角色)
*/
public class AppleCharger implements ICharge {
@Override
public void charge(){
System.out.println("苹果手机正在充电中 ...");
}
}
现在,要对这个特殊的充电器进行适配,上个适配的接口(Target目标角色):
package com.jt.adapter.class_adapter;
/**
* 要对这个特殊的充电器进行适配,适配器接口
* (Target角色)
* 这就是所期待得到的接口。
* 注意:由于这里讨论的是类适配器模式,因此目标不可以是类。
*/
public interface IChargeAdapter {
void charge();
}
创建类的适配器:
/**
* 类的适配器
* 多接头充电器,支持一边充手机,一边充耳机
* (Adaper)
* 适配器类是本模式的核心。适配器把源接口转换成目标接口。
* 显然,这一角色不可以是接口,而必须是具体类。
*/
public class MultipleJointsCharger extends AppleCharger implements IChargeAdapter {
@Override
public void charge() {
super.charge();
System.out.println("耳机正在充电 ...");
}
}
启动类:
@SpringBootApplication
public class StartClass {
public static void main(String[] args) {
new MultipleJointsCharger().charge();
}
}
控制台:
对象的适配
对象的适配依赖于对象的组合,而不是类适配中的继承。
示例:
故事情景:苹果手机用苹果充电器充电,安卓手机用安卓手机充电,现在需要一个万能充电器能给这两种手机充电。
/**
* 充电器接口
*/
public interface ICharge {
void charge();
}
创建两个充电器类(Adaptee源角色):
/**
* 安卓手机充电器需要适配的角色(Adaptee)
*/
public class AndroidCharger implements ICharge {
@Override
public void charge() {
System.out.println("The AndroidPhone is charging ...");
}
}
/**
* 苹果手机充电器需要适配的角色(Adaptee)
*/
public class AppleCharger implements ICharge {
public void charge() {
System.out.println("The ApplePhone is charging ...");
}
}
现在,要对这个特殊的充电器进行适配,上个适配的接口(Target目标角色):
/**
* 要对这个特殊的充电器进行适配,适配器接口(Target角色)
*/
public interface IChargeAdapter{
void charge();
}
创建类的适配器:
/**
* 万能充电器,类的适配器(Adaper)
*/
public class UniversalCharger implements IChargeAdapter {
private ICharge iCharge;
public UniversalCharger(ICharge iCharge) {
this.iCharge = iCharge;
}
@Override
public void charge() {
iCharge.charge();//传入ICharge下的什么对象,就调用什么对象的charge()方法
}
}
启动类:
@SpringBootApplication
public class StartClass {
public static void main(String[] args) {
new UniversalCharger(new AppleCharger()).charge();
new UniversalCharger(new AndroidCharger()).charge();
}
}
控制台:
总结:
1、类的适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。
2、对象的适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个包装类,持有原类的一个实例,在包装类的方法中,调用实例的方法就行。
适配器设计模式的优缺点
优点
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
- 灵活性好。
缺点:
-
过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
-
由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。