设计模式概述
设计模式:是一套用来提高代码可复用性,可维护性、可读性、稳健型以及安全性的解决方案
设计模式的本质:是面向对象
设计原则的实际运用,是对类的封装、继承、多态以及类的关联关系和组合关系的充分理解。
设计模式的的基本要素:模式名称、问题、解决方案、效果
使用设计模式,有以下优点:
- 提高程序员的思维能力、编程能力和设计能力
- 使程序设计更加标准化、代码编制更加工程化,提高开发效率
- 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强
设计模式分为三大类:
模式分类 | 作用 | 具体模式 |
---|---|---|
创建型模式 | 描述怎么创建一个对象 创建和使用分离 | 单例模式、工厂模式、抽象工厂模式、 建造者模式、原型模式 |
结构型模式 | 描述如何将类和对象按照某种类型, 组成更大的结构 | 适配器模式、桥接模式、装饰模式、 组合模式、外观模式、享元模式、代理模式 |
行为型模式 | 描述类和对象如何相互协作, 完成单个类无法完成的任务 | 模板方法模式、命令模式、迭代器模式、观察者模式、 中介者模式、备忘录模式、解释器模式、状态模式、 策略模式、职责链模式、访问者模式 |
OOP七大原则
开闭原则:对扩展开发,对修改关闭(当需求需要改变的时候,尽量去扩展)
里氏替换原则: 继承必须确保超类所拥有的性质在子类中仍然成立(尽量不重写父类的方法)
依赖倒置原则: 要面向接口编程,不要面向实现编程
单一职责原则: 控制类的粒度大小,将对象解耦,提高其内聚性(一个对象不应该担任太多的职责,原子性,单一的方法做单一的事情)
接口隔离原则: 要为各个类建立他们需要的专用接口
迪米特法则: 只与你的直接朋友交谈,不跟“陌生人”说话,降低代码之间的耦合度
合成复用原则: 尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现
单例模式(single)
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
这样的模式有几个好处:
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。节约系统资源
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。
缺点:由于单例模式中没有抽象层,因此单例类的扩展
有很大的困难。在一定程度是违背了“开闭原则”。
在以下情况下可以使用单例模式:
• 系统只需要一个实例对象
,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
• 客户调用类的单个实例只允许使用一个公共访问点
,除了该公共访问点,不能通过其他途径访问该实例。
• 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。(要学会变通)
饿汉式单例
//饿汉式单例
//缺点:类加载期间就创建对象,容易造成资源浪费
public class Hungry{
//可能会浪费空间
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
//构造方法私有
private Hungry(){
}
private final static Hungry HUNGRY = new Hungry()l;
public static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式单例
//懒汉式单例
//单线程下能实现单例,多线程不行
public class LazyMan{
private LazyMan(){
}
private static LazyMan LAZYMAN;
public static LazyMan getInstance(){
if(LAZYMAN == null){
LAZYMAN = new LazyMan();
}
return LAZYMAN;
}
}
DCL懒汉式解决并发问题
注意:synchronized 解决并发问题,但是因为lazyMan = new LazyMan();不是原子性操作(可以分割,见代码注释),可能发生指令重排序的问题,通过volatil
来解决
- Java 语言提供了 volatile和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
- 原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1;
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"OK");
}
private volatile static LazyMan lazyMan;
//双重检测锁模式的 懒汉式单例 DCL懒汉式
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class){//synchronized加锁解决多线程下的问题
if(lazyMan == null){
lazyMan = new LazyMan();//不是一个原子性操作
}
}
}
return lazyMan;
}
/*
* 1.分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向者个空间
*
* 123
* 132 A
*
* B //此时lazyMan还没有完成构造
*
* */
//多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
/**
* 静态内部类,实现单例
*/
public class SingleHolder {
private SingleHolder() {}
public static SingleHolder getInstance(){
return InnerClass.singleHolder;
}
//静态内部类:注意class关键字大小写
public static class InnerClass{
public static final SingleHolder singleHolder = new SingleHolder();
}
}
反射破坏单例
/**
* 通过反射破坏 DCL,和通过构造器加锁防止破坏
*/
public class SingleLazy {
//构造器私有化
private SingleLazy() {
/* ------2.预防------- 防止反射破坏 DCL 模式 -----------
在构造器中加锁,确保同一时间构造器只能执行一次
如果对象已经创建,则抛出异常
*/
synchronized (SingleLazy.class){
if(singleLazy != null){
throw new RuntimeException("不要用反射,破坏单例模式");
}
}
}
//关键字 volatile 避免指令重排
private volatile static SingleLazy singleLazy;
//DCL 双重检测锁
public static SingleLazy getInstance(){
if(singleLazy == null){ //第一重检测
synchronized (SingleLazy.class){ //锁住当前对象
if(singleLazy == null){ //第一重检测
singleLazy = new SingleLazy(); //这不是一个原子性操作
}
}
}
return singleLazy;
}
// ------1.破坏------- 通过反射,破解 DCL 单例 --------------
public static void main(String[] args) throws Exception {
SingleLazy singleLazy = SingleLazy.getInstance();
// 反射获取SingleLazy的Class对象
Class lazyClass = singleLazy.getClass();
// 获取构造方法(getDeclaredConstructor, 获取所有构造方法,包含私有)
Constructor<SingleLazy> constructor = lazyClass.getDeclaredConstructor(null);
// 设置构造地可见性,并创建对象
constructor.setAccessible(true);
SingleLazy singleLazy2 = constructor.newInstance();
System.out.println(singleLazy);
System.out.println(singleLazy2);
}
}
/**
* 1.不调用getInstance方法,全部使用反射创建对象,破坏DCL
* 2.使用红绿灯法,阻止这种破坏
* 3.通过反射找到这个红绿灯标志,依然可破坏 DCL
*/
public class SingleLazy {
private static boolean flag = false;
//构造器私有化
private SingleLazy() {
/* ------4.预防------- 防止反射破坏 DCL 模式 -----------
采用红绿灯法,避免全部使用反射破坏 DCL
*/
synchronized (SingleLazy.class){
if(!flag){
flag = true; //将flag作为红绿灯,也可以用UUID或者别的
}else{
throw new RuntimeException("不要用反射,破坏单例模式");
}
}
}
//关键字 volatile 避免指令重排
private volatile static SingleLazy singleLazy;
//DCL 双重检测锁
public static SingleLazy getInstance(){
if(singleLazy == null){ //第一重检测
synchronized (SingleLazy.class){ //锁住当前对象
if(singleLazy == null){ //第一重检测
singleLazy = new SingleLazy(); //这不是一个原子性操作
}
}
}
return singleLazy;
}
/* ------3.破坏------- 通过反射,破解 DCL 单例 --------------
如上面所示,构造方法中锁住当前对象。
但是如果不调用getInstance,全部通过反射创建对象,依然可以破坏单例
*/
public static void main(String[] args) throws Exception {
//不调用.getInstance();
//SingleLazy singleLazy = SingleLazy.getInstance();
//通过类路径获取Class对象(不能通过对象获取,那时候已经初始化)
Class lazyClass = SingleLazy.class;
Constructor<SingleLazy> constructor = lazyClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
//全部用反射创建对象
SingleLazy singleLazy = constructor.newInstance();
// -------- 5. 如果找到隐藏的标志,也可用反射破坏 -------
Field flagField = lazyClass.getDeclaredField("flag");
flagField.set(singleLazy,false);
// -------- 5.成功创建第二个对象 ---------
SingleLazy singleLazy2 = constructor.newInstance();
System.out.println(singleLazy);
System.out.println(singleLazy2);
}
}
枚举
/**
* 通过枚举实现单例:
* enum本身也是一个Class类,枚举默认就是单例
*/
public enum SingleEnum {
SINGLE_ENUM;
public SingleEnum getInstance(){
return SINGLE_ENUM;
}
public static void main(String[] args) {
SingleEnum singleEnum1 = SingleEnum.SINGLE_ENUM;
SingleEnum singleEnum2 = SingleEnum.SINGLE_ENUM;
System.out.println(singleEnum1.hashCode());
System.out.println(singleEnum2.hashCode());
}
}
所以为了单例安全,我们可以考虑使用枚举,但是需要注意的是枚举中没有无参构造,而是有两个参数(String,int)
序列化和反序列化也能破坏单例
首先我们要知道什么是序列化和反序列化:
序列化:把对象转换为字节序列的过程称为对象的序列化
反序列化:把字节序列恢复为对象的过程称为对象的反序列化
可以添加方法解决这个问题:
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return getInstance();
}
工厂模式(factory)
作用 | 实现创建者与调用者的分离 |
---|---|
详细分类 | 简单工厂模式 、工厂方法模式 、抽象工厂模式 |
涉及原则 | 开闭原则 、 依赖倒转原则 、迪米特法则 |
核心本质
- 实例化对象不适用new,用工厂方法代替
- 将选择实现类、创建对象统一管理和控制。从而将调用者与实现类解耦
三种模式:
- 简单工厂模式:用来生产同一等级结构中的任意产品(对于增加新的产品,需要扩展已有代码)
- 工厂方法模式:用来生产同一等级结构中的固定产品(支持增加任意产品)
- 抽象工厂模式:围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。
分类 | 内容 |
---|---|
简单工厂模式 (静态工厂模式) | 虽然某种程度上不符合设计原则,但实际使用最多 |
工厂方法模式 | 满足开闭原则,不修改已有类的前提下,通过增加新的工厂实现扩展 |
抽象工厂模式 | 不可以增加新产品,可以增加产品族 |
应用场景 | 1.JDK的Calendar中getInstance方法 2.JDBC中Connection对象的获取 3.Spring中IOC容器创建管理bean对象 4.反射中Class对象的newInstance方法 |
简单工厂模式
// ---------- 接口 ----------
public interface Car {
public void name();
}
// ---------- 两个实现类: ----------
public class Tesla implements Car {
@Override
public void name() {
System.out.println("特斯拉");
}
}
public class Wuling implements Car {
@Override
public void name() {
System.out.println("五菱宏光");
}
}
/* ---------- 简单工厂模式,又称静态工厂模式 ----------
缺点:不满足开闭原则。如果新增一个车型(大众),则车工厂需要改代码
*/
public class CarFactory {
//简单工厂方法一:
public static Car getCar(String carName){
if(carName.equals("五菱")){
return new Wuling();
}else if(carName.equals("特斯拉")){
return new Tesla();
}else{
return null;
}
}
//简单工厂方法二:
public static Car getWuling(){
return new Wuling();
}
public static Car getTesla(){
return new Tesla();
}
}
// ---------- 消费者 ----------
public class Consumer {
public static void main(String[] args) {
Car wuling = CarFactory.getCar("五菱");
Car tesla = CarFactory.getCar("特斯拉");
wuling.name(); //输出:五菱宏光
tesla.name(); //输出:特斯拉
}
}
弊端:增加一个新的产品,做不到不修改代码。
违背了开闭原则
违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中
工厂方法模式
// ---------- 接口 ----------
public interface Car {
public void name();
}
// ---------- 两个实现类: ----------
public class Tesla implements Car {
@Override
public void name() {
System.out.println("特斯拉");
}
}
public class Wuling implements Car {
@Override
public void name() {
System.out.println("五菱宏光");
}
}
/* ---------- 方法工厂模式:每个实现类,分别创建一个工厂 ----------
优点:满足开闭原则,新增一个车型,不用修改原代码
缺点:代码量明显增加
*/
public interface CarFactory {
public Car getCar();
}
//特斯拉工厂,只生产特斯拉
public class TeslaFactory implements CarFactory {
@Override
public Car getCar() {
return new Tesla();
}
}
//五菱工厂,只生产五菱
public class WulingFactory implements CarFactory {
@Override
public Car getCar() {
return new Wuling();
}
}
// ---------- 消费者:通过特定的工厂,生产特定的车 ----------
public class Consumer {
public static void main(String[] args) {
Car wuling = new WulingFactory().getCar();
Car tesla = new TeslaFactory().getCar();
wuling.name(); //输出:五菱宏光
tesla.name(); //输出:特斯拉
}
}
对比简单工厂模式
1、结构复杂度:simple>method
2、代码复杂度:simple>method
3、编程复杂度:simple>method
4、管理上的复杂度:simple>method
根据设计原则,使用工厂方法模式;根据实际业务,使用简单工厂模式
抽象工厂模式
定义 | 抽象工厂模式,提供了一个创建一系列相关或者相互依赖对象的接口,无需指定他们具体的类 |
---|---|
应用场景 | 1.客户端不依赖于实例如何被创建、实现等细节 2.强调一系列相关的产品对象(属于同一产品族)一起使用,创建对象需要大量的重复代码 3.提供一个产品类的库,所有的产品以同样的接口出现,使客户端不依赖于具体实现 |
优点 | 1.具体产品在应用层的代码隔离,无需关心创建细节 2.将一个系列的产品,统一到一起创建 |
缺点 | 1.规定了所有可能被创建的产品集合,产品族中亏站新的产品困难 2.增加了系统的抽象性和理解难度 |
/**
* 接口中修饰符:
* Java 7 方法只能为抽象方法,默认为 public abstract
* Java 8 增加:默认方法default;静态方法static(默认方法不强制重写,静态方法不会被实现类继承)
* Java 9 增加:私有方法
*
* 接口中不可以定义变量即只能定义常量,属性默认是public static final,且必须赋值
*/
// 1.------------ 超级工厂,定义产品族 ------------
public interface IProductFactory {
//生产手机
IPhoneProduct phoneProduct();
//生产路由器
IRouterProduct routerProduct();
}
// 2.------------ 产品接口,定义产品功能 ------------
//手机产品接口
public interface IPhoneProduct {
void open(); //开机
void callup(); //打电话
}
//路由器产品接口
public interface IRouterProduct {
void open(); //开机
void openWifi(); //打开WiFi
}
// 3.------------ 具体产品,实现产品接口 ------------
//小米手机
public class XiaomiPhone implements IPhoneProduct {
@Override
public void open() {
System.out.println("小米手机:开机");
}
@Override
public void callup() {
System.out.println("小米手机:打电话");
}
}
//小米路由器
public class XiaomiRouter implements IRouterProduct {
@Override
public void open() {
System.out.println("小米路由器:开机");
}
@Override
public void openWifi() {
System.out.println("小米路由器:打开WiFi");
}
}
//华为手机
public class HuaweiPhone implements IPhoneProduct {
@Override
public void open() {
System.out.println("华为手机:开机");
}
@Override
public void callup() {
System.out.println("华为手机:打电话");
}
}
//华为路由器
public class HuaweiRouter implements IRouterProduct {
@Override
public void open() {
System.out.println("华为路由器:开机");
}
@Override
public void openWifi() {
System.out.println("华为路由器:打开WiFi");
}
}
// 4.------------ 超级工厂实现类,生产具体产品 ------------
//小米工厂,生产小米手机、路由器
public class XiaomiFactory implements IProductFactory{
@Override
public IPhoneProduct phoneProduct() {
return new XiaomiPhone();
}
@Override
public IRouterProduct routerProduct() {
return new XiaomiRouter();
}
}
//华为工厂,生产华为手机、路由器
public class HuaweiFactory implements IProductFactory{
@Override
public IPhoneProduct phoneProduct() {
return new HuaweiPhone();
}
@Override
public IRouterProduct routerProduct() {
return new HuaweiRouter();
}
}
// 5.------------ 消费者,实现具体消费 ------------
public class Consumer {
public static void main(String[] args) {
System.out.println("------------小米产品---------------");
XiaomiFactory xiaomiFactory = new XiaomiFactory(); //小米工厂
IPhoneProduct xiaomiPhone = xiaomiFactory.phoneProduct(); //生产小米手机
IRouterProduct xiaomiRouter = xiaomiFactory.routerProduct();//生产小米路由器
System.out.println("------------华为产品---------------");
HuaweiFactory huaweiFactory = new HuaweiFactory(); //华为工厂
IPhoneProduct huaweiPhone = huaweiFactory.phoneProduct(); //华为手机
IRouterProduct huaweiRouter = huaweiFactory.routerProduct();//华为路由器
}
}
- 超级工厂接口,只起到一个限制作用,限制产品族
- 具体工厂,实现超级工厂接口,生产不同规格的产品
简单工厂模式:虽然某种程度上不符合设计原则,但实际使用最多
工厂方法模式:不修改已有类的前提下,通过增加新的工厂类实现扩展
抽象工厂模式:不可以增加产品,可以增加产品族
建造者模式(builder)
建造者模式 | 也属于创建者模式,它提供了一种创建对象的最佳方式 |
---|---|
定义 | 讲一个复杂对象的构建与表示分离,使得同样的构建过程,可以创建不同的表示 |
主要作用 | 用户不需要知道,对象的的建造过程和细节,就能创建复杂的对象 |
例子 | 1.工厂(建造者模式):负责造汽车(组装过程和细节在工厂内) 2.购买者(用户):只需要说出型号(对象的类型和内容),就能直接购买(不用知道汽车怎么组装的) |
用户只需要给出复杂对象的类型和内容,建造者模式负责按顺序创建复杂对象(隐藏内部的过程和细节)
//1.-----------抽象的建造者:定义产品的建造方----------法
public abstract class Builder {
abstract void buildA(); //步骤一
abstract void buildB(); //步骤二
//建造完成,得到产品:
abstract Product getProduct();
}
//2.------- 定义产品,产品与建造者无必然联系 ---------
public class Product {
private String buildA; //步骤一搞定
private String buildB; //步骤二搞定
//get set 方法
public String getBuildA() { return buildA; }
public void setBuildA(String buildA) { this.buildA = buildA; }
public String getBuildB() { return buildB; }
public void setBuildB(String buildB) { this.buildB = buildB; }
//toString方法
@Override
public String toString() {
return "Product{" + "buildA='" + buildA + '\'' + ", buildB='" + buildB + '\'' + '}';
}
}
/*
3.----------工人继承Builder类,实现方法-------
不同的工人,可以按照构建者模式,生产不同规格的产品
*/
public class Worker extends Builder {
private Product product; //产品
/*--- 这一步很重要,工人负责创建产品,而不是外部传进来 --*/
public Worker() {
this.product = new Product();
}
//执行第一步
@Override
void buildA() {
product.setBuildA("步骤A");
System.out.println("步骤A");
}
//执行第二步
@Override
void buildB() {
product.setBuildB("步骤B");
System.out.println("步骤B");
}
//返回完整产品
@Override
Product getProduct() {
return product;
}
}
//4.----------指挥者,指挥工人生产产品
public class Director {
//****指挥工人,按照顺序构建产品****
public Product build(Builder builder){
//*****可以控制顺序*******
builder.buildB();
builder.buildA();
return builder.getProduct();
}
}
//5.--------测试:按照指挥者指定的顺序,得到产品--------
public static void main(String[] args) {
Director director = new Director();
Product product = director.build(new Worker());
System.out.println(product);
}
- 上面是Builder模式的常规用法,指挥类Director在构造者模式中作用很大:指挥具体构建者(Worker)如何构建产品,控制先后次序,并返回完整的产品类(Product)
- 通过静态内部类方式,实现零件无序装配构造,这种方式更灵活、更符合定义。内部有复杂对象的默认实现,使用时也可自定义更改内容,并且无需改变构造方式。
- 比如:饭店的套餐,服务员(具体构造者),可以默认几种菜品(零件)组成套餐(产品),客户(使用者)也可要求更换其中的菜品(零件)
- 比第一种少了指挥者,客户就是指挥者
//1.------------ 产品,属性有默认值 ------------
public class Product {
private String foodA = "小龙虾";
private String foodB = "大白菜";
//get set 方法
public String getFoodA() { return foodA; }
public void setFoodA(String foodA) { this.foodA = foodA; }
public String getFoodB() { return foodB; }
public void setFoodB(String foodB) { this.foodB = foodB; }
@Override
public String toString() {
return "Product{" + "foodA='" + foodA + '\'' + ", foodB='" + foodB + '\'' + '}';
}
}
//2.---------- 抽象构造者,定义生产产品有哪些步骤----------
public abstract class Builder {
//构造方法
abstract Builder buildA(String msg);
abstract Builder buildB(String msg);
//返回产品
abstract Product getProduct();
}
//3.---------- 具体构造者(服务员)----------
public class Worker extends Builder {
private Product product;
//产品在构造器中创建,而不是外部传进来
public Worker() {
product = new Product();
}
@Override
Builder buildA(String msg) {
product.setFoodA(msg);
return this; //返回当前对象
}
@Override
Builder buildB(String msg) {
product.setFoodA(msg);
return this; //返回当前对象
}
@Override
Product getProduct() {
return product;
}
}
//4.---------- 测试:----------
public static void main(String[] args) {
//服务员
Builder worker = new Worker();
//输出默认产品
System.out.println(worker.getProduct());
//链式编程(执行方法之后,依然返回当前对象:worker)
Product product = worker.buildA("大豆角").buildB("冰淇淋").getProduct();
System.out.println(product);
}
输出结果:
Product{foodA='小龙虾', foodB='大白菜'}
Product{foodA='大豆角', foodB='冰淇淋'}
优点
产品的建造和表示分离,实现了解耦。客户端不用知道具体细节
将负责的产品创建步骤、分节在不同的方法中,使创建过程更清晰
符合“开闭原则”
,不同的建造者之间相互独立,有利于系统的扩展
缺点
适用范围受限
:创建的产品具有较多共同点,组成部分相似;如果产品之间差异性很大,则不适用建造者模式。
如果产品的内部变化复杂,可能导致需要定义很多具体建造者(实现类)来实现这种变化,导致系统很庞大
应用场景
- 产品对象 内部结构复杂、具备共性
- 隔离复杂对象的创建和使用,使相同的创建过程可以创建不同的产品
- 适合创建有较多属性的对象
与抽象工厂比较
建造者模式 | 抽象工厂 |
---|---|
返回一个组装好的完整产品 | 返回一系列相关产品,具有不同的产品等级结构,阻证产品族 |
不直接调用建造者的方法,通过指挥者指导如何生成对象, 包括对象的组装过程和建造步骤。侧重于分步骤构造一个复杂对象 | 客户端实例化工厂类,调用工厂方法,获取所需产品对象 |
类似于汽车组装工厂,通过对部件的组装,返回一辆整车 | 类似于汽车配件生产工厂,生产一个产品的产品族 |
原型模式(prototype)
原型模式是一种对象创建型模式, 用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象 。
原型模式允许通过一个原型对象创建一个或多个同类型的其他对象,而无须知道任何创建的细节
模式动机
-
复制
一个对象,从而克隆出多个与原型对象一模一样的对象
——原型模式 -
有些对象的创建过程较为复杂,而且需要频繁创建
-
通过给出一个原型对象来
指明所要创建的对象的类型
,然后用复制这个原型对象
的办法创建出更多同类型的对象
与new的区别:克隆有cloneable接口和clone()方法,new相当于创造了一个新的对象,会有原本的默认值,而克隆是直接拷贝了一个对象,完全一致。
原型模式的适用场景
-
创建新对象成本较大
,新对象可以通过复制已有对象来获得,如果是相似对象,则可以对其成员变量稍作修改(或者是对象比较复杂的情况) -
系统要保存对象的状态,而
对象的状态变化很小
-
需要避免使用分层次的工厂类
来创建分层次的对象
spring bean:单例模式+原型模式
工厂模式: new 对象 <–替换成–> 原型模式
浅拷贝和深拷贝
浅拷贝:对当前对象进行克隆,并克隆该对象所包含的8种基本数据类型和String类型属性(拷贝一份该对象并重新分配内存,即产生了新的对象);但如果被克隆的对象中包含除8中数据类型和String类型外的其他类型的属性,浅拷贝并不会克隆这些属性(即不会为这些属性分配内存,而是引用原来对象中的属性)。
深拷贝:深拷贝是在浅拷贝的基础上,递归地克隆除8种基本数据类型和String类型外的属性(即为这些属性重新分配内存而非引用原来对象中的属性)
深拷贝简单的理解就是在浅拷贝的基础上,进一步对对象属性进行依次拷贝,对象里面的复杂属性也重新生成了新的引用。
浅拷贝中由于除8中数据类型和String类型外的其他类型的属性不会被克隆,因此当通过新对象对这些属性进行修改时,原对象的属性也会同时改变。而深拷贝则已经对这些属性重新分配内存,所以当通过新对象对这些属性进行修改时,原对象的属性不会改变。
/**
* 浅克隆:将对象的值完全拷贝一份,
* 如果属性为引用类型变量,则拷贝属性指向的内存地址
* 实现Cloneable接口
* 重写 clone 方法(不做改造)
*/
public class Student implements Cloneable{
public int id;
public Integer age;
public String name;
public Date createTime;
public Student(int id, Integer age, String name,Date createTime) {
this.id = id;
this.age = age;
this.name = name;
this.createTime = createTime;
}
@Override
public String toString() {
return "Student{" + "id=" + id + ", age=" + age + ", name='" + name + '\'' + ", createTime=" + createTime + '}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws Exception {
Student stu_1 = new Student(1,18,"小红",new Date());
Student stu_2 = (Student) stu_1.clone();
System.out.println(stu_1);
System.out.println(stu_2);
System.out.println("-------修改stu_2的值------");
stu_2.id = 2; //不影响原对象
stu_2.age = 20; //不影响原对象
stu_2.name = "小兰";//不影响原对象
// ------- 影响原对象 -------
stu_2.createTime.setTime(22123156);
System.out.println(stu_1);
System.out.println(stu_2);
}
}
输出结果为:
Student{id=1, age=18, name='小红', createTime=Sun Apr 24 12:39:28 CST 2022}
Student{id=1, age=18, name='小红', createTime=Sun Apr 24 12:39:28 CST 2022}
-------修改stu_2的值------
Student{id=1, age=18, name='小红', createTime=Thu Jan 01 14:08:43 CST 1970}
Student{id=2, age=20, name='小兰', createTime=Thu Jan 01 14:08:43 CST 1970}
/**
* 实现深克隆的方式:
* 1.序列化与反序列化
* 2.改造clone方法
*/
@Override
protected Object clone() throws CloneNotSupportedException {
Student stu = (Student) super.clone();
//将引用类型的变量,也克隆一份
stu.createTime = (Date) this.createTime.clone();
return stu;
}
输出结果为:
Student{id=1, age=18, name='小红', createTime=Sun Apr 24 12:39:28 CST 2022}
Student{id=1, age=18, name='小红', createTime=Sun Apr 24 12:39:28 CST 2022}
-------修改stu_2的值------
Student{id=1, age=18, name='小红', createTime=Sun Apr 24 12:39:28 CST 2022}
Student{id=2, age=20, name='小兰', createTime=Thu Jan 01 14:08:43 CST 1970}
原型模式的优缺点
优点:
-
简化对象的创建过程
,通过复制一个已有实例可以提高新实例的创建效率 -
扩展性较好
-
简化创建结构
,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品 -
可以使用深克隆的方式
保存对象的状态
,以便在需要的时候使用,可辅助实现撤销操作
缺点:
需要为每一个类配备一个克隆方法
,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了开闭原则
- 在
实现深克隆时需要编写较为复杂的代码
,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦
适配器模式(adapter)
结构型模式的作用:
从程序的结构上实现松耦合,从而可以扩大整体的类结构,用来解决更大的问题
适配器模式(也称包装样式or包装):将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。
//1. -------- 被适配的类:网线 --------
public class NetCable {
public void request(){
System.out.println("连上网线,可以上网");
}
}
//2. -------- 客户端类:想上网,但接口插不上网线 --------
public class Computer {
//上网的具体实现,插不上传统网线,需要一个转接头
public void net(NetToUsb adapter){
//上网的具体实现
adapter.handlerRequest();
}
}
//3. -------- 接口转换器的抽象实现 --------
public interface NetToUsb {
//作用:处理请求(将 网线插口 ---》 USB插口)
public void handlerRequest();
}
/**
* 4. -------- 真正的适配器:需要连接USB 和 网线 --------
* 继承网线类,实现网线转USB接口
*/
public class Adapter extends NetCable implements NetToUsb {
@Override
public void handlerRequest() {
super.request(); //调用网线的上网方法
}
}
//5. -------- 测试:电脑通过适配器,调用网线的上网方法 --------
public static void main(String[] args) {
//创建电脑、适配器;适配器实现了转接口的功能,可以调用
Computer computer = new Computer();
Adapter adapter = new Adapter();
computer.net(adapter);
}
适配器有两种方式:
1.继承:类适配器(java是单继承) 上面代码就是 类适配器
2.组合:对象适配器(常用)
// ------ 对象适配器:通过组合的方式,将网线的功能集成进来 ------
public class Adapter2 implements NetToUsb {
private NetCable netCable; //通过组合,集成网线类
//构造方法中,传入网线对象
public Adapter2(NetCable netCable) {
this.netCable = netCable;
}
@Override
public void handlerRequest() {
netCable.request();
}
}
public static void main(String[] args) {
//1.创建电脑
Computer computer = new Computer();
//2.创建网线对象
NetCable netCable = new NetCable();
//3.将网线对象,放入适配器中
Adapter2 adapter2 = new Adapter2(netCable);
//4.电脑调用适配器接口,可以上网
computer.net(adapter2);
}
适配器模式:将一个类的接口,转换成客户希望的另一个接口。Adapter模式使得原来由于接口不兼容而不能一起工作的类,可以在一起工作。
角色分析:
- 目标接口:客户期待的接口。目标接口可以是具体或者抽象类,也可以是接口(电脑需要的USB接口)
- 需要适配的类:被适配的类或者适配者类(网线)
- 适配器:通过包装一个需要适配的对象,把原接口转换成目标对象(SUB、网线 接口转换器)
分类 | 描述 |
---|---|
对象适配器优点 | 1.一个对象适配器,可以将多个不同的适配者,适配到同一个目标 2.满足里氏替换原则:由于适配器和适配者是关联关系,可以适配一个适配者或者它的子类 |
类适配器缺点 | 1.对于java、C#这种不支持多继承的语言,一个适配器只能适配一个适配者 2.在java、C#等语言中,类适配器模式中的目标抽象类,只能为接口不能为类,有局限性 |
适用场景 | 1.系统要使用一些现有的类,但这些类的接口(如方法名)不符合系统需要,甚至没有这些类的源代码 2.想创建一个可以重复使用的类,用于与一些彼此之间没有关联、甚至将来可能引进的类,一起工作 |
桥接模式(bridge)
上图为多重继承模式,类的个数比较多,扩展比较麻烦;
下图为抽象出两个纬度,减少类的构建:
//1.品牌纬度 --------------- 接口:电脑品牌 ---------------
public interface Brand {
void brandName();
}
//联想品牌
public class Lenovo implements Brand {
@Override
public void brandName() {
System.out.print("联想品牌");
}
}
//苹果品牌
public class Apple implements Brand {
@Override
public void brandName() {
System.out.print("苹果品牌");
}
}
//2.种类纬度 --------------- 抽象类:电脑种类 ---------------
public abstract class Computer {
//通过组合的方式,给电脑增加上品牌属性
protected Brand brand;
//构造器中,引入品牌(出厂自带品牌)
public Computer(Brand brand) {
this.brand = brand;
}
//输出电脑信息:品牌 + 种类
abstract void computerInfo();
}
//笔记本
public class NoteBook extends Computer {
public NoteBook(Brand brand) { super(brand); }
@Override
void computerInfo() {
brand.brandName();
System.out.println("笔记本");
}
}
//平板
public class Flat extends Computer {
public Flat(Brand brand) { super(brand); }
@Override
void computerInfo() {
brand.brandName();
System.out.println("平板");
}
}
//3. ----------- 测试 ------------
public static void main(String[] args) {
//苹果笔记本
Computer computer = new NoteBook(new Apple());
computer.computerInfo();
//联想平板
Computer computer2 = new Flat(new Lenovo());
computer2.computerInfo();
}
输出结果:
苹果品牌笔记本
联想品牌平板
优点:
- 桥接模式偶尔类似于多重继承方案。但多重继承违背了单一职责原则,类的个数非常多。桥接模式抽象出不同纬度,可以减少子类的个数,降低管理和维护成本
- 桥接模式符合开闭原则,提高系统的可扩充性。如上面例子,两个纬度任意变化,都不需要修改原系统。就像一座桥,(通过组合)把两个纬度连接起来
缺点
- 桥接模式增加系统的理解与设计难度。聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程
- 桥接模式要求正确识别出系统中两个独立变化的纬度,使用范围有局限性
最佳实践
- 如果一个系统需要在构建的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
- 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
场景
- Java语言通过Java虚拟机实现了平台的无关性。
- AWT中的Peer架构
- JDBC驱动程序也是桥接模式的应用之一
代理模式(Proxy)
动态代理分为两大类:基于接口的动态代理(例如JDK动态代理)
基于类的动态代理(例如Cglib代理)
可以去了解一下JAVAssist(字节码类库)
静态代理模式
角色分析:
- 抽象角色: 一般会使用接口或抽象类来解决(租房接口)
- 真实角色: 被代理的角色(房东)
- 代理角色: 代理真实角色,代理真实角色后,我们一般会做一些附属操作(中介)
- 客户: 访问代理对象的人(租户)
// ------- 租房接口。代理对象与被代理对象,都实现这个接口 -------
public interface Rent {
void rent();
}
// ------- 房东,实现租房接口 -------
public class Host implements Rent {
@Override
public void rent() {
System.out.println("房东出租房子");
}
}
/**
* 代理对象
* 使用组合将被代理的对象,集合进来(尽量不适用继承)
*/
public class Proxy implements Rent {
Host host; //房东:被代理的对象
public Proxy(Host host) { //构造方法中,传入房东
this.host = host;
}
//中介代理房东出租房子,执行一些额外的操作
@Override
public void rent() {
watchHouse(); //额外操作1.租房子
host.rent();
charge(); //额外操作2.收中介费
}
private void watchHouse(){
System.out.println("中介带你看房");
}
private void charge(){
System.out.println("中介带你看房");
}
}
// ------------ 测试 -----------------
public static void main(String[] args) {
Host host = new Host(); //房东要出租房子
Proxy proxy = new Proxy(host); //中介代理房东,与租户打交道
proxy.rent(); //租户找中介,租房子
}
输出结果:
中介带你看房
房东出租房子
中介带你看房
代理模式的好处:
- 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务
- 公共也就交给代理角色,实现了业务的分工
- 公共业务发生扩展的时候,方便集中管理
缺点
- 一个真实角色就会产生一个代理角色,代码量会翻倍,开发效率会变低
静态代理加深理解:
//1. ---------- 业务层的接口,增删改查方法 ----------
public interface IUserService {
public void add();
public void delete();
public void modify();
public void search();
}
//2. ------- 被代理的类:业务层具体实现 --------
public class UserService implements IUserService {
@Override
public void add() { System.out.println("新增方法"); }
@Override
public void delete() { System.out.println("删除方法"); }
@Override
public void modify() { System.out.println("修改方法"); }
@Override
public void search() { System.out.println("查询方法"); }
}
//3.-----代理类(与被代理类,实现同一个接口),实现日志功能
public class UserServiceProxy implements IUserService{
//被代理的类私有化,使用set方法注入
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
/**
* 在执行原业务逻辑之前,给每个方法添加上日志
*/
@Override
public void add() {
log("add");
userService.add();
}
@Override
public void delete() {
log("delete");
userService.delete();
}
@Override
public void modify() {
log("modify");
userService.modify();
}
@Override
public void search() {
log("search");
userService.search();
}
private void log(String msg){
System.out.println("log日志:调用了"+msg+"方法");
}
}
//4. ------------ 测试 -----------
public static void main(String[] args) {
//具体业务的实现类
UserService userService = new UserService();
//将业务实现类,注入到代理类
UserServiceProxy userServiceProxy = new UserServiceProxy();
userServiceProxy.setUserService(userService);
/*
通过代理类,调用业务处理方法:
在不修改业务处理类的情况下,添加日志
*/
userServiceProxy.add();
}
动态代理模式
- 动态代理与静态代理,角色组成一样(抽象角色、真实角色、代理角色)
- 动态代理的代理类是动态生成的,不是我们写好的
- 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
- 基于接口: JDK动态代理
- 基于类: cglib
- java字节码实现:javasist
动态代理两个重要的类:
- Proxy:代理(生成动态代理实例)
- InvocationHandler:调用处理程序(调用处理程序,并返回结果)
//1.------------- 租房接口。代理对象与被代理对象,都实现这个接口 ------------
public interface Rent {
void rent();
}
//2.------------- 房东,实现租房接口 -------------
public class Host implements Rent {
@Override
public void rent() {
System.out.println("房东出租房子");
}
}
/**
* MyInvocationHandler:代理实例的调用处理程序,继承了InvocationHandler接口
* 用这个类,自动生成代理类(上面的UserServiceProxy 也是代理类)
*/
public class MyInvocationHandler implements InvocationHandler {
//1.被代理的接口:
private Rent rent;
public void setRent(Rent rent) {
this.rent = rent;
}
//2.生成代理类
public Object getProxy(){
/**
* ClassLoader loader //定义 代理类 的类加载器
* Class<?>[] interfaces //代理类 实现的接口列表
* InvocationHandler h //将方法调用分派到的 调用处理程序
*/
return Proxy.newProxyInstance(rent.getClass().getClassLoader(),rent.getClass().getInterfaces(),this);
}
//3.处理代理程序,并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
seeHouse();
//动态代理的本质,就是利用反射机制实现
Object result = method.invoke(rent, args);
charge();
return result;
}
private void seeHouse(){
System.out.println("中介带你看房子");
}
private void charge(){
System.out.println("收取中介费");
}
}
// ---------------------- 测试 ---------------------------
public static void main(String[] args) {
//1.真实角色
Host host = new Host(); //房东
//2.调用处理程序
MyInvocationHandler mih = new MyInvocationHandler();
//3.通过调用处理程序,处理我们要调用的【接口实现类对象】
mih.setRent(host);
//4.动态得到代理类,并强转为 Rent 接口类型:
// 这里的rentProxy就是动态生成的,并没有定义这个类(不像之前的 UserServiceProxy )
Rent rentProxy = (Rent) mih.getProxy();
//5.1 调用代理实例的 rent() 方法,
//5.2 被编码并转发到 调用处理程序(mih)的invoke方法上
rentProxy.rent();
}
输出结果:
中介带你看房子
房东出租房子
收取中介费
下面是动态代理的工具类,将被代理的接口,换成Object
/**
* 将被代理的接口(上面的Rent),换成Object。可以代理所有的Object子类
* 用这个类,自动生成代理类(上面的UserServiceProxy 也是代理类)
*/
public class DynamicInvocationHandler implements InvocationHandler {
//1.被代理的接口:
private Object target;
public void setTarget(Object target) {
this.target = target;
}
//2.生成代理类
public Object getProxy(){
return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
//3.处理代理程序,并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//动态代理的本质,就是利用反射机制实现
printLog(method.getName());
Object result = method.invoke(target, args);
return result;
}
//日志方法
private void printLog(String msg){
System.out.println("执行了 "+msg+" 方法");
}
}
// ---------------------- 测试 ---------------------------
public static void main(String[] args) {
//万能代理类:
//1.真实对象
UserService userService = new UserService();
//2.调用处理程序
DynamicInvocationHandler dih = new DynamicInvocationHandler();
//3.通过调用处理程序,处理我们要调用的【接口实现类对象】
dih.setTarget(userService);
//4.动态得到代理类,并强转类型
// 只能转换成接口,不能强转成 实现类
IUserService proxy = (IUserService) dih.getProxy();
//5.通过代理类,执行方法
proxy.add();
proxy.delete();
}
输出结果:
执行了 add 方法
新增方法
执行了 delete 方法
删除方法
动态代理模式的优缺点
优点:
1)可以使真实角色的操作更加纯粹,不用去关注一些公共的业务
2)公共业务就交给了代理角色,实现了业务的分工
3)公共业务发生扩展的时候,方便集中管理
4)一个动态代理类代理的是一个接口,一般就是对应的一类业务
5)一个动态代理类可以代理多个类,只要实现了同一个接口即可
动态代理模式和静态代理模式的区别说一个例子就很好理解了,现在是一个房东一个中介,那如果有两个房东呢?我们就需要一个与之对应的中介了,否则不符合开闭原则
代理模式的应用场景
如果已有的方法在使用的时候需要对原有的方法进行改进,此时有两种办法:
1、修改原有的方法来适应。这样违反了开闭原则。
2、就是采用一个代理类调用原有的方法,且对产生的结果进行控制。这种方法就是代理模式。
使用代理模式,可以将功能划分的更加清晰,有助于后期维护!
代理模式与适配器模式
都是借助第三方去访问目标对象
适配器模式的重点在于衔接不同类型的类或接口
代理模式的重点在于处理不同类之间的逻辑实现