设计模式六大原则
开闭原则:对扩展开放,对修改关闭
单一职责原则:一个类只负责一个功能领域中的相应职责
里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象
依赖倒置原则:依赖于抽象,不能依赖于具体实现,
接口隔离原则:类之间的依赖关系应该建立在最小的接口上
合成/聚合 复用原则:尽量使用 合成/聚合,而不是通过 继承 达到复用的目的
迪米特法则(最少知识原则):一个软件实体应当尽可能少的与其他实体发生相互作用
设计模式的分类
创建型:在创建对象的同时隐藏创建逻辑,不使用new直接实例化对象,程序在判断需要创建哪些对象时更灵活。
包括工厂/抽象工厂/单例/建造者/原型模式。
结构型:通过类和接口间的继承和弓|用实现创建复杂结构的对象。
包括适配器/桥接模式/过滤器/组合/装饰器/外观/享元/代理模式。
行为型:通过类之间不同通信方式实现不同行为。
包括责任链/命名/解释器/迭代器/中介者/备忘录/观察者/状态/策略/模板/访问者模式。
一、工厂模式
实例化对象不是用new,用工厂方法替代。将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。
Spring使用工厂模式通过BeanFactory、ApplicationContext 创建bean对象。
1.简单工厂模式
简单工厂模式指由一个工厂对象来创建实例,客户端不需要关注创建逻辑,只需提供传入工厂的参数。
在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例。
适用于:工厂类负责创建对象较少的情况。
缺点是:如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则,且产品多的话会使工厂类比较复杂。
如何解决缺点?我们可以定义一个创建对象的抽象方法并创建多个不同的工厂类实现该抽象方法,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。这种方法也就是我们接下来要说的工厂方法模式。
例子:
接下来创建一个接口,两个实现类,一个工厂,一个测试类
//创建手机接口
public interface Phone {void name();
}
//创建华为实现类
public class HuaWei implements Phone{@Overridepublic void name() {System.out.println("华为手机");}
}
//创建小米实现类
public class XiaoMi implements Phone{@Overridepublic void name() {System.out.println("小米手机");}
}
//创建工厂
public class PhoneFactory {public static Phone getPhone(String phone){if(phone.equals("华为")){return new HuaWei();}else if(phone.equals("小米")){return new XiaoMi();}else {return null;}}
}
//测试类
public class Consumer {public static void main(String[] args) {Phone p1= PhoneFactory.getPhone("华为");Phone p2= PhoneFactory.getPhone("小米");p1.name();p2.name();}
}
// 输出 :
//华为手机
//小米手机
我们通过创建一个PhoneFactory类,成功的完成工厂的创建。我们在创建对象时,也就不需要直接创建对象,而是可以通过创建工厂,这样大大的降低了代码的耦合性。
但是,静态工厂模式是不能添加数据的。比如说,我们想添加一个“Oppo”手机类,你不直接修改PhoneFactory工厂代码,是不能实现的。所以,就有了第二种的工厂方法模式。
2.工厂方法模式
**定义:**定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。
**工厂方法模式:**用来生产同一等级架构中的固定产品,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。(支持增加任意产品)
简单工厂模式中: 工厂是个类,类里面用 if-else 语句来区分 小米 还是 华为
工厂方法模式中:工厂是个接口,然后华为工厂继承这个接口,小米工厂继承这个接口。
//创建手机接口
public interface Phone {void name();
}
//创建华为实现类
public class HuaWei implements Phone{@Overridepublic void name() {System.out.println("华为手机");}
}
//创建手机工厂接口
public interface PhoneFactory {Phone getPhone();
}
//创建华为工厂
public class HuaWeiFactory implements PhoneFactory{@Overridepublic Phone getPhone() {return new HuaWei();}
}
//测试类
public class Consumer {public static void main(String[] args) {Phone phone = new HuaWeiFactory().getPhone();phone.name();}
}
我们创建了手机工厂接口PhoneFactory,再创建华为工厂HuaWeiFactory实现工厂,这样就可以通过HuaWeiFactory创建对象。增加新的具体工厂和产品族很方便,比如说,我们想要增加小米,只需要创建一个小米工厂XiaoMiFactory实现手机工厂接口PhoneFactory,合理的解决的简单工厂模式不能修改代码的缺点。
工厂方法存在的问题:客户端需要创建类的具体的实例。简单来说就是用户要订华为工厂的手机,他必须去华为工厂,想订小米工厂的披萨,必须去小米工厂。 当华为工厂和小米工厂发生变化了,用户也要跟着变化,这无疑就增加了用户的操作复杂性。
解决:为了解决这一问题,我们可以把工厂类抽象为接口,用户只需要去找默认的工厂提出自己的需求(传入参数),便能得到自己想要产品,而不用根据产品去寻找不同的工厂,方便用户操作。这也就是我们接下来要说的抽象工厂模式。
3.抽象工厂模式
//手机接口
public interface Phone {void send();void call();
}
//创建华为手机对象
public class HuaWeiPhone implements Phone{@Overridepublic void send() {System.out.println("HuaWei's send");}@Overridepublic void call() {System.out.println("HuaWei's call");}
}
//抽象工厂
public interface IProductFactory {//生产手机Phone phone();
}
//创建华为工厂
public class HuaWeiFactory implements IProductFactory{@Overridepublic Phone phone() {return new HuaWeiPhone();}}
//测试类
public class Consumer {public static void main(String[] args) {HuaWeiFactory huaWeiFactory = new HuaWeiFactory();Phone phone = huaWeiFactory.phone();phone.call();phone.send();Computer computer = huaWeiFactory.computer();computer.play();computer.watch();}
}
抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口,无需指定它们具体的类(围绕一个超级工厂创建其他工厂,该超级工厂称为工厂的工厂)
工厂的工厂:一个大工厂,里面有具体的不同产品的小工厂,每个生成的工厂都能按照简单工厂模式提供对象,他们自己决定生产哪一些产品
二、单例模式
Spring中的Bean默认都是单例的。
概念
单例模式指的是:在内存中只会创建且仅创建一次对象 的设计模式
比如Student a = new Student(); 就是Student类只创建这一个实例,只能有这一个对象存在
主要解决:一个全局使用的类频繁地创建与销毁。在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)
何时使用:当您想控制实例数目,节省系统资源的时候。
关键代码:构造函数是私有的。
1、为了保证只有一个对象,不能new对象,所以设置构造方法私有。
2、只能通过方法或者属性获取对象,如果通过属性获取,这个属性是可以修改的,所以属性只能
是私有的。所以只能通过方法获取。
3、由于我们不能new对象,所以获取对象的方法定是静态的。属性也得是静态的,因为不是静态的,静态的方法访问不到
**缺点:**没有接口,不能继承,与单一 职责原则冲突,一 个类应该只关心内部逻辑, 而不关心外面怎么样来实例化。
使用场景:
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
**注意事项:**getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
**枚举:**对象固定,私有构造器
1.饿汉式
类加载后一开始就会创建对象,
优点:线程安全,没有加锁,执行效率高
缺点:类加载的时候就初始化,浪费内存空间
public class PreloadSingleton {//初始化public final static PreloadSingleton instance = new PreloadSingleton();//私有化构造方法:其他的类无法实例化单例类的对象private PreloadSingleton() {};//对外提供⼀个公共的⽅法获取实例public static PreloadSingleton getInstance() {return instance;}
}
2.懒汉式
需要的时候才会去创建对象
懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化**(判空),**若已实例化直接返回该类对象。,否则则先执行实例化操作。
好处:节省内存
坏处:用的时候才创建稍微有点慢,线程不安全
public class Singleton {private static Singleton singleton;private Singleton(){}//对象创建写到方法中,当方法被调用的时候才会创建对象public static Singleton getInstance() {if (singleton == null) {singleton = new Singleton();}return singleton;}}
线程不安全,当创建了100个线程,调用getInstance方法时,可能会有多个线程同时看到没有new,就会执行多次new,调用多次构造方法(也就是会进行多次初始化)
3.懒汉式-双重校验锁
为了解决 懒汉式线程不安全的问题
最容易想到的解决方法就是在方法上加锁,或者是对类对象加锁,程序就会变成下面这个样子
//对方法加锁
public static synchronized Singleton getInstance() {if (singleton == null) {singleton = new Singleton();}return singleton;
}
// 或者 对类对象加锁
public static Singleton getInstance() {synchronized(Singleton.class) { if (singleton == null) {singleton = new Singleton();}}return singleton;
}
这样就规避了两个线程同时创建Singleton对象的风险,
但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。
接下来要做的就是**优化性能,目标是:**如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例
所以直接在方法上加锁的方式就被废掉了,因为这种方式无论如何都需要先获取锁。
所以有了下面这种双重校验锁的方式
双重校验锁
public class Singleton {//加volatile 防止指令重排序private static volatile Singleton singleton;private Singleton(){}public static Singleton getInstance() {if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singletonsynchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支singleton = new Singleton();}}}return singleton;}}
上面的代码已经完美地解决了并发安全+性能低效问题:
第1个if,如果singleton不为空,则直接返回对象,不需要获取锁;而如果多个线程发现singleton为空,则进入分支;
第3行代码,多个线程尝试争抢同一个锁,只有一个线程争抢成功,第一个获取到锁的线程会再次判断singleton是否为空,因为singleton有可能已经被之前的线程实例化
其它之后获取到锁的线程在执行到第4行校验代码,发现singleton已经不为空了,则不会再new一个对象,直接返回对象即可
之后所有进入该方法的线程都不会去获取锁,在第一次判断singleton对象时已经不为空了
为什么两个if判断null
场景:有可能多个线程同时进入了第一个if,都读到对象null了,如果第一个线程加上锁创建了对象之后,释放锁之后,如果不进行再次判断null的话,就会再次进行创建对象(以第一个判断null为准),两次就是为了防止多次创建对象
第一个if:对象为null的时候才进入下面if进行创建对象,如果不为null就return
第二个if:判断在第一个为null的多线程情况下阻止多创建对象的。
使用volatile防止指令重排
创建一个对象,在JVM中会经过三步:
(1)为singleton分配内存空间
(2)初始化singleton对象
(3)将singleton指向分配好的内存空间
指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能
反射破坏单例
无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。
演示利用反射破坏单例模式 :利用反射,强制访问类的私有构造器,去创建另一个对象
public static void main(String[] args) {// 获取类的显式构造器Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();// 可访问私有构造器construct.setAccessible(true); // 利用反射构造新对象Singleton obj1 = construct.newInstance(); // 通过正常方式获取单例对象Singleton obj2 = Singleton.getInstance(); System.out.println(obj1 == obj2); // false
}
序列化和反序列化破坏单例
public static void main(String[] args) {// 创建输出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));// 将单例对象写到文件中oos.writeObject(Singleton.getInstance());// 从文件中读取单例对象File file = new File("Singleton.file");ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));Singleton newInstance = (Singleton) ois.readObject();// 判断是否是同一个对象System.out.println(newInstance == Singleton.getInstance()); // false
}
两个对象地址不相等的原因是:readObject() 方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址。
三、观察者模式
定义对象间的一种一对多的关系,当一个对象状态发生改变时,所有依赖这个对象的都会得到通知,自动更新,比如微信的订阅,当作者发文则通知订阅者。
观察者模式: Spring事件驱动模型就是观察者模式很经典的一-个应用。
观察者模式是一种常见的设计模式,用于定义对象之间的一对多依赖关系,使得当一个对象的状态发生变化时,其所有依赖对象都会得到通知并自动更新。观察者模式是一种行为型设计模式,它将对象的行为和状态分离,以实现松耦合。
以下是观察者模式的关键元素和工作原理:
- 主题(Subject):主题是被观察的对象,它包含了一组观察者对象的引用,以及用于注册、删除和通知观察者的方法。主题通常具有状态,当状态发生变化时,它会通知所有观察者。
- 观察者(Observer):观察者是依赖于主题的对象,它定义了一个更新接口,以便主题在状态发生变化时能够通知观察者。观察者根据主题的通知来执行相应的操作。
- 具体主题(Concrete Subject):具体主题是主题的具体实现,它维护了一个状态,并在状态变化时通知观察者。具体主题可以有多个具体观察者。
- 具体观察者(Concrete Observer):具体观察者是观察者的具体实现,它实现了观察者接口的更新方法,以响应主题的通知。
观察者模式的工作原理如下:
- 当主题的状态发生变化时,它会迭代通知所有注册的观察者。
- 每个观察者接收到通知后,执行自己的更新操作,通常是根据主题的状态变化来更新自身的状态或执行特定的行为。
优缺点、适用场景
适用场景
- 关联行为场景。
- 事件多级触发场景。
- 跨系统的消息交换场景,如消息队列、事件总线的处理机制。
优点
- 低耦合:主题和观察者之间的关系是松散的,主题不需要知道观察者的具体实现,只需要知道它们实现了观察者接口。
- 可扩展性:可以轻松地添加新的观察者或主题,而不会影响现有的代码。
- 可重用性:观察者可以在不同的主题中重复使用。
- 通知机制:观察者模式提供了一种通知机制,使得对象之间的通信更加灵活和可靠。
缺点
在应用观察者模式时需要考虑一下开发效率和运行效率的问题,程序中包括一个被观察者、多个观察者,开发、调试等内容会比较复杂,而且在Java中消息的通知一般是顺序执行,那么一个观察者卡顿,会影响整体的执行效率,在这种情况下,一般会采用异步实现。
代码举例
观察者模式这种发布-订阅的形式我们可以拿微信公众号来举例,假设微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号,当这个公众号更新时就会通知这些订阅的微信用户。
抽象观察者(Observer)
里面定义了一个更新的方法:
public interface Observer {public void update(String message);
}
具体观察者(ConcrereObserver)
微信用户是观察者,里面实现了更新的方法:
public class WeixinUser implements Observer {// 微信用户名private String name;public WeixinUser(String name) {this.name = name;}@Overridepublic void update(String message) {System.out.println(name + "-" + message);}}
抽象被观察者(Subject)
抽象主题,提供了增加订阅者、删除订阅者、通知订阅者更新消息三个方法:
public interface Subject {/*** 增加订阅者* @param observer*/public void attach(Observer observer);/*** 删除订阅者* @param observer*/public void detach(Observer observer);/*** 通知订阅者更新消息*/public void notify(String message);
}
具体被观察者(ConcreteSubject)
微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法:
是一个类继承了那个接口
public class SubscriptionSubject implements Subject {//储存订阅公众号的微信用户private List<Observer> weixinUserlist = new ArrayList<Observer>();@Overridepublic void attach(Observer observer) {weixinUserlist.add(observer);}@Overridepublic void detach(Observer observer) {weixinUserlist.remove(observer);}@Overridepublic void notify(String message) {for (Observer observer : weixinUserlist) {observer.update(message);}}
}
客户端调用
public class Client {public static void main(String[] args) {SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();//创建微信用户WeixinUser user1=new WeixinUser("杨影枫");WeixinUser user2=new WeixinUser("月眉儿");WeixinUser user3=new WeixinUser("紫轩");//订阅公众号mSubscriptionSubject.attach(user1);mSubscriptionSubject.attach(user2);mSubscriptionSubject.attach(user3);//公众号更新发出消息给订阅的微信用户mSubscriptionSubject.notify("博主xxx的专栏更新了");}
}
输出:
杨影枫-博主xxx的专栏更新了
月眉儿-博主xxx的专栏更新了
紫轩-博主xxx的专栏更新了
四、代理模式
当我们访问国外网站的时候,往往需要VPN, 他可以帮助我们去访问一些国内不能访问的网站,**也就是说他代理了这个访问过程,把结果返回给了我们。**这就是代理模式。
代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。
代理设计模式**:SpringAOP功能的实现。**动态代理
优缺点、适用场景
优点
代理模式是最常用的设计模式之一,因为他包含以下优点:
- 可以使真实业务角色责任更纯粹,不用包含一些公共业务。
- 公共业务交给了代理类,实现了解耦。
- 提供了面向切面编程的基础,使一个横向业务更容易编写
- 动态代理可以代理多个实现了同一个接口的类。
缺点
增加代理类可能会导致业务处理速度变慢。
适用场景
以下场景适合适用代理模式
- 当业务无法直接访问某个类时,需要一个代理类去代理访问。
- 当需要横向添加一些功能,比如日志功能时,可以使用代理模式。
五、建造者模式
它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成.
建造者模式包括的角色:
(1)Builder:给出一个抽象接口或抽象类,以规范产品的建造。这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建,一般由子类具体实现。
(2)ConcreteBuilder:Builder接口的实现类,并返回组建好对象实例。
(3**)Director(指挥者):调用具体建造者来创建复杂对象的各个部分**,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
(4)Product:要创建的复杂对象,产品类。
建造者模式的使用场景:
(1)当产品有复杂的内部构造时(参数很多)。
(2)需要生产的产品的属性相互依赖,这些属性的赋值顺序比较重要时(因为在调用ConcreteBuilder的赋值方法时是有先后顺序的)。
建造者模式优缺点
建造者模式的优点:
(1)建造模式是将复杂的内部创建封装在内部,对于外部调用的人来说,只需要传入建造者和建造工具,对于内部是如何建造成成品的,调用者无需关心,良好的封装性是建造者模式的优点之一。
(2)建造者类逻辑独立,易拓·1`展。
建造者模式的缺点:
很明显产生了多余的Build对象以及Dirextor对象,消耗了内存。
要组装一台电脑(Computer类),我们假设它有三个部件:CPU 、主板以及内存。在Computer类中提供三个set方法分别设置这三个属性。
public class Computer {private String mCpu;private String mMainboard;private String mRam;public void setmCpu(String mCpu) {this.mCpu = mCpu;}public void setmMainboard(String mMainboard) {this.mMainboard = mMainboard;}public void setmRam(String mRam) {this.mRam = mRam;}
}
Builder类
里面提供了安装CPU、主板和内存的抽象方法,以及组装成电脑的create方法
public abstract class Builder {public abstract void buildCpu(String cpu);public abstract void buildMainboard(String mainboard);public abstract void buildRam(String ram);public abstract Computer create();
}
Builder实现类
里面不仅新建了Computer的实例,还提供了安装CPU、主板和内存的具体实现方法,并且在组装成电脑的create方法中将该Computer对象实例返回
public class MyComputerBuilder extends Builder {private Computer mComputer = new Computer();@Overridepublic void buildCpu(String cpu) {mComputer.setmCpu(cpu);}@Overridepublic void buildMainboard(String mainboard) {mComputer.setmMainboard(mainboard);}@Overridepublic void buildRam(String ram) {mComputer.setmRam(ram);}@Overridepublic Computer create() {return mComputer;}
}
指挥者(Director)类用来规范组装电脑的流程顺序,先安装主板,再安装CPU,最后安装内存并组装成电脑。
public class Direcror {Builder mBuild=null;public Direcror(Builder build){this.mBuild=build;}public Computer CreateComputer(String cpu,String mainboard,String ram){//规范建造流程,这个顺序是由它定的this.mBuild.buildMainboard(mainboard);this.mBuild.buildCpu(cpu);this.mBuild.buildRam(ram);return mBuild.create();}
}
Builder mBuilder = new MyComputerBuilder();
Direcror mDirecror=new Direcror(mBuilder);
mDirecror.CreateComputer("i7","Intel主板","mRam");//返回Computer实例对象