设计模式
概要
所有的设计模式和相关的原则都不是强制性的,在大多数情况下需要遵守,在有些特殊情况下可以灵活变通,千万不要故不自封,设计模式只是为了帮我们更好地编写和维护代码。
所有的法则都是作用于服务端的,客户端是业务逻辑的体现,业务逻辑发送变化时,客户端肯定要变化,所以不要钻牛角尖,尽量让客户端保持最少的代码完成更多的功能。
只要代码足够简单,以下原则都可以不遵守
设计模式的目的
- 代码重用性:相同功能的代码不用多次编写
- 可读性:编程规范性,便于其他程序员阅读和理解
- 可扩展性:当需要增加新的功能时,非常方便,便于维护
- 可靠性:当我们增加新的功能后,对原来的功能没有影响
- 使程序呈现高内聚,低耦合的特性
设计模式的七大原则
单一职责原则
一个类应当只负责一项职责
如果一个类有两个不同的职责则需要将A的粒度分解为A1,A2
- 降低类的复杂度,一个类只负责一个职责
- 提高类的可读性
- 降低变更带来的风险
- 通常情况下,我们应当遵守单一职责原则,只有在代码逻辑足够简单时,才可以在代码级违反单一职责原则。只有在方法足够少时,可以在方法级别遵守单一职责原则(但是如何划分方法,如何划分类,只能具体情况具体分析,既不能划分得过多,也不能太少)
一般情况下:一个实体划分为一个类,实体的行为被划分为方法,最基础的行为是单独的一个方法,复杂的行为由多个基础行为组成。同级别的不同实体应当写成多个类。将公共的特征抽取出来,作为所有实体的父类,实现父类的功能,然后将父类逐步进行划分,逐步实现子类的功能。
接口隔离原则
客户端不应当依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
(即一个类不需要实现它用不到的接口方法)
一个类可以实现多个接口,每个接口都可以代表这个类的一部分
一个类(A,C)在使用另一个类的对象时,则称这个类对所使用的类产生了依赖。
如果被依赖的类实现了多个接口,我们只需要在方法中传入最小的接口,根据我们想要使用的方法,来选择使用哪个接口。
并且这样拆解后,客户端的代码不用变化,而降低了接口的耦合度。
例如在depend1中只会使用到interface1中的方法,我们只需要关心interface1的逻辑即可,而不用关心传入对象是否需要实现interface2和interface3。这样做后,interface1的实现类都可以作为参数传进来,而不用被迫实现interface2和interface3中那些我们不需要的方法(扩大的传入的对象的类型,使方法的适用性更广,更改代码的风险降低)
什么是依赖:
a.depend1(new B()) 就说类A通过接口interface1依赖(使用)类B
依赖倒转原则
- 高层模块不应依赖底层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 中心思想:面向接口编程
- 原因:相对于细节的多变性,抽象的东西要稳定得多。因而以抽象为基础搭建的架构比细节为基础的架构要稳定得多。在java中,抽象就是接口或者抽象类,细节是具体的实现类
- 使用接口或者抽象类的目的是制定好规范,而不设计任何具体的操作,把展现细节的任何交给他们的实现类来完成
注意要点:
- 方法参数上不应当直接使用具体的实现类,而应该使用这个实现类的接口(这样设计可以让设计地架构更有弹性,在有新的实现类时,不用再写新的方法)
- 每一个类都尽量有一个接口或者抽象类,使用的使用它的接口而不是直接使用实现类,这样就存在一个缓冲层,稳定性更好
- 继承时需要遵循里氏替换原则
依赖的产生
1.作为方法参数
2.作为成员变量(构造器,set方法)
里氏替换原则
1.在使用继承时,子类可以重写父类的方法,重写之后可能会对整个继承体系(业务逻辑)造成破环
class Base {
void getprint2() {
System.out.println("fatherprint");
}
void print() {
getprint2();
System.out.println("base");
}
}
class Son extends Base {
@Override
void getprint2() {
System.out.println("sonprint");
}
}
public class Test {
public static void main(String[] args) {
new Son().print();
}
}
我们调用print方法时,由于子类对getprint2方法进行了重写,本来父类的print是想要调用父类的getprint2方法,结果缺调用了子类的getprint2,业务逻辑就发生了破坏(这种重写有时候是无意识的,出现错误也很难排查,这也是@Override)。
2.修改父类时,必须考虑到所有的子类,并且子类的功能可能发发生故障
3.解决方案:里氏替换原则
思想要点
假定A继承B
1.使用继承时,尽量不要重写父类的方法,否则不要使用继承(A继承B后,不要重写B的方法)(这个也不是严格的不能重写,可以在父类写一些托底的通用的逻辑,然后子类实现更加具体的功能,这样写也是符合里氏替换原则的,设计模式所有的思想都不是死的,一定要灵活运用)
2.如果需要修改方法,可以让两个类共同继承一个更加基础的父类,或者实现一个公共的接口(让A,B实现一个接口或者父类base)
3.如果A还要使用B的方法,将B作为A成员变量来调用用
开闭原则
开:提供功能的代码对扩展开放
闭:使用功能的代码对修改关闭
举个例子:A使用B的功能,B增加新的功能时,或者原有的行为发生变化时,A可以在不发生变化的前提下适应新的变化
整个调用过程可以形成一个树形结构,在需要修改功能时,尽量去修改叶子结点(或靠近叶子结点)的部分
interface Shape{
void draw();
}
class SquareShape implements Shape{
@Override
public void draw() {
System.out.println("正方形");
}
}
class Triangle implements Shape{
@Override
public void draw() {
System.out.println("三角形");
}
}
class Circle implements Shape{
@Override
public void draw() {
System.out.println("圆");
}
}
interface Drawer{
void toDraw(Shape shape);
}
class RawDrawer implements Drawer{
@Override
public void toDraw(Shape shape) {
System.out.println("Quick");
shape.draw();
}
}
/**
* @author 李天航
*/
public class OpenClosePrincipleDemo {
public static void main(String[] args) {
Drawer drawer=new RawDrawer();
drawer.toDraw(new SquareShape());
drawer.toDraw(new Triangle());
drawer.toDraw(new Circle());
}
}
假如我们要使用绘画工厂来画图,创建会话工厂后,如果我们想要添加新的图形,只需要实现Shape接口即可,如果想使用这个新的功能,只需要传入对应的实现类即可。
这里也能体现出开闭原则:
开:提供功能的一方可以提供更多的功能,我们可以增加新的图形
闭:使用功能的一方原有代码保持不变
这是也体现出面向接口编程的好处
迪米特法则
- 一个对象应该对其他对象保持最少的了解
- 类与类的关系越密切,耦合度越大
- 迪米特法则又叫最小知道原则,即一个类对自己依赖的类知道的最少越好。也就是说,除了提供的public方法,不对外泄露任何的信息,都尽量将逻辑封装在类的内部。对除了提供了public方法,不对外泄露任何信息
- 更简单的定义:只和直接的朋友通信
需要使用的对象应当只出现在方法参数,成员变量,返回值中,而不要出现在局部变量中去完成这个类的功能。这个类的功能应当在它的内部来完成。使用一个类的功能时,不要把这个类的功能和逻辑出现在其他类中,其他类只需要知道它要使用这个类的功能。
我们追求的只是降低不必要的耦合,而不是让耦合完全消失,模块完全没有耦合,无法实现,代码也就失去了意义
合成复用原则
如何只是为了使用一个类的方法则不要使用继承,当然,如何两个类有亲缘关系,比如一个类是另一个类的具体化,这时推荐使用继承,而如果是两个不怎么相干的类则不要使用继承。
用依赖,组合,聚合来代替
假如要替代B继承A
依赖:将A的对象作为参数传进来,来使用A的方法
聚合:将A作为B的成员变量,然后用set方法设置A的引用
组合:将A作为B的成员变量并在创建B的时候直接将A创建出来
设计模式
1.将变化的部分和不变的部分分离开
2.面向接口编程
3.追求松耦合
UML类图
UML是一种统一建模语言,可以帮我们进行建模
依赖关系:只要用到了另一个类就是依赖关系
泛化(继承),实现关系都是依赖关系的特例
关联关系是依赖关系的特例
聚合关系是关联关系的特例(整体和部分的关系)整体和部分可以分开
组合关系也是关联关系的特例(整体和部分的关系,但是整体和部分不能分开)一般创建时级联创建,删除时级联删除
单例模式(全局唯一)
懒加载
等到使用的时候采取加载这个对象
单例模式就是全局只有一个对象,对外提供一个访问接口,一般适用于例如线程池这样的重量级对象。
package 单例模式;
public class Single {
private static Single instance;
Single(){
}
public Single getSingle()
{
if(instance==null)
{
instance=new Single();
}
return instance;
}
}
如果只是这么写,在有多个线程访问时,instance对象会被创建多次,导致之前创建的对象丢失
想要避免重复创建可以加锁
package 单例模式;
public class Single {
private static Single instance;
Single(){
}
public Single getSingle()
{
synchronized(Single.class){
if(instance==null)
{
instance=new Single();
}
}
return instance;
}
}
但是这样每次获取对象都有获得锁,会消耗大量的性能,可以考虑在对象为空的时候采取获得锁
package 单例模式;
public class Single {
private static Single instance;
Single(){
}
public Single getSingle()
{
if(instance==null)
{
synchronized(Single.class){
instance=new Single();
}
}
return instance;
}
}
这样如果一次来了多个访问请求,没有拿到锁的线程会在处于等待中,等待结束后仍然会重复创建对象,因而还没有满足要求
package 单例模式;
public class Single {
private static Single instance;
Single(){
}
public Single getSingle()
{
//单例模式,所有视图获取这个对象的都必须等待
if(instance==null) {
synchronized (Single.class) {
if (instance == null) {
synchronized (Single.class) {
instance = new Single();
}
}
}
}
return instance;
}
}
使用上面这种模式可以解决之前的问题,因为会判断两次,加了两层锁,一旦有一个线程完成了锁的创建,其他线程(新来的和阻塞的)都会知道这个对象已经被创建出来因而不会重复创建。
Tip:
synchronized(this) :监视这一类的一个对象实例,每个对象都有一把锁
synchronized(Single.class):这个类的所有对象公用一把锁
但是还没有完全解决这个问题,java创建对象时,会经历三个步骤:
1.分配内存
2.初始化
3.引用赋值
但是在cpu和jvm执行的过程中,由于第2步和第3步的顺序可以交换,所以可能会进行一些指令重排,这在单线程中是没有问题的,但是在多线程中,如果先执行了第3步,此时新来的线程看到instance!=null,则会直接返回,但此时分配的内存中的值并不是正确的默认值,因而可能会出现一些异常,因而可以使用volatile关键字来阻止对这个对象的指令重排,确保是先初始化在进行引用赋值。
private static volatile Single instance;
饿汉模式
class HungrySingleton{
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return instance;
}
}
class Main{
public static void main(String[] args)
{
new Thread(()->{
System.out.println(HungrySingleton.getInstance());
}).start();
new Thread(()->{
System.out.println(HungrySingleton.getInstance());
}).start();
new Thread(()->{
System.out.println(HungrySingleton.getInstance());
}).start();
}
}
饿汉模式简洁高效,避免了同步代码块的开销,缺点是类加载的时机不确定,可能会浪费一点内存,如果能保证对象一定会被使用,则没有任何问题(但其实问题不大,也不用纠结这些)
如果我们需要创建的单例对象很大,需要设置属性,用懒加载的方式比较好,如果对象很简单,用饿汉模式即可。
调用类方法时,会检测内存中是否加载了这个类,如果没有加载则触发类加载机制:
加载
类加载过程的一个阶段,ClassLoader通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个class对象。
验证
目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
准备
为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,(如static int i = 5 这里只是将 i 赋值为0,在初始化的阶段再把 i 赋值为5),这里不包含final修饰的static ,因为final在编译的时候就已经分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中。
解析
这里主要的任务是把常量池中的符号引用替换成直接引用
初始化
这里是类记载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。(前面已经对static 初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化)
类记载器的任务是根据类的全限定名来读取此类的二进制字节流到 JVM 中,然后转换成一个与目标类对象的java.lang.Class 对象的实例,在java 虚拟机提供三种类加载器,引导类加载器,扩展类加载器,系统类加载器。
而new HungrySingleton();的过程发生在初始化阶段,后面再次使用的时候,instance就已经被加载完成了,所以不会重复地new 对象。
new 对象时,同样要经过:分配内存,调用构造方法初始化,引用赋值,这样instance就拥有了内存中的值,后面再调用这个类中的方法或者new 新的对象时,不会再去加载这个类。
静态内部类
class InnerStaticClass{
private static class holder{
protected static InnerStaticClass instance=new InnerStaticClass();
}
private InnerStaticClass(){
System.out.println(System.currentTimeMillis());
}
public static InnerStaticClass getInstance(){
return holder.instance;
}
public void test(){}
}
在外部调用InnerStaticClass的其他方法时(例如test),instance并不会被加载出来,只有在调用getinstance方法时,出发holder的类加载,jvm才会创建instacne的实例,所以这也是一种懒加载机制。
将构造函数设置为private可以防止外部new这个对象。
反射攻击
Constructor<InnerStaticClass> classConstructor= InnerStaticClass.class.getDeclaredConstructor();
classConstructor.setAccessible(true);
InnerStaticClass innerStaticClass=classConstructor.newInstance();
InnerStaticClass instance=InnerStaticClass.getInstance();
System.out.println(instance);
System.out.println(innerStaticClass);
强行将类的访问劝设置为true,然后调用构造方法拿到对象,这样得到的对象和一开jvm得到的对象都会说两种不同的对象。
反射攻击拦截:静态内部类
class InnerStaticClass{
private static class holder{
protected static InnerStaticClass instance=new InnerStaticClass();
}
private InnerStaticClass(){
if(holder.instance!=null)
{
throw new RuntimeException("单例不允许多例");
}
}
public static InnerStaticClass getInstance(){
return holder.instance;
}
}
调用构造函数的时候,判断instance是否已经被创建,被创建则抛出异常
枚举类型
enum EnumInstance{
INSTANCE;
void print()
{
System.out.println(this.hashCode());
}
}
枚举类型的初始化是在内部的静态方法块中,所以也是单例对象,并且是不能被new的,也不能被反射拿出来。
构造函数有两个:string和idx,代表名称和下标
序列化
让类继承Serializable
序列化:
class Main{
public static void main(String[] args) throws IOException {
InnerStaticClass instance=InnerStaticClass.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("outputFile"));
oos.writeObject(instance);
oos.close();
}
}
反序列化:
ObjectInputStream inputStream=new ObjectInputStream(new BufferedInputStream(new FileInputStream("outputFile")));
InnerStaticClass obj= (InnerStaticClass) inputStream.readObject();
System.out.println(obj.getdata());
inputStream.close();
完整:
class InnerStaticClass implements Serializable {
private int data;
private static class holder{
protected static InnerStaticClass instance=new InnerStaticClass();
}
private InnerStaticClass(){
data=12;
if(holder.instance!=null)
{
throw new RuntimeException("单例不允许多例");
}
}
public static InnerStaticClass getInstance(){
return holder.instance;
}
public int getdata(){
return data;
}
}
class Main{
public static void main(String[] args) throws IOException, ClassNotFoundException {
InnerStaticClass instance=InnerStaticClass.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("outputFile"));
oos.writeObject(instance);
oos.close();
ObjectInputStream inputStream=new ObjectInputStream(new BufferedInputStream(new FileInputStream("outputFile")));
InnerStaticClass obj= (InnerStaticClass) inputStream.readObject();
System.out.println(obj.getdata());
inputStream.close();
}
}
序列化允许我们将对象写入文件后再很方便地写出来
package 单例模式;
import javax.annotation.processing.SupportedSourceVersion;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Timer;
class InnerStaticClass implements Serializable {
/**
* 想要再序列化前后拿到的都是同一个对象,需要知道这个对象的版本号
*/
static final long serialVersionUID = 42L;
private int data;
private static class holder{
protected static InnerStaticClass instance=new InnerStaticClass();
}
private InnerStaticClass(){
data=12;
if(holder.instance!=null)
{
throw new RuntimeException("单例不允许多例");
}
}
public static InnerStaticClass getInstance(){
return holder.instance;
}
/**
* 序列化的留只能从这个名称的方法中拿到
* @return
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException
{
return holder.instance;
}
public int getdata(){
return data;
}
public void setData(int x){
this.data=x;
}
}
class Main{
public static void main(String[] args) throws IOException, ClassNotFoundException {
InnerStaticClass instance=InnerStaticClass.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("outputFile"));
oos.writeObject(instance);
oos.close();
instance.setData(13);
ObjectInputStream inputStream=new ObjectInputStream(new BufferedInputStream(new FileInputStream("outputFile")));
InnerStaticClass obj= (InnerStaticClass) inputStream.readObject();
System.out.println(instance.getdata());
System.out.println(obj.getdata());
inputStream.close();
}
}
为了解决序列化前后会生成不同的对象的问题,Serializiable中提供了一个解决方案:
private Object readResolve() throws ObjectStreamException
{
return holder.instance;
}
生成流的数据如果有这个方法则从这个方法拿到,如果没有这个方法就从流里面拿到
static final long serialVersionUID = 42L;
同时需要设置一个不能再被修改的版本号,序列化时会将版本号也写入文件,反序列化后会根据版本号拿到单例对象的引用,并且文件中的数据不会覆盖在内存中对象的数据。如果文件中的版本号和内存中的版本号不一致,则会抛出异常。
枚举类型
枚举类型天然支持序列化和反序列化,拿到的会是同一个对象。同时也是线程安全的
public enum Singleton {
/**
* 单例对象
*/
INSTANCE;
void sayOk(){
System.out.println("ok");
}
}
小结
推荐使用的单例模式有饿汉模式,双重判定,枚举类型,静态内部类。双重判断模式一般用于创建的单例对象依赖其他单例对象的适合使用(例如Spring)
工厂模式(将创建对象的逻辑封装进工厂)
场景:披萨订购
简单工厂模式
不要把大量的分支杂糅到业务代码中,把这种逻辑分离出来
把根据……创建……的逻辑封装到一个工厂里面,然后调用工厂拿到对象(这里要依赖接口)。这个工厂可以使用聚合从外部传入,由驱动类创建工厂对象。如果这个工厂是唯一的,我们可以将创建对象的方法也设置为static,这样就可以直接调用这个类的方法而不需要创建这个工厂对象
静态工厂,缺点是披萨属性特别多时,需要编写大量的披萨类,维护起来比较麻烦
抽象工厂
简单工厂只有一个工厂,要把所有的Bean的创建都集中在这一个工厂里面,显得有些冗杂,比如下面这个例子,一共有四个披萨,如果要用简单工厂就要写四个if-else,假如以后影响披萨种类的因素变多,每种因素都要自由组合,最后披萨的数量就会非常之多。本质上是没有将影响披萨的因素分离出来而是去枚举每一种可能出现的披萨。而抽象方法模式则是按照某些影响因素将工厂分为几个具体的工厂(工厂子类簇),每个工厂再按照逻辑创建bean,而至于按照什么因素将工厂分类就要具体情况具体分析。这样即使Bean的种类很多,不同种类的Factory创建Bean的过程是互不干扰的,可以让程序的结构更加清晰(前提是划分工厂的依据合理)。所以抽象工厂本质上就是对工厂进行了分类,假如bean很多很多,只用一个工厂来创建bean会使得这个工厂有些臃肿,这时候可以考虑将这个简单工厂拆分成若干不同类别的工厂,也就成了抽象工厂,基类保留创建bean的公共部分,具体的实现类再去保留个性的部分
interface Pizza{
void make();
void bake();
void cut();
void send();
}
abstract class AbstractPizza implements Pizza{
@Override
public void bake() {
System.out.println("烘烤");
}
@Override
public void cut() {
System.out.println("切分");
}
@Override
public void send() {
System.out.println("送达");
}
}
class BeijingCheesePizza extends AbstractPizza{
@Override
public void make() {
System.out.println("make BeijingCheesePizza");
}
}
class ShanghaiCheesePizza extends AbstractPizza{
@Override
public void make() {
System.out.println("make ShanghaiCheesePizza");
}
}
class BeijingZhishiPizza extends AbstractPizza{
@Override
public void make() {
System.out.println("make BeijingZhishiPizza");
}
}
class ShanghaiZhishiPizza extends AbstractPizza{
@Override
public void make() {
System.out.println("make ShanghaiZhishiPizza");
}
}
abstract class AbstractPizzaFactory{
abstract Pizza createPizza(String tasteType);
}
class BeijingPizzaFactory extends AbstractPizzaFactory{
@Override
Pizza createPizza(String tasteType) {
switch (tasteType){
case "cheese":
return new BeijingCheesePizza();
case "zhishi":
return new BeijingZhishiPizza();
default:
return null;
}
}
}
class ShanghaiPizzaFactory extends AbstractPizzaFactory{
@Override
Pizza createPizza(String tasteType) {
switch (tasteType){
case "cheese":
return new ShanghaiCheesePizza();
case "zhishi":
return new ShanghaiZhishiPizza();
default:
return null;
}
}
}
public class PizzaFactoryDemo {
public static AbstractPizzaFactory pizzaFactory;
public static AbstractPizzaFactory getPizzaFactory(String location){
switch (location){
case "Beijing":
return new BeijingPizzaFactory();
case "Shanghai":
return new ShanghaiPizzaFactory();
default:
return null;
}
}
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
String pizzaLocation = scanner.nextLine();
pizzaFactory=getPizzaFactory(pizzaLocation);
while(scanner.hasNext()){
Pizza pizza= pizzaFactory.createPizza(scanner.nextLine());
if(pizza!=null) {
pizza.make();
pizza.bake();
pizza.cut();
pizza.send();
}else{
System.out.println("没有这种披萨");
}
}
}
}
小结
如果Bean的种类很少使用简单工厂,Bean的种类很多使用抽象工厂
原型模式(实现clone)
场景:克隆羊
创建10只姓名为Tom,年龄为1,颜色为白色的羊
原始方法(灵活性差)
@Data
@NoArgsConstructor
@AllArgsConstructor
class Sheep{
private String name;
private Integer age;
private String color;
}
public class CloneSheepDemo {
public static void main(String[] args) {
Sheep prototypeSheep=new Sheep("tom",1,"白");
Sheep sheep1=new Sheep(prototypeSheep.getName(), prototypeSheep.getAge(), prototypeSheep.getColor());
Sheep sheep2=new Sheep(prototypeSheep.getName(), prototypeSheep.getAge(), prototypeSheep.getColor());
Sheep sheep3=new Sheep(prototypeSheep.getName(), prototypeSheep.getAge(), prototypeSheep.getColor());
Sheep sheep4=new Sheep(prototypeSheep.getName(), prototypeSheep.getAge(), prototypeSheep.getColor());
Sheep sheep5=new Sheep(prototypeSheep.getName(), prototypeSheep.getAge(), prototypeSheep.getColor());
}
}
先创建一只满足要求的羊作为原型,然后调用有参构造方法new十只羊,参数来源为这只羊
缺点:不够灵活和优雅,需要cv大量的代码,假如后面这只羊需要添加一个属性,所以创建这个羊的代码都需要改,非常麻烦
解决方法:原型模式(简单而优雅)
让需要clone的类实现Cloneable接口,然后实现里面的Clone方法
@Data
@NoArgsConstructor
@AllArgsConstructor
class Sheep implements Cloneable{
private String name;
private Integer age;
private String color;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
/**
* @author 李天航
*/
public class CloneSheepDemo {
@SneakyThrows
public static void main(String[] args) {
Sheep prototypeSheep=new Sheep("tom",1,"白");
Sheep sheep1= (Sheep) prototypeSheep.clone();
Sheep sheep2= (Sheep) prototypeSheep.clone();
Sheep sheep3= (Sheep) prototypeSheep.clone();
Sheep sheep4= (Sheep) prototypeSheep.clone();
Sheep sheep5= (Sheep) prototypeSheep.clone();
System.out.println(sheep1);
System.out.println(sheep2);
System.out.println(sheep3);
System.out.println(sheep4);
System.out.println(sheep5);
}
}
这样在进行clone的适合我们只需要调用原型的clone方法即可,如果后面增加了属性我们也不需要修改代码,由父类Object来帮我们完成拷贝工作,我们也可以加上一些自己的逻辑。同时clone方法是native方法,由字节码来高效完成拷贝工作,比我们调用构造函数的效率要高。同时客户端clone时无需关系clone具体是怎么实现的,具体的实现交给clone方法内部来完成。
而实现Cloneable接口,为了让这个类可克隆,否则会抛出CloneNotSupportedException异常
Spring源码
我们在注入bean的时候,除了可以将bean设置为单例的(singleton),也可以将bean设置为多例的(prototype),设置为多例的时候,创建bean的方式就是原型模式
浅拷贝和深拷贝
浅拷贝
假如类中有一个引用类型的变量:
@NoArgsConstructor
@AllArgsConstructor
class Hobby{
String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Sheep implements Cloneable{
private String name;
private Integer age;
private String color;
private Hobby hobby;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
此时如果直接使用Object的拷贝方法,基本数据类型和String类型可以被正常复制,而引用类型则是进行引用复制而不会创建新的对象
可以看到所以的Sheep对象中的hobby都指向同一个对象
此时的拷贝就是浅拷贝,相当于对上述所以字段使用等号=
来赋值
深拷贝
深拷贝就是字段中依赖的其他对象也都是新的对象,而不是和原型指向相同的对象
可以通过将依赖的对象拿出来单独进行clone,这样就能达到深拷贝:
@NoArgsConstructor
@AllArgsConstructor
class Hobby implements Cloneable{
String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Sheep implements Cloneable{
private String name;
private Integer age;
private String color;
private Hobby hobby;
@Override
protected Object clone() throws CloneNotSupportedException {
Sheep clone = (Sheep) super.clone();
clone.setHobby((Hobby) hobby.clone());
return clone;
}
}
但是如果依赖的对象很多,甚至依赖的对象还依赖了其他类的对象,这样一个一个clone会很麻烦,因而我们可以采用JDK的序列化机制来实现深拷贝。
@NoArgsConstructor
@AllArgsConstructor
class Hobby implements Serializable{
String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Sheep implements Serializable{
private String name;
private Integer age;
private String color;
private Hobby hobby;
protected Object deepClone() throws CloneNotSupportedException {
ByteArrayOutputStream byteArrayOutputStream=null;
ObjectOutputStream objectOutputStream=null;
ByteArrayInputStream byteArrayInputStream=null;
ObjectInputStream objectInputStream=null;
try {
byteArrayOutputStream=new ByteArrayOutputStream();
objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
byteArrayInputStream=new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
objectInputStream=new ObjectInputStream(byteArrayInputStream);
return objectInputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}finally {
try {
assert objectOutputStream != null;
assert objectInputStream != null;
objectOutputStream.close();
objectInputStream.close();
byteArrayInputStream.close();
byteArrayOutputStream.close();
} catch (IOException | NullPointerException e) {
e.printStackTrace();
}
}
return null;
}
}
看着代码很多,其实大部分都是在处理异常和关闭流和初始化流,核心代码就六行:
//开启一个字节数组流作为缓冲区
byteArrayOutputStream=new ByteArrayOutputStream();
//将这个对象序列化并写入上面的缓冲区
objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
//创建对象数据流并从缓冲区中读进来进行反序列化
byteArrayInputStream=new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
objectInputStream=new ObjectInputStream(byteArrayInputStream);
//读对象的时候会创建一个新的对比保存反序列化的结果
return objectInputStream.readObject();
因为需要序列化和反序列化,所以需要实现Serializable,其实就只是一个标志,表示这个对象可以序列化
另外,因为我们的深拷贝没有用到super.clone()所以可以不用实现Cloneable接口,自己另写一个方法即可
如图所示,所有的Hobby对象都不一样
使用JDK的序列化机制就不需要我们一个一个手动clone引用类型的对象了,十分方便,也推荐使用
特点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hYTQ20V7-1654221719953)(https://s2.loli.net/2022/06/03/WgH73IvERJeZr2M.png)]
原型模式深拷贝违背了OCP原则(是吧,想要遵循所有的原则几乎是不可能的),因为我们要为之前写好的类配备一个clone方法,或者让他实现一个Serializable接口。不过这个修改成本其实是很低的,修改后不会改变原有的业务逻辑,并且我们一般会在开发时直接继承Serializable接口,也就消除了这个问题。(我们只需要知道它违反了这个原则即可,需要用的时候还是正常用,不用原型模式耦合度会更大)
建造者模式(将产品,建造者,建造规范,指挥者分开)
场景:盖房子
直接想到的方式是将房子的属性,建造房子的步骤,以及调用这些步骤建造的过程都封装到一个类里面,具体的步骤设计为抽象方法,交给子类去实现
这样做将房子和建造房子的过程耦合在了一起,我们将建造房子的过程分离出来,就成了建造者。这样职责更加分明,耦合度更低
建造者模式的四个角色
- product就是我们需要建造的产品
- Builder是建造者的抽象层,用于定义有哪些建造的步骤,具体的实现由子类来完成。同时会调用Director来建造产品而不关心产品的建造过程
- Director负责调用Builder中的步骤完成建造过程,而不关心具体的实现
- 子类只需要实现建造的具体步骤,供Director来调用即可
Director只负责掌管产品制造的顺序,Builder负责实现产品每一步的生产过程
Director相当于盖房子的指挥者,AbstractBuilder相当于应聘要求,Builder则是实际干活的工人
这样如果建造流程发生了变化,只用修改Director就行了,如果有不同的解决方案,我们只需要创建不同的Director即可,建造不同的房子我们创建对应的Builder即可(如果像之前那样写在一起,实际上会将Builder和Director自由组合,产生大量的House类,修改时会更改大量的代码,耦合度很高)
并且创建房子的步骤可能很复杂,步骤之间可能会有一些依赖关系,这时候将步骤步骤到Director里面可以使得程序逻辑更加清晰,减少出错的概率
盖房子实现
House
@Data
public class House implements Cloneable, Serializable {
private String base;
private String wall;
private String roof;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
这里可以配合原型模式,这样houseDirector每调用一次build方法都会建造一个新的房子
AbstractHouseBuilder
负责设置建造的接口与设计规范,封装建造房子的通用逻辑
public abstract class AbstractHouseBuilder {
protected House house=new House();
abstract protected void buildBase();
abstract protected void buildWall();
abstract protected void buildRoof();
public House createHouse(){
try {
return (House) house.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
CommonHouseBuilder
建造小房子的实现类
public class CommonHouseBuilder extends AbstractHouseBuilder{
@Override
protected void buildBase() {
house.setBase("5米地基");
System.out.println("打地基");
}
@Override
protected void buildWall() {
house.setWall("3米的墙");
System.out.println("盖墙");
}
@Override
protected void buildRoof() {
house.setRoof("小房顶");
System.out.println("盖房顶");
}
}
HighHouseBuilder
盖大房子的实现类
public class HighHouseBuilder extends AbstractHouseBuilder{
@Override
protected void buildBase() {
house.setBase("10米地基");
System.out.println("打地基");
}
@Override
protected void buildWall() {
house.setWall("100米墙");
System.out.println("搭建墙");
}
@Override
protected void buildRoof() {
house.setRoof("大房顶");
System.out.println("盖房顶");
}
}
HouseDirector
盖房子的指挥者,调用建造者来创建对象并交付实例对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class HouseDirector {
private AbstractHouseBuilder houseBuilder;
House buildHouse(){
houseBuilder.buildBase();
houseBuilder.buildWall();
houseBuilder.buildRoof();
return houseBuilder.createHouse();
}
}
Client(调用方)
中间可以切换建造者,每次建造出的房子也都不一样
public class Client {
public static void main(String[] args) {
HouseDirector houseDirector=new HouseDirector(new CommonHouseBuilder());
System.out.println(houseDirector.buildHouse());
System.out.println(houseDirector.buildHouse());
houseDirector.setHouseBuilder(new HighHouseBuilder());
System.out.println(houseDirector.buildHouse());
}
}
源码分析StringBuilder
可以看到链式编程的思想
这里因为建造的流程本身就是不确定的,我们可以调用很多次append方法来添加字符串,所以实际的指挥者,其实是我们的业务代码。所以如果建造的流程不确定,我们应当将建造的接口暴露给Client,让Client来调用接口来建造,每一步建造都能得到建造后的结果(return this),这样也能包含建造者模式的精髓。而如果建造的过程是确定的,并且有时候会很复杂,这时候应当将建造的过程封装到Director中
抽象工厂模式专注于创建不同种类的对象
建造者模式则专注于创建不同属性的对象
工厂模式只负责将对象的创建过程和使用过程分离开,并不关心这个对象具体是怎么创建的,因而实际上创建对象的具体过程可以交给建造者模式来完成
建造者模式适用于建造同类的产品(属性要是一样的),如果需要创建的产品之间差异很大,可能不适合用建造者模式
以上设计模式属于创建型设计模式===========
下面介绍结构性设计模式==========
适配器模式(将不同类统一成一个类)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o7mOD2wt-1654221719957)(https://s2.loli.net/2022/05/31/C1LdIksHAoWB8xy.png)]
适配器是让原来不能兼容的两个类可以兼容,可以交Adapter也可以叫Wrapper
实现原理:调用源接口的方法来实现新接口的功能
场景:手机充电
手机充电需要5V电源,而现在只有220V的电源。
解决方案是给电源添加一个适配器
类适配器(通过继承src来适配)(不推荐)
实现:
手机(需要5V电压来充电)
public class Phone {
void charging(Voltage5V v){
if(v.getVoltage5V()==5){
System.out.println("可以充电");
}else{
System.out.println("电压不满足要求");
}
}
}
220V电源
public class Voltage220V {
public int getVoltage220V(){
return 220;
}
}
创建5V电源的接口
public interface Voltage5V {
public int getVoltage5V();
}
适配器继承220V电源实现5V电源的功能
public class ClassAdapter extends Voltage220V implements Voltage5V{
@Override
public int getVoltage5V() {
return getVoltage220V()/44;
}
}
客户端
public class Client {
public static void main(String[] args) {
Phone phone=new Phone();
phone.charging(new ClassAdapter());
}
}
phone只知道传进来的是一个5V的电源,而不关心这个电源是从哪里来的,客户端通过传入适配器即可完成功能
优缺点
缺点很明显,直接继承被适配的类是一种很不优雅的行为,因为java是单继承,迫使dst必须是一个接口,有一定的局限性。继承的好处是可以改写src中的方法来适配dst,灵活性更强。其他的适配器方法可以解决这些局限性。
对象适配器(将继承改成组合)(推荐)
根据合成复用原则,能使用组合的情况就不要使用继承,基于这个思想将上述的继承改成组合,这样dst也不再要求必须是接口,使用更加灵活
只需要将适配器的继承改成组合即可,被适配的对象在构造函数中被传进来
public class ObjectAdapter implements Voltage5V {
Voltage220V v;
ObjectAdapter(Voltage220V v){
this.v=v;
}
@Override
public int getVoltage5V() {
return v.getVoltage220V()/44;
}
}
客户端:
public class Client {
public static void main(String[] args) {
Phone phone=new Phone();
phone.charging(new ObjectAdapter(new Voltage220V()));
}
}
接口适配器模式(可以不实现方法的接口)
接口适配器模式和上面两种的目的不同,接口适配器是给接口一个默认实现(比如空方法,抛出异常,或者一些简单的逻辑),这样创建对象或者继承这个适配器时,不用实现接口的所有方法,可以按照自己的需求实现自己想要的方法。
简而言之就是给接口一个默认实现,也可以在接口中使用default来达成相同的目的
interface Operators{
void operator1();
void operator2();
void operator3();
}
abstract class OperatorAdapter implements Operators{
@Override
public void operator1() {
}
@Override
public void operator2() {
}
@Override
public void operator3() {
}
}
public interface InterfaceAdapterDemo {
public static void main(String[] args) {
OperatorAdapter operator = new OperatorAdapter() {
@Override
public void operator1() {
System.out.println("使用方法1");
}
};
operator.operator1();
}
}
接口适配器,适用于不想实现接口所有的方式时使用
源码示例:SpringMVC的HandlerAdapter
SpringMVC会根据URL进行最长前缀匹配原则拿到handler(也可以叫controller)
然后根据handler拿到能处理这个handler的Adapter,SpringMVC提供的Adapter有这些:
遍历所有的Adapter,调用他们的support方法判断支不支持,来找到能处理的Adapter,我们自己写的Controller里的方法会由RequestMappingHandlerAdapter 来处理(处理带有@RequestMapping注解的方法)
然后会在DispatcherServlet调用Adapter的handle方法来执行目标方法。拿到的handler就是我们在controller中编写的业务逻辑,而想要执行这些业务逻辑需要我们配好好它需要的参数,以及处理好它的返回值,而完成这些工作的就是Adapter。
适配器模式其实就是对src进行了包装,封装成一个同一的格式,使其能够被外部统一调用。比如SpringMVC的HandlerAdapter,各个Handler其实是毫不关联的,通过适配器进行包装,这样在DisPatcherServlet中就能够利用多态来同一调用,若后面有新的handler也只需要配置一个新的适配器即可完成功能扩展,十分方便。
桥接模式(将一个继承关系变为组合关系)
场景:多种手机
现在有三类手机,每种类别的手机有三种不同的品牌,每种手机都有相同的功能(打电话等),现在需要我们创建调用某种手机的call方法
传统实现:
类似工厂模式,先按照手机类别创建三个类,每个类下根据品牌创建具体的手机对象
可以实现功能,但是当类很多时会出现组合爆炸
桥接模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-00ZJVzHm-1654221719961)(https://s2.loli.net/2022/06/03/GFOHP7SnfRbiZUa.png)]
桥接模式本质是使用合成复用原则将其中的一个继承变为了组合,使得代码的耦合度降低
public interface Brand {
void call();
}
public class Apple implements Brand{
@Override
public void call() {
System.out.println("Apple");
}
}
public abstract class Phone {
private final Brand brand;
Phone(Brand brand){
this.brand=brand;
}
void call(){
brand.call();
}
}
public class SplitPhone extends Phone{
SplitPhone(Brand brand) {
super(brand);
System.out.println("SplitPhone");
}
}
其中抽象类Phone其实就起到了桥接的作用,通过调用父类的方法间接使用Brand中的方法,这样就可以避免组合爆炸的问题,降低了耦合性。这样增加手机的类型时,只需要实现Phone接口即可
个人思考后改进桥接模式
上述是将一个继承变成了组合而保留了另一个继承关系。但是如果将另一个继承也变成组合,这样设计的耦合度会更低。
也就是将上述的两个特征抽象出来,变为phone的两个属性,这样就可以在构造方法中传入这两个属性
@AllArgsConstructor
public class Phone {
Type type;
Brand brand;
void call(){
brand.call();
type.call();
}
}
这样做,无论是增加品牌还是增加类型,这个Phone都不用发生变化,耦合度更低
但是也有它的缺点:灵活性会降低,并且只有所有手机的call方法的流程都是这样才能使用,因而桥接模式实用性更广
使用桥接模式时:
SplitPhone(Brand brand) {
super(brand);
System.out.println("SplitPhone");
}
可以在方法前后很灵活得进行修改,相当于进行了代理
而全部都使用组合后:
void call(){
brand.call();
type.call();
}
type的方法只能放在前面或者后面,想要实现功能会变得更加复杂,并且可能含义不清晰,不利于我们编写代码。因为继承有衍生的含义而组合代表是这个类的一部分。
小结
桥接模式是将一部分继承关系变为组合关系来降低耦合性,但是不能将所有的继承都变成组合,否则会使得设计模式的适用性降低,如果出现了整个系统都不能兼容添加的这个类的情况,这个系统的根基就会被破坏。因而设计模式要在低耦合和高适用性之间做出权衡。像这种有多个因素决定种类的情况,可以将一个主要因素适用继承实现,其余的次要因素用组合来实现。
桥接模式的作用就是减少继承的个数
装饰者模式(动态添加新功能)
场景:星巴克咖啡
装饰者模式
装饰者模式实现
package DecoratorMode.DecoratorMode1;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
abstract class Coffee{
protected int price;
protected String description;
abstract int cost();
abstract String getDescription();
}
class LongBlackCoffee extends Coffee{
LongBlackCoffee(){
super(5,"黑咖啡");
}
@Override
int cost() {
return super.price;
}
@Override
String getDescription() {
return super.description;
}
}
class ShortBlackCoffee extends Coffee{
public ShortBlackCoffee() {
super(4,"浓缩咖啡");
}
@Override
int cost() {
return super.price;
}
@Override
String getDescription() {
return super.description;
}
}
abstract class Decorator extends Coffee{
Coffee coffee;
}
class Chocolate extends Decorator{
Chocolate(Coffee coffee){
super.coffee=coffee;
price=3;
description=" 巧克力 ";
}
@Override
int cost() {
return price+coffee.cost();
}
@Override
String getDescription() {
return description+coffee.getDescription();
}
}
class Milk extends Decorator{
Milk(Coffee coffee){
super.coffee=coffee;
price=2;
description=" 牛奶 ";
}
@Override
int cost() {
return price+coffee.cost();
}
@Override
String getDescription() {
return description+coffee.getDescription();
}
}
public class Client {
public static void main(String[] args) {
Coffee coffee=new LongBlackCoffee();
coffee=new Milk(coffee);
coffee=new Chocolate(coffee);
System.out.println(coffee.getDescription());
System.out.println(coffee.cost());
}
}
其实本质上就是一个链表,每加入一个配料(装饰者),就相当于在使用头插法在链表的头部加入了一个结点
而计算费用的过程则是cost方法的递归调用
而如果从面向对象的角度来理解,所有的装饰者和主体都是继承了一个抽象类,还是拿咖啡举例,一开始是普通的咖啡,装饰后仍然是一杯咖啡,每一次装饰都是将一个咖啡进行包装,包装后仍然是一杯咖啡,这样就可以继续被装饰。计算费用时会从最外层开始递归调用cost方法,得到费用。
使用集合来代替这个隐式的链表
因为我们这个场景比较简单,所有装饰器对属性的处理都是一样的(做加法),如果是这样的话,我们可以使用集合类型来代替这个链表
@Data
@AllArgsConstructor
@NoArgsConstructor
abstract class Coffee{
protected int price;
protected String description;
}
class LongBlackCoffee extends Coffee{
LongBlackCoffee(){
super(5,"黑咖啡");
}
}
class ShortBlackCoffee extends Coffee{
public ShortBlackCoffee() {
super(4,"浓缩咖啡");
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
abstract class Decorator{
protected int price;
protected String description;
abstract int cost(int rawCost);
}
class Milk extends Decorator{
Milk(){
super(3,"牛奶");
}
@Override
int cost(int rawCost) {
return rawCost+price;
}
}
class Chocolate extends Decorator{
Chocolate(){
super(3,"巧克力");
}
@Override
int cost(int rawCost) {
return rawCost+price;
}
}
class Order{
Coffee coffee;
List<Decorator> decorators=new LinkedList<>();
Order(Coffee coffee){
this.coffee=coffee;
}
void addDecorator(Decorator decorator){
decorators.add(decorator);
}
int getCost(){
int cost=coffee.price;
for (Decorator decorator : decorators) {
cost=decorator.cost(cost);
}
return cost;
}
}
public class Client {
public static void main(String[] args) {
Order order=new Order(new LongBlackCoffee());
order.addDecorator(new Chocolate());
order.addDecorator(new Milk());
System.out.println(order.getCost());
}
}
我们直接使用List也可以达到类似的效果,并且也更好理解
对比两者
前者将前面装饰的结果作为一个整体来加上本次装饰
而后者是遍历每一个装饰者,轮流对主体进行处理
前者更加灵活,后者更加好理解。一般情况下两者方式都可以实现功能。
JDK源码在开发时,可能还没有util这个工具类,所以有时候会看到装饰者模式的影子,但在实际开发中,使用后者不仅功能可以达到,扩展性,耦合度都低于前者,开发时使用后者即可。
源码分析IO流
IO流是在JDK1.0引入的,而util包是在JDK1.2引入的,在开发IO流的时候还没有List集合,所以自然会使用我们的装饰者模式
我们既可以叫装饰,也可以叫它包装
组合模式(为一对多提供统一的接口)
也叫部分整体模式,创建对象组的树形结构,将对象组合成树形结构以表示“整体-部分”的层次关系
场景:院校展示
每个学校有多个学院,每个学院有多个系
按照这个思路,使用List来实现即可,实际上上一节我们也用到了组合模式
实现
编写一个Organization类作为所有组件的抽象,里面给出方法的默认实现(适配器模式)
@Data
public abstract class Organization {
String name;
String description;
void add(Organization organization){
throw new UnsupportedOperationException();
}
void remove(Organization organization){
throw new UnsupportedOperationException();
}
abstract void print();
}
然后不同类别的组件实现这个类,使用List集合来表示一对多的关系
public class College extends Organization{
List<Organization> departments =new LinkedList<>();
@Override
void add(Organization organization) {
departments.add(organization);
}
@Override
void remove(Organization organization) {
departments.remove(organization);
}
@Override
void print() {
System.out.println(name);
departments.forEach(Organization::print);
}
}
public class University extends Organization{
List<Organization> colleges =new LinkedList<>();
@Override
void add(Organization organization) {
colleges.add(organization);
}
@Override
void remove(Organization organization) {
colleges.remove(organization);
}
@Override
void print() {
System.out.println(name);
colleges.forEach(Organization::print);
}
}
public class Department extends Organization{
@Override
void print() {
System.out.println(name);
}
}
如果内部的组合关系发生了变化(假如Colleage变成了第一层的结点),结点内部基本上不用变,因为都是依赖Organization这个抽象层。但是这也要求各个组件内部比较相似,内部差别很大的则不适合使用组合模式
外观模式(将一个场景中的功能进行封装)
场景:家庭影院
使用DVD,音响等设备的功能
传统方案:创建对应的对象直接使用
外观模式:提供一个统一的客户端来调用这些设备
完成一个功能需要许多组件配合完成,如果我们直接在main方法写回显得杂乱无章。我们可以将实现的每一个功能封装到一个外观类中,由这个外观类来负责完成这个场景的具体功能,而客户端通过调用外观类的方法来完成具体的的功能。这里外观类起到协调组件配合使用的功能
实现
其实下面这个一看就知道什么意思了,实现一个功能需要多个组件配合使用,我们可以把这个流程封装起来,等我们需要使用的时候直接调用这个方法即可,把这个场景所有涉及到的功能都实现出来就是我们的外观模式
public class HomeTheaterFacade {
Player player=Player.getPlayer();
Screen screen=Screen.getScreen();
void start(){
screen.on();
player.on();
}
void pause(){
player.pause();
}
void restart(){
player.restart();
}
void off(){
screen.off();
player.end();
}
}
调用外观类实现功能:
public class Client {
public static final HomeTheaterFacade client=new HomeTheaterFacade();
public static void main(String[] args) {
client.start();
client.pause();
client.restart();
client.off();
}
}
这样当我们需要多次使用某一功能时,代码量也会减少,功能内部需要增添一些细节时也会很方便
外观模式主要是为了让系统更有层次,更利于维护,如果子系统的功能已经足够简单则不需要使用外观模式。外观模式使用得过多和过少都不好,需要我们灵活处理。
享元模式(如果缓存中有从缓存中拿,缓存没有就创建)
场景:网站外包
我们想要一个网站以不同的形式展现出来。
传统做法是每需要一个网站就new一个这样的对象出来,这样的缺点是系统内部会出现大量相似的对象,使得系统效率降低,要是网站出现了bug,所有创建的对象都要重新创建,维护起来会很麻烦。假如有多个用户想要以新闻的形式发布,我们可以让这些用于共享一个以新闻发布的网站,这样就不用重复地创建相同的对象,网站要是出现了bug,修改起来也会很容易,这也就是下面所说的享元模式
享元模式
也叫蝇量模式,常用于系统底层优化性能,解决重复对象的内存浪费问题,当系统中由大量相似对象,需要内存时,不需要总是创建新的对象,而是一次使用的时候,将创建的对象放入缓冲池中,其他人再使用的时候直接从缓冲池中那就行了,这样就可以避免创建大量的重复对象。享元模式在Java中用的非常多比如各种连接池(提前创建好连接对象,要用的时候从池中直接拿,用完再放回去),String类(使用的字符串都会从常量池中拿,如果没有则创建一个字符串放入常量池,再把它的引用拿过来赋值)
内部状态:对象的类别(黑白两种)
外部状态:对象赖以生存的,不可共享标记(在这个坐标上需要棋子时,直接从缓冲区中拿,而不需要再次创建对象)(谁来使用这个共享的对象)
实现
网站的抽象类:
@Data
public abstract class WebSite {
private String type;
abstract void use(User user);
}
网站具体的实现类:
public class ConcreteWebsite extends WebSite{
ConcreteWebsite(String type){
setType(type);
}
@Override
void use(User user) {
System.out.println("当前网站的类型是:"+getType()+"\n使用的用户是:"+user.getName());
}
}
用户:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
String name;
}
网站工厂,里面封装了网站的使用池
其中内部模式是type,也就是网站的类型,而外部模式是用户(线程)。每个网站有不同的类型,而每个类型最多只会有一个示例,所有用户共享这些示例,这样就可以避免创建过多的重复对象,提供系统性能
public class WebFactory {
private static final WebFactory webFactory=new WebFactory();
private static final HashMap<String,WebSite> webSitePool=new HashMap<>(3);
private WebFactory(){}
public static WebFactory getWebFactory(){
return webFactory;
}
public WebSite getWebSite(String type){
if(webSitePool.containsKey(type)){
return webSitePool.get(type);
}else{
WebSite webSite=new ConcreteWebsite(type);
webSitePool.put(type,webSite);
return webSite;
}
}
public int getWebSiteCount(){
return webSitePool.size();
}
}
得到结果:
微信形式的网站调用了两次,但是只创建了一个对象
是不是和Spring容器很像?Spring的整个框架就是基于享元模式来实现了,所有的组件都放在单例池中,如果我们设置的是单例对象,获取对象时会先从单例池中找,如果没有找到就创建一个组件。(当然,创建对象的流程远不止这些)
享元模式源码示例:Integer
我们来查看valueof的源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer在类加载的时候,会开一个数组存放值为[-128,127]的Integer对象,如果我们需要这个范围的Integer对象,它就会直接将缓存中的对象返回,如果不在这个返回内就会创建一个新的Integer对象。(因为[-128,127]使用频率较高),这也是x==z的原因,因为x,z获取的都是cache中的对象,所以是是同一个对象,而x1,x2因为不在[-128,127]这个范围内,所以会创建两个不同的对象
注意细节
代理模式(对原有方法进行增强)
基本介绍
类图
通过调用代理对象来间接使用模板对象的方法,代理对象和目的对象需要实现相同的接口或者继承相同的父类
代理对象会对原有方法进行补充
静态代理(手动编写代理类)
我们自己手动创建一个代理对象,和目标类实现相同的接口或者父类,然后对目标类的每一个方法进行改写
编写一个接口:
public interface ITeacher {
void teach();
}
编写目标类:
public class Teacher implements ITeacher{
@Override
public void teach() {
System.out.println("授课");
}
}
编写代理类:
public class TeacherProxy implements ITeacher{
ITeacher teacher;
TeacherProxy(ITeacher teacher){
this.teacher=teacher;
}
@Override
public void teach() {
System.out.println("开始代理");
teacher.teach();
System.out.println("代理结束");
}
}
客户端调用:
public class Client {
public static void main(String[] args) {
ITeacher teacher=new TeacherProxy(new Teacher());
teacher.teach();
}
}
客户端调用时,就感觉是在调用目标对象一样
静态代理的优点是灵活,缺点是如果目标类的方法很多,我们要为每个方法都编写代理方法,代码量很庞大,并且当目标类增加方法时,目标类和代理类都需要修改,代理类和目标类的耦合度很大,为了解决这个问题我们可以使用动态代理
JDK动态代理
目标对象需要实现一个接口,而代理对象由JDK帮我们生成不需要我们来实现接口
类图:
JVM利用反射机制生成代理对象,我们通过调用代理对象来间接调用目标对象的方法
编写一个代理工厂来为实现了接口的方法生成代理对象(包括已经被代理的对象)
实现:
和静态代理一样需要实现一个接口,对接口包含的所有方法进行代理(包括toString等Object里的方法)
代理对象工厂:
public class ProxyFactory {
Object getProxyInstance(Object target){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始代理");
Object result = method.invoke(target, args);
System.out.println("代理结束");
return result;
}
}
);
}
}
对所有传入的对象进行动态代理,实现原理是调用Proxy.newProxyInstance方法,这个方法需要三个参数:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
ClassLoader loader:目标对象的类加载器,直接使用target.getClass().getClassLoader()即可
Class<?>[] interfaces:目标对象实现的接口,直接使用target.getClass().getInterfaces()即可
InvocationHandler h:方法具体的增强逻辑,我们可以创建一个匿名内部类,也可以使用Lamda表达式
这样这个工厂可以为任何方法创建代理对象,代理的逻辑定义在内部类中
CGLIB动态代理
不需要实现接口,使用性能很高的ASM细节吗生成框架,常用于各种AOP框架
被代理的类不能是final的
使用的是拦截器机制
实现
创建目标类:
public class Teacher {
void teach(){
System.out.println("上课");
}
}
创建代理工厂:
public class ProxyFactory implements MethodInterceptor {
private static final ProxyFactory proxyFactory=new ProxyFactory();
private final ThreadLocal<Object> targetMap=new ThreadLocal<>();
private ProxyFactory(){}
public static ProxyFactory getProxyFactory(){
return proxyFactory;
}
public Object getProxyInstance(Object target){
targetMap.set(target);
Enhancer enhancer=new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("CGLIB 动态代理");
System.out.println(o.getClass());
Object result = methodProxy.invoke(targetMap.get(),objects);
methodProxy.invokeSuper(o,objects);
method.invoke(targetMap.get(),objects);
targetMap.remove();
System.out.println("代理结束");
return result;
}
}
调用目标方法有三种:
methodProxy.invoke(targetMap.get(),objects);
methodProxy.invokeSuper(o,objects);
method.invoke(targetMap.get(),objects);
targetMap.get()得到的是被代理的原对象,而参数中的o是提前暴露的代理对象,一定不要视图直接使用o,否则会爆栈
method是原原方法,methodProxy是代理方法
创建客户端测试:
public class Client {
private static final ProxyFactory proxyFactory=ProxyFactory.getProxyFactory();
public static void main(String[] args) {
Teacher teacher = (Teacher) proxyFactory.getProxyInstance(new Teacher());
teacher.teach();
}
}
======以上是结构型设计模式=
======下面是行为型设计模式=
模板方法(将公共的部分放在抽象类,不同的部分放在子类)
需求:制作不同材料的豆浆
这个设计模式很简单,我们很容易就能想到定义一个抽象类,让子类来实现
将相同的方法在抽象类中实现
将不同的方法设置为抽象方法,延迟到子类来实现
在抽象类中定义的通用方法叫做模板方法,模板方法可以调用其他模板方法或者抽象方法来实现算法(架构师把架子搭好,让下面的小弟来实现)
模板方法如果不想让子类重写,可以设置为final
实现
抽象方法,编写共有的逻辑
public abstract class AbstractSoyMilk {
public final void make(){
selectBeans();
addCondiments();
soak();
beat();
}
private void selectBeans(){
System.out.println("选择黄豆");
}
protected abstract void addCondiments();
private void soak(){
System.out.println("浸泡");
}
private void beat(){
System.out.println("beat");
}
}
抽象的逻辑可以让子类去实现,比如在客户端使用匿名内部类:
public class Client {
public static void main(String[] args) {
AbstractSoyMilk soyMilk=new AbstractSoyMilk() {
@Override
protected void addCondiments() {
System.out.println("添加花生");
}
};
soyMilk.make();
}
}
这样做有个小缺点,当我们不像添加配料时,也要实现这个方法,可以使用钩子方法判断这个操作是否需要调用
public abstract class AbstractSoyMilk {
public final void make(){
selectBeans();
if(wantToAddCondiments()) {
addCondiments();
}
soak();
beat();
}
private void selectBeans(){
System.out.println("选择黄豆");
}
protected abstract void addCondiments();
private void soak(){
System.out.println("浸泡");
}
private void beat(){
System.out.println("beat");
}
protected boolean wantToAddCondiments(){
return true;
}
}
public class Client {
public static void main(String[] args) {
AbstractSoyMilk soyMilk=new AbstractSoyMilk() {
@Override
protected void addCondiments() {
System.out.println("添加花生");
}
};
soyMilk.make();
AbstractSoyMilk pureMilk=new AbstractSoyMilk() {
@Override
protected void addCondiments() {
}
@Override
protected boolean wantToAddCondiments() {
return false;
}
};
pureMilk.make();
}
}
这样就不会调用这个空方法(当然也可以采用默认实现的方式,这就是前面所说的适配器模式了)
命令模式(将设备的功能封装成命令并用控制器统一操作)
场景:智能家电
使用一个App控制所有的家电
基本介绍
调用者和执行者直接添加一个中介,调用只需要向某些对象发送请求,而不需要知道具体是哪些对象,具体的对象由中介来完成,这样就可以实现调用者和被调用者之间的解耦
invoker:调用者
command:命令(中介)
receiver:接收者
invoker组合command,command组合receiver
invoker通过调用command类间接调用者receiver
RemoteController其实就是我们的遥控器,遥控器里有很多命令,通过调用这些命令完成功能,每个命令都有执行和撤销两种功能,功能的实现由具体的任务接收者来完成,比如下面写的电灯。假如电灯有很多,而我们只想关闭其中的一个,那么此时Command就可以按照自己策略选择关闭哪一个电灯(命令接收者),比如常用的负载均衡策略
实现
一开始我们有一台电视,有关闭和打开两种操作,我们想要一个遥控器来控制这个设备
public class Tv {
void tvOn(){
System.out.println("打开电视");
}
void tvOff(){
System.out.println("关闭电视");
}
}
编写一个命令接口,表示所有的命令的抽象,表示遥控器上的一个按钮,或者代表一个命令
public interface Command {
void execute();
void rollback();
}
然后编写具体的命令:
每个设备都有两个按钮,代表开和关,还有一个撤回按钮。我们目前只有电视机,所以需要编写操作电视的开关按钮,同时还需要一个空按钮代表空按钮,即没有和设备设置对应关系的按钮,按下去后会没有反应
三种按钮:
public class TvOnCommand implements Command{
Tv tv;
TvOnCommand(Tv tv){
this.tv=tv;
}
@Override
public void execute() {
tv.tvOn();
}
@Override
public void rollback() {
tv.tvOff();
}
}
public class TvOffCommand implements Command{
Tv tv;
public TvOffCommand(Tv tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.tvOff();
}
@Override
public void rollback() {
tv.tvOn();
}
}
public class NullCommand implements Command{
@Override
public void execute() {
}
@Override
public void rollback() {
}
}
核心实现是遥控器,里面有Command数组代表所有的按钮,最大限制为MAX_COMMAND_COUNT,有一个双端队列记录之前执行过的操作,最大容量为MAX_ROLLBACK_COUNT,超过这个容量会去掉最早执行的操作,可以通过setCommand方法来建立按钮和命令之间的映射关系,调用rollback来回滚
public class RemoteController {
private final Command[] onCommands;
private final Command[] offCommands;
private final Deque<Command> rollbackCommands=new ArrayDeque<>();
private final int MAX_COMMAND_COUNT=5;
private final int MAX_ROLLBACK_COUNT=100;
public RemoteController(){
onCommands=new Command[MAX_COMMAND_COUNT];
offCommands=new Command[MAX_COMMAND_COUNT];
for (int i = 0; i < MAX_COMMAND_COUNT; i++) {
onCommands[i]=new NullCommand();
offCommands[i]=new NullCommand();
}
}
private void dequeAdd(Command command){
if(rollbackCommands.size()>MAX_ROLLBACK_COUNT){
rollbackCommands.removeFirst();
}
rollbackCommands.addLast(command);
}
private Command dequePoll(){
return rollbackCommands.pollLast();
}
public void setCommand(int num,Command onCommand,Command offCommand){
if(num<=MAX_COMMAND_COUNT){
System.out.println(num+"超过限制的最大数量:"+MAX_COMMAND_COUNT);
}
onCommands[num]=onCommand;
offCommands[num]=offCommand;
}
public void executeOn(int num){
if(num<MAX_COMMAND_COUNT) {
onCommands[num].execute();
dequeAdd(onCommands[num]);
}
}
public void executeOff(int num){
if(num<MAX_COMMAND_COUNT) {
offCommands[num].execute();
dequeAdd(offCommands[num]);
}
}
public void rollback(){
Command command = dequePoll();
if(command!=null){
command.rollback();
}
}
}
然后调用客户端来测试:
public class Client {
public static void main(String[] args) {
RemoteController remoteController=new RemoteController();
Tv tv=new Tv();
remoteController.setCommand(0,new TvOnCommand(tv),new TvOffCommand(tv));
remoteController.executeOn(0);
remoteController.executeOn(0);
remoteController.rollback();
remoteController.executeOff(0);
remoteController.rollback();
}
}
如果我们想要添加一台设备,前面所写的代码都不需要改变,只需要编写一个设备类,两个操作这个设备的命令,然后在客户端建立控制器和命令的映射关系,这样就能通过在控制器调用命令来使用设备的的功能。体现了设计模式的开闭原则。
注意细节
访问者模式(不同的歌手对不同的访问者进行不同的反应)
场景:歌手评分
有很多歌手,有一些男女观众来拜访这些歌手,这些歌手有好坏之分,会用不同的方式来接待这些观众
基本介绍
将数据操作和数据结构分离(是不是很像我们常说的MVC模式)
通用类图
当然,这还是太抽象了,下面还是用一个具体的例子来讲解
实现
再回顾一下需求:有很多歌手,有一些男女观众来拜访这些歌手,这些歌手有好坏之分,会用不同的方式来接待这些观众
访问者模式是用于在不同的访问者到来时,不同的元素会给出不同的反应
创建访问者的抽象类,里面有两个方法表示访问两种不同的歌手
abstract class Visitor{
String name;
Visitor(String name){
this.name=name;
}
abstract void visitGoodSinger(GoodSinger goodSinger);
abstract void visitBadSinger(BadSinger badSinger);
}
歌手的抽象类,里面有一个方法表示接待访问者,但是具体是怎么接待的需要依靠子类
interface Singer{
void accept(Visitor visitor);
}
两种歌手:好歌手和坏歌手,好歌手会说你好,坏歌手会说再见,两种歌手都有accept方法来接待访问者:歌手接待访问者发生的事情就是访问者访问歌手发生的事情。
class GoodSinger implements Singer{
@Override
public void accept(Visitor visitor) {
//好歌手接待访问者就是访问者访问了好歌手
visitor.visitGoodSinger(this);
}
public void sayHello(Visitor visitor){
System.out.println(" hello to "+visitor.name);
}
}
class BadSinger implements Singer{
@Override
public void accept(Visitor visitor) {
//坏歌手接待访问者就是访问者访问了坏歌手
visitor.visitBadSinger(this);
}
public void sayBye(Visitor visitor){
System.out.println(" goodbye to "+visitor.name);
}
}
于是具体的接待逻辑追溯到了访问者的子类中,同时把自己传了进去,这样确保了最后执行逻辑时信息的完整
两种访问者:
不同的访问者访问不同的歌手时会发生不同的事情
class ManVisitor extends Visitor{
ManVisitor(String name) {
super(name);
}
@Override
public void visitGoodSinger(GoodSinger goodSinger) {
System.out.print("The Singer finds he is a man and say");
goodSinger.sayHello(this);
}
@Override
public void visitBadSinger(BadSinger badSinger) {
System.out.print("The Singer finds he is a man and say");
badSinger.sayBye(this);
}
}
class WoManVisitor extends Visitor{
WoManVisitor(String name) {
super(name);
}
@Override
public void visitGoodSinger(GoodSinger goodSinger) {
System.out.print("The Singer finds she is a woman and say");
goodSinger.sayHello(this);
}
@Override
public void visitBadSinger(BadSinger badSinger) {
System.out.print("The Singer finds she is a woman and say");
badSinger.sayBye(this);
}
}
歌手有很多,需要一个集合来管理,当有访客来的时候,集合中的歌手会做出不同的反应
class SingerList{
private final List<Singer> singerList=new LinkedList<>();
void add(Singer singer){
singerList.add(singer);
}
void remove(Singer singer){
singerList.remove(singer);
}
void display(Visitor visitor){
for (Singer singer : singerList) {
singer.accept(visitor);
}
}
}
并且这个设计的模式的精髓在于,不同的歌手接待不同的访问者时,本来会有很多种情况发生,但是在应对这种情况时,只用一个accept方法就代表了所有这些方法,并且在扩展新的访问者时,原有的代码不需要发生变化
但其实缺点也很明显:
依赖了Singer的具体类,这么做是因为不同的歌手里面的调用的方法不一定相同,因为不能依赖抽象(如果调用的都是同一种方法,那就可以,所以其实可以使用依赖抽象,只是这个设计模式没有使用,还有改进的空间)如果歌手的种类发生变化,扩展比较困难
歌手对访问者公布了全部的细节,如果使用不慎,会造成死递归:
public void accept(Visitor visitor) {
//好歌手接待访问者就是访问者访问了好歌手
visitor.visitGoodSinger(this);
}
@Override
public void visitGoodSinger(GoodSinger goodSinger) {
System.out.print("The Singer finds she is a woman and say");
goodSinger.sayHello(this);
}
如果下面那个goodSinger调用了accept方法就会产生递归,将任务相互推诿
总之这不是一个好的设计模式,要求数据结构比较稳定(歌手种类确定后就不会变化)
迭代器模式(为集合提供通用的迭代器)
就是无论是集合还是数组,我们都提供一个通用的迭代器来遍历集合,这样就会方便很多,但是java已经帮我们实现了这些迭代器,实际开发中并不需要我们自己手动去写,增强for循环就是基于迭代器模式,大致思想就是实现下面这个接口,详细的不过多赘述
interface{
boolean hasNext();
Object next();
}
观察者模式(事件发生时,通知所有观察者)
我们需要在消息发生变化时,主动将变化推送给第三方
传统实现
数据展示板,第三方的一种,在更新数据的同时展示最新数据
/**
* @author 李天航
* 天气数据显示板
*/
public class WeatherData {
private float temperature;
private float pressure;
private float humidity;
void update(float temperature,float pressure,float humidity){
this.humidity=humidity;
this.pressure=pressure;
this.temperature=temperature;
display();
}
void display(){
System.out.println("温度:"+temperature);
System.out.println("气压:"+pressure);
System.out.println("湿度:"+humidity);
}
}
数据收集者,当有新数据到来时(调用了update方法),会通知第三方weatherData,调用它的update方法,通知它有数据到来了
public class DataCollector {
private float temperature;
private float pressure;
private float humidity;
private final WeatherData weatherData;
DataCollector(WeatherData weatherData){
this.weatherData=weatherData;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
public float getTemperature() {
return temperature;
}
void update(float temperature,float pressure,float humidity){
this.humidity=humidity;
this.pressure=pressure;
this.temperature=temperature;
weatherData.update(temperature,pressure,humidity);
}
}
客户端测试
public class Client {
public static void main(String[] args) {
WeatherData weatherData=new WeatherData();
DataCollector dataCollector=new DataCollector(weatherData);
dataCollector.update(1,2,3);
dataCollector.update(4,5,6);
}
}
这种方式的缺点是第三方写死在了代码中,当有新的第三方到来时,需要增加DataCollector中通知的第三方个数,不合法开闭原则
改进方法也很容易想到,写一个集合保存所有的第三方,在数据发生变化时遍历集合,逐个通知不就好了
改进后就是我们的观察者模式了,这些第三方我们就叫他观察者
观察者模式
所有的观察者都需要实现这个统一的接口
public interface Observer {
void update(float temperature);
}
这里编写了两个实现类:
public class BoardObserver implements Observer{
@Override
public void update(float temperature) {
System.out.println("控制台得到数据:"+temperature);
}
}
public class WebObserver implements Observer{
@Override
public void update(float temperature) {
System.out.println("web端得到气压数据:"+temperature);
}
}
如果有新的观察者就可以继续添加实现类
搜集数据,如果发生变化就通知所有的观察者(调用他们的update方法)
public class DataSelector {
private float temperature;
private final List<Observer> observers=new LinkedList<>();
void registerObserver(Observer observer){
observers.add(observer);
}
void removeObserver(Observer observer){
observers.remove(observer);
}
public float getTemperature() {
return temperature;
}
public void setTemperature(float temperature) {
this.temperature = temperature;
for (Observer observer : observers) {
observer.update(temperature);
}
}
}
测试:
public class Client {
public static void main(String[] args) {
DataSelector dataSelector=new DataSelector();
dataSelector.registerObserver(new BoardObserver());
dataSelector.registerObserver(new WebObserver());
dataSelector.setTemperature(5);
}
}
如果后面有了新的观察者,只需要实现Observer接口,然后注册进观察者集合中即可
JDK源码中有Observable类也是采用了这个模式
中介者模式(所有的子系统不要直接通信,通过中介来通信)
场景:智能家电
我们想要使用一个功能时,可以自动帮我们完成一系列流程
基本思想
如果子系统之间想要直接通信,就要为不同的子系统之间编写大量的接口,增加和修改都比较困难。而如果我们所有子系统都和一个统一的中介来通信,用中介来调度,就可以省去编写大量接口的麻烦。例如我们的MVC架构,Controller就起到了中介者的作用
中介者中有一个HashMap,存放所有的组件,每个组件在创建时会被自动放进来。在需要组件时,我们可以通过name或者type来获取组件(是不是很像IOC容器的依赖注入),每个组件可以发送指令给中介者,让它来帮我们调用组件完成一系列功能。中介者起到一个组件的管理者的作用,这些消息指令在实际中可以用于服务器和中间件之间的交流,一个客户端发送命令后,通过这个中介者来进行远程过程调用来完成功能。
同时每个组件内部也有一个中介者,我们可以通过中介者来获取我们想要的组件,完成自己的功能(例如IOC容器)。但这里我们还是采用组件发送指令的方式来实现(这样业务功能就集中在中介者中,而不是分布在其他组件里面),然后因为传递指令和名称太不优雅了,并且还有大量的if-else语句,我们基于函数式编程的思想可以对它进行改进
实现
编写组件的抽象类
@AllArgsConstructor
public abstract class Component {
private String name;
private Mediator mediator;
public String getName() {
return name;
}
}
它有三个子类
空调
public class AirConditioner extends Component{
public AirConditioner(String name, Mediator mediator) {
super(name, mediator);
}
void turnOnAirConditioner(){
System.out.println("开空调");
}
void turnOffAirConditioner(){
System.out.println("关空调");
}
}
咖啡机
public class CoffeeMachine extends Component{
public CoffeeMachine(String name, Mediator mediator) {
super(name, mediator);
}
void getCoffee(){
System.out.println("拿一杯咖啡");
}
}
电视
public class Tv extends Component{
public Tv(String name, Mediator mediator) {
super(name, mediator);
}
void turnOnTv(){
System.out.println("打开电视");
}
void turnOffTv(){
System.out.println("关闭电视");
}
}
函数式接口:
@FunctionalInterface
public interface Function {
void doFunction();
}
中介者的抽象类:
public abstract class Mediator {
/**
* 执行任务
* @param functionIndex 任务编号
* @return 是否成功
*/
abstract boolean doFunction(String functionIndex);
/**
* 注册组件
* @param clazz 类名
* @param component 组件
*/
abstract void registerComponent(Class<? extends Component> clazz,Component component);
/**
* 添加指令
* @param functionIndex 指令编号
* @param function 指令
*/
abstract void registerFunction(String functionIndex,Function function);
void sendMessage(){
}
}
中介者的实现类
componentMap用于保存组件,functionMap则为方法列表,客户端和各个组件都可以调用doFunction方法来使用其他组件的功能,也就是通过中介者和其他组件通信,并且可以动态地添加组件,添加方法,灵活性更好,也符合开闭原则
/**
* 具体的中介者
* @author 李天航
*/
public class ConcreteMediator extends Mediator{
Map<Class<? extends Component>,Component> componentMap= new ConcurrentHashMap<>();
Map<String, Function> functionMap=new ConcurrentHashMap<>();
ConcreteMediator(){
functionMap.put("watchMovie",()->{
try {
Tv tv = (Tv) componentMap.get(Tv.class);
CoffeeMachine coffeeMachine = (CoffeeMachine) componentMap.get(CoffeeMachine.class);
AirConditioner airConditioner = (AirConditioner) componentMap.get(AirConditioner.class);
airConditioner.turnOnAirConditioner();
coffeeMachine.getCoffee();
tv.turnOnTv();
}catch (NullPointerException e){
e.printStackTrace();
}
});
functionMap.put("endMovie",()->{
try {
Tv tv = (Tv) componentMap.get(Tv.class);
AirConditioner airConditioner = (AirConditioner) componentMap.get(AirConditioner.class);
tv.turnOffTv();
airConditioner.turnOffAirConditioner();
}catch (NullPointerException e){
e.printStackTrace();
}
});
}
@Override
boolean doFunction(String functionIndex) {
Function function = functionMap.get(functionIndex);
if(function==null){
System.out.println("没有这个方法");
return false;
}else{
function.doFunction();
return true;
}
}
@Override
void registerComponent(Class<? extends Component> clazz, Component component) {
if(componentMap.containsKey(clazz)){
throw new RuntimeException("组件已经存在");
}
componentMap.put(clazz,component);
}
@Override
void registerFunction(String functionIndex, Function function) {
functionMap.put(functionIndex,function);
}
}
客户端调用
public class Client {
@SneakyThrows
public static void main(String[] args) {
Mediator mediator=new ConcreteMediator();
mediator.registerComponent(AirConditioner.class,new AirConditioner("空调", mediator));
mediator.registerComponent(Tv.class,new Tv("电视",mediator));
mediator.registerComponent(CoffeeMachine.class,new CoffeeMachine("咖啡机",mediator));
mediator.doFunction("watchMovie");
System.out.println("看电影");
TimeUnit.SECONDS.sleep(2);
mediator.doFunction("endMovie");
}
}
这种方式相当于整合了许多设计模式,整个系统更加灵活和更具扩展性,想要添加新的功能registerFunction,添加新的组件registerComponent,这也体现出函数式编程的好处,显然也比传统的中介者模式中那一堆if-else要好很多(设计模式都是很早之前提出来的,结合的JDK的新特性可以使设计模式的功能更加强大)
备忘录模式(保存状态)
场景:游戏存档
可以把游戏的状态保存到一个集合中,可以根据编号进行回滚
基本原理
其实很简单,就是在需要存档的时候将对象的状态拷贝一份(可以使用原型模式)加入到一个集合(一般是HashMap)中,然后再需要回档的时候将存档拿出来更新当前的状态。
其实我们之前讲控制器模式的时候已经用到过备忘录模式了(可以撤销多个操作),只不过那里的撤销和存档都是自动完成的
案例实现
游戏状态,使用原型模式进行拷贝
/**
* @author 李天航
* 需要保存的对象
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Originator implements Cloneable{
private float attack;
private float defence;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
档案,除了游戏状态还保存了存档的时间
@Data
public class Memento {
Originator originator;
LocalDateTime localDateTime;
Memento(Originator originator){
try {
this.originator= (Originator) originator.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
localDateTime=LocalDateTime.now();
}
}
档案表
/**
* @author 李天航
* 存档类数据结构
*/
public class CareTaker {
Map<String, Memento> mementoMap=new ConcurrentHashMap<>();
void saveMemento(String mementoId, Originator originator){
mementoMap.put(mementoId,new Memento(originator));
}
Memento getMemento(String mementoId){
return mementoMap.get(mementoId);
}
Originator rollbackTo(Memento memento){
try {
return (Originator) memento.getOriginator().clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
客户端,测试
public class Client {
public static CareTaker careTaker=new CareTaker();
public static void main(String[] args) {
Originator nowPlayer=new Originator(0,0);
nowPlayer.setAttack(5);
nowPlayer.setDefence(6);
System.out.println(nowPlayer);
careTaker.saveMemento("初出茅庐",nowPlayer);
nowPlayer.setDefence(0);
System.out.println(nowPlayer);
nowPlayer= careTaker.rollbackTo(careTaker.getMemento("初出茅庐"));
System.out.println("回滚后:"+nowPlayer);
}
}
使用场景
解释器模式(编译原理)
终结符表达式和非终结符表达式可以构造出我们的文法,根据文法进行语法分析和经过语义制导翻译构造抽象语法树
解释器模式其实就是编译原理所需要的各个组件和环节,算法难度远高于设计模式,所以这里就不给出解释器模式的实现了
状态模式(一个实体有不同的状态,不同的状态有不同的行为)
场景:抽奖游戏
我们有一个抽奖活动,活动之间有不同的状态,不同的状态在做出不同的行为时可以转移到其他状态
UML类图
在Activity中会聚合所有的状态,所有的状态需要实现所有可能出现的行为,这些行为会使Activity转移到新的状态,相当于是用面向对象的思路构造了一个状态转移矩阵,这个矩阵隐式地包含在了对象和方法中,设计得更加巧妙
代码实现
状态的接口,这里采用了适配器模式,为所有的方法添加上默认实现,没有重写这些方法的代表不能进行的操作
public interface State {
default void costCredits(){
System.out.println("不能扣除积分");
}
default boolean raffle(){
System.out.println("不能抽奖");
return false;
}
default void dispensePrize(){
System.out.println("不能发放奖品");
}
}
状态的抽象类,添加一些公共属性
public class AbstractState implements State{
Activity activity;
AbstractState(Activity activity){
this.activity=activity;
}
}
下面是各种状态的实现类,实现类中需要将对应的活动传进来,这样就可以在方法中进行活动状态的转移
public class CannotRaffleState extends AbstractState{
CannotRaffleState(Activity activity) {
super(activity);
}
@Override
public void costCredits() {
System.out.println("扣除积分");
activity.setState(activity.getCanRaffleState());
}
}
public class CanRaffleState extends AbstractState{
CanRaffleState(Activity activity) {
super(activity);
}
@Override
public boolean raffle() {
int result= new Random().nextInt(10);
System.out.println("正在抽奖,请稍等");
if(result==0){
System.out.println("恭喜中奖");
activity.setState(activity.getDispensePrizeState());
return true;
}else{
System.out.println("很遗憾,未中奖");
activity.setState(activity.getCannotRaffleState());
return false;
}
}
}
public class DispensePrizeState extends AbstractState{
DispensePrizeState(Activity activity) {
super(activity);
}
@Override
public void dispensePrize() {
int res=activity.dispensePrize();
if(res==0){
System.out.println("活动结束");
activity.setState(activity.getEndState());
}else {
System.out.println("获得奖品");
activity.setState(activity.getCannotRaffleState());
}
}
}
public class EndState extends AbstractState{
EndState(Activity activity) {
super(activity);
}
}
Activity,表示具体的抽奖活动,里面聚合了所有状态,当活动刚开始时都是不可抽奖的状态
@Data
public class Activity {
private State state;
private int prizeCount;
private State cannotRaffleState =new CannotRaffleState(this);
private State canRaffleState = new CanRaffleState(this);
private State dispensePrizeState=new DispensePrizeState(this);
private State endState =new EndState(this);
Activity(int prizeCount){
this.prizeCount=prizeCount;
state=cannotRaffleState;
}
public int dispensePrize(){
System.out.println("发放奖品");
prizeCount--;
return prizeCount;
}
void costCredits(){
state.costCredits();
}
void raffle(){
if (state.raffle()) {
state.dispensePrize();
}
}
}
客户端测试
public class Client {
public static void main(String[] args) {
Activity activity=new Activity(2);
for (int i = 0; i < 30; i++) {
activity.costCredits();
activity.raffle();
}
}
}
上述实现还有优化的地方,状态一般都是只读的对象,这种对象全局只需要一个,所以我们可以加上static关键字,将其设置为单例的
@Data
public class Activity {
private State state;
private int prizeCount;
private static State cannotRaffleState =new CannotRaffleState();
private static State canRaffleState = new CanRaffleState();
private static State dispensePrizeState=new DispensePrizeState();
private static State endState =new EndState();
public State getCannotRaffleState() {
return cannotRaffleState;
}
public State getCanRaffleState() {
return canRaffleState;
}
public State getDispensePrizeState() {
return dispensePrizeState;
}
public State getEndState() {
return endState;
}
Activity(int prizeCount){
this.prizeCount=prizeCount;
state=cannotRaffleState;
}
public int dispensePrize(){
System.out.println("发放奖品");
prizeCount--;
return prizeCount;
}
void costCredits(){
state.costCredits(this);
}
void raffle(){
if (state.raffle(this)) {
state.dispensePrize(this);
}
}
}
此时@Data对于静态变量会失效,需要我们手动编写get和set
使用场景
产生很多类其实不算是缺点,只要项目做大了类都会很多。当一个实体有多个状态的场景,不同的状态有不同的行为,通过不同的行为可以转移到新的状态,这种场景我们可以考虑使用状态模式。
策略模式(同一个方法有不同的实现)
场景:鸭子
传统思路
编写一个功能的父类,让他的子类去继承他
但是有些类功能和父类相同,有些类功能又不一样需要重写父类的方法,但是重写方法会违反里氏替换原则,可能会导致父类其他功能异常,解决方法为策略模式
具体实现
具体的思路就是将同一种方法中可能会发送变化的部分分离出来,衍生为一个一个的策略,然后在原方法中调用调用策略完成功能
编写鸭子的接口
public interface Duck {
void fly();
void swim();
}
策略的接口
@FunctionalInterface
public interface FlyStrategy {
void fly();
}
策略接口的实现类
public class BadFlyStrategy implements FlyStrategy{
@Override
public void fly() {
System.out.println("飞的不好");
}
}
public class GoodFlyStrategy implements FlyStrategy{
@Override
public void fly() {
System.out.println("飞得好");
}
}
public class NoFlyStrategy implements FlyStrategy{
@Override
public void fly() {
System.out.println("不能飞");
}
}
鸭子:
有的鸭子飞的好,有的鸭子飞得不好,还有的鸭子不会飞,但是总结起来描述的都是飞这个行为,那么我们把飞这个变化的行为分离出来,成为一个飞的策略,在ConcreteDock类中组合这个策略,在原来需要调用飞这个行为时,调用策略中的对应方法即可,至于飞这个行为的具体实现,交给策略的子类来完成
public class ConcreteDock implements Duck{
private FlyStrategy flyStrategy;
public ConcreteDock(FlyStrategy flyStrategy) {
this.flyStrategy = flyStrategy;
}
public void setFlyStrategy(FlyStrategy flyStrategy) {
this.flyStrategy = flyStrategy;
}
@Override
public void fly() {
flyStrategy.fly();
}
@Override
public void swim() {
System.out.println("鸭子会游泳");
}
}
客户端测试:
public class Client {
public static void main(String[] args) {
ConcreteDock duck=new ConcreteDock(new BadFlyStrategy());
duck.fly();
duck.setFlyStrategy(()->{
System.out.println("我想飞");
});
duck.fly();
}
}
我们可以结合JDK8,函数式接口的特性很好的解决这个问题,使用Lamda表达式就可以避免编写大量的策略类(如果是想要重复使用的策略则需要编写为一个类),其实函数式编程就是策略模式的一个具体应用,包括runnable,comparator等函数式接口其实都用到了策略模式
策略模式其实和之前的模板模式很像,都实现了将变化的方法交给子类来完成,模板模式使用的是继承的方式,而策略模式使用的是组合的方式,所以策略模式一般优于模板模式,但是如何策略过多时,类的数目会过于庞大。所以如果变化的方法比较少,可以使用策略模式,如果需要延迟到子类的方法比较多,简历仍然使用模板模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xRcO3pRq-1654221719988)(https://s2.loli.net/2022/06/03/9irm8joFaSsHzXN.png)]
职责链模式(多个流程处理同一个请求)
场景:采购审批
传统的实现方式就是使用if-else语句来实现,但是这样如果金额发送了变化,或者增加了审批环节,客户端的代码就要发生变化,难以维护
UML类图
所谓职责链模式就是创建一个链表,挨个执行每个对象的方法,如果不能处理就交给下一个结点(SpringMVC的拦截器链),当然,我们可以对链表进行排序来控制它的执行顺序
但其实我们还是直接使用集合类型就行了……,并不需要我们手动构造一个链表
代码实现
编写接口,作为设计规范
public interface Handler {
boolean canHandle(Price price);
void process(Price price);
void setNextHandler(Handler nextHandler);
Handler getNextHandler();
}
编写抽象类,实现公共的逻辑
public abstract class AbstractHandler implements Handler{
Handler nextHandler;
String name;
public AbstractHandler(String name) {
this.name = name;
}
@Override
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
@Override
public Handler getNextHandler() {
return nextHandler;
}
}
编写子类,实现里面的处理方法
public class EmployeeHandler extends AbstractHandler{
public EmployeeHandler(String name) {
super(name);
}
@Override
public boolean canHandle(Price price) {
return price.getPrice()<1000;
}
@Override
public void process(Price price) {
if(canHandle(price)){
System.out.println("员工可以处理");
}else if(nextHandler!=null){
nextHandler.process(price);
}else{
System.out.println("无法处理");
}
}
}
public class LeaderHandler extends AbstractHandler{
public LeaderHandler(String name) {
super(name);
}
@Override
public boolean canHandle(Price price) {
return price.getPrice() >= 1000;
}
@Override
public void process(Price price) {
if(canHandle(price)){
System.out.println("领导可以处理");
}else if(nextHandler!=null){
nextHandler.process(price);
}else{
System.out.println("无法处理");
}
}
}
编写请求类,维护链表这个数据结构,但其实链表完全可以换成一个List
public class Request {
Handler head,last;
void addHandler(Handler handler){
if(head==null){
head=last=handler;
}else{
last.setNextHandler(handler);
last=last.getNextHandler();
}
}
void process(Price price){
head.process(price);
}
}
编写客户端测试
public class Client {
public static void main(String[] args) {
Request request=new Request();
request.addHandler(new EmployeeHandler("lth"));
request.addHandler(new LeaderHandler("leader lth"));
request.process(new Price(5000));
}
}
注意事项
总结
设计模式讲述的其实是一种思想,并不是按照上述的模板写才叫用到了设计模式,上述的案例实现只是各个设计模式思想的一种实现,并且很多都实现得不太好,可以结合其他设计模式以及语言的一些新特性进行进一步改进。上述的23种设计模式一般都需要相配合着使用,具体情况下还需要进行适当的改写,写代码的时候也不要太转牛角尖,耦合度降低了,同时也就以为代码的适用性变窄了,若设计模式使用不当,可能可以使得代码耦合度降低,但是却不能很好地完成我们想要的需求,可能会导致整个系统的重构,也就本末倒置了。平时多写代码,提升自己的代码量,这样即使不刻意去记忆这些设计模式,也会不自觉地用到。