GOF23--23种设计模式(二)
一.建造者模式
建造者模式也是属于建造型模式,它提供了一种创建对象的最佳方式
定义:将一个复杂的对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示
主要作用:在用户不知道对象的构建细节的情况下,就可以创建复杂的对象
这里需要注意一下,建造者模式都都是用来创建复杂对象的,如果对象很简单,直接自己使用new关键字实例化就行了,建造者模式是为了构造复杂的对象的
建造者模式的特点:用户只需要指定复杂对象的类型和内容,建造者模式负责按顺序创建复杂对象(把建造的细节都隐藏起来),建造者模式中有个很关键的类就是建造者或者叫指挥者,它是创建对象的关键,对于相同的对象类型和内容,经过建造者会变成不同的对象,相同的资源,怎么建造就是建造者的事情,用户是不用感知的
角色分析:
如上:
在建造模式中,建造者本身不具有任何创建所需的任何资源,创建对象的资源都是实施者拥有,指挥者负责的是怎么创建对象,可以有多个实施者(可以想象为由多个类)
构造方案就是建造者拥有的,它包括创建对象需要那些步骤,怎么完成这些步骤,最后的对象源自于实施者,指挥者只负责加工步骤,并且构造方案都是抽象的,抽象方案只记录做什么,实施者才知道怎么做
区分工厂模式和建造者模式
工厂模式:会将通过不同的工厂加工出不同的零件,但是每个工厂都只加工一种零件(专一接口)
建造者模式:通过提供的不同的零件,建造负责安装自己的方式,组装出不同品牌,样式的车
建造模式模拟
构造方案:
//构建方案 public abstract class Builder { //查询用户是否已经存在 abstract void checkUser(); //不存在就创建 abstract void createUser(); //新用户信息持久化到数据库 abstract void registerToDB(); //上述步骤完成后一定有一个用户返回,新用户或已注册的用户对象 abstract User getUser(); }
实际实施者:
//实际的实施者,创建对象的内容 //自己只知道自己能干什么,并不知道怎么去操作 public class Worker extends Builder{ private User user; public Worker(User user) { this.user = user; } @Override void checkUser() { //注入信息给对象,或者理解为对象需要的参数 user.setAnswer("用户已存在"); System.out.println("需要检测数据库,查看是否已有当前用户的信息"); System.out.println("如果有,则是已注册用户返回登录,没有则进行下一步"); } @Override void createUser() { System.out.println("根据提供的信息,开始校对信息是否合法"); System.out.println("合法则创建此用户,并返回登录"); } @Override void registerToDB() { //注入信息给对象,或者理解为对象需要的参数 user.setAnswer("新用户创建成功"); System.out.println("数据持久化收到的信息,保证此用户为注册成功"); } @Override User getUser() { return user; } }
对象类:
//最总的返回对象 //根据指挥者的各种检测和操作,最后将所需的参数或结果注入此对象中 public class User { private String answer; public String getAnswer() { return answer; } public void setAnswer(String answer) { this.answer = answer; } @Override public String toString() { return "User{" + "answer='" + answer + '\'' + '}'; } }
指挥者:
//指挥者 //负责调用建造,并且告诉它们怎么做,什么时候做什么,完成后对象创建成功 public class director { private Builder builder; public User creat(Builder builder) { builder.checkUser(); builder.registerToDB(); builder.createUser(); return builder.getUser(); } }
上面的有很大部分都是开发者做的,对于真正的调度者,其实做的就是实例化指挥者所在的类,然后调用它的方法,从而拿到对象
接下来,我们就来看看调度者的视角:
public static void main(String[] args) { //指挥者 director director = new director(); //指挥方案 User user = director.creat(new Worker(new User())); System.out.println(user.toString()); }
如上:
我们要的得到一个user对象所经历的操作对于调度者来说都是不可见的,它根本不知道做了什么就拿到了一个对象,
而调度者需要知道的是,指挥者是谁,然后实例化类,调用它的方法,需要什么参数丢进去,其它创建的过程,调度者都是不用知道的
理解了上面的代码就会知道,指挥者在建造者模式的地位是很重要的,它把实现细节抹除,使得调度者可以着手业务开发,复杂对象它不必关注如何创建的
指挥者的作用也显而易见,它控制创建对象的方法顺序,指导实施者具体的操作过程,最后返回建造好的对象
总结
优点:
- 产品的建造和表示分离,实现了解耦,使用者不需要知道对象构建的细节
- 将复杂产品的创建步骤分解在不同方法中,使创建过程更加清晰
- 具体的建造者类之间都是相互独立的,有利于系统的拓展,符合“开闭原则”
缺点:
- 建造者模式所创建的产品一般有较多的共同点,其组成部分相似
- 如果产品内部发生变化,可能会需要很多创建者来实现这种变化,导致系统很庞大
二.原型模式
原型模式是创建型设计模式,允许通过复制原型实例来创建新的对象,而无需知道创建细节。
原型实例是已经创建好的实例,通过克隆方法创建原型实例的新对象
工作原理:通过将一个原型对象传给那个要发动创建对象,这个要发动创建的对象通过请求原型对象拷贝他们自己来实施创建
在Java中,克隆对象有很多中方式,其中包括IO可以做,但是不高效,最高效的方式应该是在内存中就完成克隆,但是Java本身不提供操作内存的操作
所以需要用到Java的本地方法栈,调用C++的方法来操作内存
对于我们操作者直接调用clone()方法就可以了
如上图:
克隆方法是native修饰的,说明他是本地方法栈的方法,可能是C/C++写的
克隆clone()方法出来的方法可以分为深克隆和浅克隆
浅克隆
浅克隆出来的对象和原型对象还具有藕断丝连的效果,原型对象的属性指向的空间,克隆对象也会指向这片空间,不会重新指向一片新的空间
//对象需要克隆需要先实现Cloneable接口 //然后重写clone方法 public class Instance implements Cloneable{ private String name; private Date createTime; public Instance(String name, Date createTime) { this.name = name; this.createTime = createTime; } //重写的clone方法不需要做什么,直接调用父类的方法就行了,因为克隆方法要做的事情是C++写的 //需要C++操作内存,在内存中操作是最高效的 @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } }
主方法:它的作用需要构建一个原型对象,然后通过这个原型对象调用克隆方法,从而获得原型对象
public static void main(String[] args) throws CloneNotSupportedException { Date date = new Date(); //原型实例 in1 Instance in1 = new Instance("new实例",date); System.out.println("in1=>hashcode:"+in1.hashCode()); System.out.println("in1=>"+in1); //克隆 in1 Instance in2 = (Instance) in1.clone(); System.out.println("in2=>"+in2); System.out.println("in2=>hashcode:"+in2.hashCode()); System.out.println("========================================================================"); date.setTime(20211104); System.out.println("in1=>"+in1); System.out.println("in2=>"+in2); }
测试输出结果:
如上图:
我们先看两个对象的hashcode,是不同的,所以肯定是两个对象,
然后在观察对象的属性,也是完全一样的,这就是原型模式的浅克隆,克隆的时候属性指向的空间是一样的,这一个空间的内容改变时,两个对象都会改变
有些情况下,要求指向的空间见应该是不一样的,就是深克隆
浅克隆略图:
深克隆
深克隆也是通过原型对象克隆出来一个新的对象,但是克隆出来的对象是指向的一片新的内存空间,就不会出现它们一模一样的情况了
深克隆实现只需要更改克隆方法,让克隆时候连同属性也克隆了
@Override protected Object clone() throws CloneNotSupportedException { Object clone = super.clone(); //实现深度克隆 Instance in =(Instance) clone; //将这个属性也进行克隆~ in.createTime=(Date) this.createTime.clone(); return clone; }
主方法:它的作用需要构建一个原型对象,然后通过这个原型对象调用克隆方法,从而获得原型对象
public static void main(String[] args) throws CloneNotSupportedException { Date date = new Date(); //原型实例 in1 Instance in1 = new Instance("new实例",date); System.out.println("in1=>"+in1);//克隆 in1 Instance in2 = (Instance) in1.clone(); System.out.println("in2=>"+in2); System.out.println("========================================================================"); date.setTime(20211104); System.out.println("in1=>"+in1); System.out.println("in2=>"+in2); }
测试输出:
如上
深克隆连对象指向的空间也会克隆一份,所以指向的空间内容都是不一样的,它们指向的空间,任何一个改变都不会影响另外一个
深克隆略图:
三.适配器模式
适配器模式:只能说是源自于生活,它就真的和现实种的适配器很像,就好比网络适配器,现代的笔记本电脑大多都是不能插网线的,如果想插网线就需要适配器,网络适配器一段连接网线,一端连接笔记本,就能使用网线上网了。
而适配器模式中的适配器也是一样的,它用于解决两个类或接口有一个类需要用到另外一个类的方法时,但是他们接口是不兼容的(主要是指的代码不兼容),
这时候就需要一个适配器类,先写好一个接口的参数,然后再把需要的参数调给另外一个接口,实现中间类让两个接口兼容然后协同工作
适配器模式是结构型模式,它和创建型模式是不同的,创造型模式是用来做对象创建的,而适配器模式是做类之间的结构适配的
适配器模式有3种形式:类适配器、对象适配器和函数适配器。类适配器是通过继承来实现适配器的功能,对象适配器是通过组合来实现适配器的功能,而函数适配器则允许你使用一个函数来适配接口。
在Java中,适配器模式常用于处理不同接口之间的兼容问题。
例如,你可能需要将一个类的接口适配成另一个类的接口,以便它们可以一起工作。在这种情况下,你可以创建一个适配器类,该类实现目标接口,并包含一个源类的实例。
适配器类将源类实例的调用转发给目标接口,从而实现接口适配。
类适配器(继承)
类适配是基于继承来的,通过继承的方式拿到父类的方法,从做适配
类适配器有一个缺点,我们知道在Java中只有单继承,所以当一个类需要两个接口做适配时,使用继承的方式就不行了,如果只需要对一个类做适配是没问题的
我们就使用上网适配的方式来模拟适配器模式
网线接口类:
//网线类 public class Reticle { //双绞线 public void TwistedPairCable(){ System.out.println("这是双绞线接口"); } //同轴电缆 public void coaxialCable(){ System.out.println("这是同轴电缆接口"); } //光纤 public void opticalFiber(){ System.out.println("这是光纤接口"); } }
PC类:
//PC只支持USB进行上网 public class Computer { public void net(){ //当输入源为USB接口时,可以上网 System.out.println("computer:我需要USB接口的网线上网"); } }
适配器接口,定义了需要那些方法才能完成适配:
//适配器的抽象接口 //标识了能实现那些转换 public interface NatAdapter { //输入源 //处理输入适配器的接口类型 public void InSource(); //输出源 //处理输出适配器的接口类型 public void OutSource(); }
适配器实现类,实际去适配这些接口:
//适配器处理类 //首先需要实现适配器的通用接口,实现适配器接口方法,看看自己需要适配什么 //类适配器,通过继承的方式,拿到需要适配的方法 public class AdapterImp extends Reticle implements NatAdapter{ //输入源的接口类型 private String InName; //输出源的接口类型 private String OutName; public AdapterImp(String inName, String outName) { InName = inName; OutName = outName; InSource(); OutSource(); } //处理输入源的方法,自己支持那些接口,通过接口类型进行适配 @Override public void InSource() { if(InName.equals("双绞线")){ TwistedPairCable(); }else if (InName.equals("同轴电缆")){ coaxialCable(); }else if (InName.equals("光纤")){ opticalFiber(); }else { System.out.println("适配器不支持此类接口"); } } //处理输出源的方法,pc支持那些接口,自己接使用那些接口输出 @Override public void OutSource() { Computer com = new Computer(); if (OutName.equals("USB")){ System.out.println("输出接口为USB"); com.net(); }else if (OutName.equals("Type-c")){ System.out.println("PC不支持"); }else { System.out.println("适配器不支持此类接口"); } } //类适配器,重写父类的方法,对父类的方法进行适配 @Override public void TwistedPairCable() { super.TwistedPairCable(); } @Override public void coaxialCable() { super.coaxialCable(); } @Override public void opticalFiber() { super.opticalFiber(); } }
代码分析:
网线类说明当前有那些网线类型,自己可以去选择用那种,但有一点PC是不直接支持这些网线接口的
PC类说明自己需要那种类型接口
适配器接口类,说明适配器的实现类需要实现那些方法才能做到适配两种接口
适配器实现类,首先实现适配器接口,实现其中的方法,然后通过继承的方式,重写父类的方法或调用父类的方法
对于适配器实现类而言,只需要将输入源接口和输出源接口类型告诉它以后,他就可以完成适配
主方法:
public static void main(String[] args) { //PC需要USB接口 String pc="USB"; //网线当前是双绞线 String reticle="双绞线"; //将两种接口传入适配中,让适配器适配 AdapterImp imp = new AdapterImp(reticle, pc); }
如上:
当我们传入USB接口的时候,适配器就去调用输出接口为USB的方法
当我们传入双绞线的时候,适配器就去调用输入接口为双绞线的方法
它的思想用一个中间类将两个接口不匹配的类适配后协同工作
对象适配器(组合常用)
上面的类适配器,我们说了它的缺点,就是无法做到一个适配器同时适配多个接口
对象适配器使用的是组合的方式,将需要适配的接口类给组合进来,使其成为适配器类的属性,好处就是可以同时适配多个接口类
更改适配器类为组合方式:
//适配器处理类 //首先需要实现适配器的通用接口,实现适配器接口方法,看看自己需要适配什么 //对象适配器,通过组合的方式,拿到需要适配的方法 public class AdapterImp implements NatAdapter{ //输入源的接口类型 private String InName; //输出源的接口类型 private String OutName; //需要适配接口类 private Reticle reticle; public AdapterImp(String inName, String outName,Reticle reticle) { this.InName = inName; this.OutName = outName; this.reticle=reticle; InSource(); OutSource(); } //处理输入源的方法,自己支持那些接口,通过接口类型进行适配 @Override public void InSource() { if(InName.equals("双绞线")){ //组合进来的属性对象,直接通过.拿取方法 reticle.TwistedPairCable(); }else if (InName.equals("同轴电缆")){ reticle.coaxialCable(); }else if (InName.equals("光纤")){ reticle.opticalFiber(); }else { System.out.println("适配器不支持此类接口"); } } //处理输出源的方法,pc支持那些接口,自己接使用那些接口输出 @Override public void OutSource() { Computer com = new Computer(); if (OutName.equals("USB")){ System.out.println("输出接口为USB"); com.net(); }else if (OutName.equals("Type-c")){ System.out.println("PC不支持"); }else { System.out.println("适配器不支持此类接口"); } } }
既然没了继承就不需要重写那几个父类的方法了,直接使用组合对象.方法就行了
总结
将一个类的接口转换为顾客需要的另一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作!
目标接口:客户所期待的接口,目标可以是具体的抽象类,也可以是接口
需要适配的类:需要适配的类或者适配类
适配器:通过包装一个需要适配的对象,把原接口转换为目标对象
- 对象适配的优点:一个适配器可以适配多个目标接口,由于适配器和适配者是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配
- 类适配器的缺点:Java和C#等语言,不支持多继承,所以无法实现一个适配器适配多个目标接口
网络适配器略图: