设计模式 - 编码模式
一、工厂模式
1、简单工厂模式
简单工厂模式:一个工厂类来创建不同类型的对象
例如:如果我们不用工厂模式,当客户需要一个面包时,一般情况是客户去创建一个面包,然后拿来用。而现在用了工厂模式,客户就不必创建面包了,只需要告知他想要一个面包,然后由食物工厂去创建面包。
- 优点
- 工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。
- 它通过一个工厂类来创建不同类型的对象,而无需客户端直接使用new关键字来实例化对象。
- 缺点
- 不易扩展、违背了“开闭原则“
解决方法 :可以考虑用反射技巧来去除Switch或if(超级工厂模式),解除分支判断带来的耦合。
2、工厂方法模式
工厂方法模式:一个工厂只能创建一个类型(产品),一个类型(产品)对应着一个工厂。
将工厂抽象为工厂父类,根据不同的产品进行创建不同的工厂。
定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到子类。
例如:我们简单工厂中有两个产品(手机、电脑),使用工厂方法会产生对应的工厂,如工厂A对应着手机、工厂B对应着电脑。
- 优点
- 解决了简单工厂带来的“开闭原则”的问题。
- 一个工厂对应一个类
- 缺点:工厂方法只能创建一种产品,一个产品对应着一个工厂,所有当产品过多的时候,会产生类爆炸的问题。
3、抽象工厂模式
抽象工厂模式:一个工厂可以创建多个不同的类型(产品)。
抽象工厂模式是一种创建型设计模式,它提供了一种将一组相关或相互依赖的对象创建起来的方式,而无需指定它们具体的类。这种模式通过提供一个接口,使得客户端能够创建一系列相关或相互依赖的对象,而无需知道具体的实现细节。
首先我们需要理解一个产品族的感念。例如我们有两种产品,两个品牌,一个Apple品牌一个格力品牌,二品牌下我们有一系列的产品,如,Apple有手机、平板、电脑等,二格力有手机、空调、洗衣机等等,以上我们可以理解为两个品牌。而两个品牌我认为可以理解为具体你的工厂,两个具体的工厂它上面有一个通用的工厂为创建手机的工厂。
- 优点
- 解决了工厂方法存在的类爆炸的问题
- 解决创建多个不同的产品
- 缺点
- 每加一个产品对象时,就需要大批量的改动,添加多个类。
- 违背了“开闭原则”
4、超级工厂模式
超级工厂模式:一个工厂类来创建不同类型的对象
超级工厂模式与简单工厂模式的概念相同,只是在超级工厂模式中很好的解决了 “ 开闭原则 ”。
简单的说就是可以创建N种产品。
超级工厂模式(简单工厂 + 反射)
- 优点:利用反射机制解决了 “开闭原则” 的问题。
- 缺点:每次创建一种对象类型的时候,会导致工厂类数量增多,可能会影响系统的性能和内存占用。
public class SuperFactory{
/**
* 根据完整类型名动态加载class并创建实例
* @param className
* @return
*/
public static <T> T create(String className){
try {
// 1、动态加载class
Class<?> aClass = Class.forName(className);
// 2、利用class对象创建实例,先得到class中的构造器,然后偶再创建实现
// 在jdk7以后,就淘汰了newInstance
T instance = (T) aClass.getConstructor().newInstance();
return instance;
} catch (Exception e) {
// 异常重抛
throw new RuntimeException("Create instance fail.",e);
}
}
}
/**
* 每次调用方法的时候,实例化的对象都是不一样的
*/
public static void main(String[] args) {
Phone phone1 = SuperFactory.create("org.nf.product.Iphone");
Phone phone2 = SuperFactory.create("org.nf.product.Iphone");
// false
System.out.print(phon1 == phone2)
}
5、容器工厂模式
容器工厂模式:先把创建的对象放在容器中中,对象的创建可以使用如上的任意这种工厂变体。
容器工厂首先我们需要理解什么是容器。所谓容器就是用来装东西,那么该容器装的东西是什么呢?答案是我们的对象,我们将需要长期使用的或者指定的类创建出来的对象放在一个容器中存储,同时对外提供获取对象的入口。
利用容器(map集合)存储所有的具体类,使用时,直接根据 键 来获取 值。
相比超级工厂而言,容器工厂实例相同的类时,两者是相等的,超级工厂则反之。
优点:可以复用,为后续所有的业务做支撑
public class ContainerFactory {
/**
* 容器
*/
private static Map<String,Object> container = new HashMap<>();
/**
* 在静态代码块中初始化整个容器工厂
* 将properties文件中配置的对象,创建实例放入Map中
*/
static {
// 加载配置文件
// 创建 Properties 对象
Properties prop = new Properties();
// 根据 Properties 文件的位置创建一个输入流,给 Properties 对象进行读操作
// InputStream:输入流 - 用完就关闭
// getClassLoader:类加载器
// getResourceAsStream:找到项目的src目录
try (InputStream input = ContainerFactory.class.getClassLoader().getResourceAsStream("Bean.properties")){
// 加载并读取 properties 文件的内容
prop.load(input);
// 循环遍历 prop 对象,得到每一个 properties 的键值对
// 把创建好的实例放入Map集合中 key:键 value:值
// map.put(key,value);
prop.forEach((key,value) -> container.put(key.toString(), newInstance(value.toString())));
} catch (Exception e) {
throw new RuntimeException("解析失败",e);
}
}
/**
* 根据完整类名创建实例
* @param className
* @return
*/
private static Object newInstance(String className){
try {
// 创建对象实例
return Class.forName(className).getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("实例化失败!",e);
}
}
/**
* 容器工厂方法
* 根据 key(键) 从 map集合 中获取你想要的 value(值)
*/
public static <T> T getBean(String name){
return (T) container.get(name);
}
}
public static void main(String[] args) {
Phone phone1 = ContainerFactory.getBean("iphone");
Phone phone2 = ContainerFactory.getBean("iphone");
// true
System.out.print(phon1 == phone2)
}
6、利用注解优化容器工厂模式
解决了原容器工厂只能拿到单例的对象,实现了可以拿到原型(新建实例) 或 单例(已建实例)。
实现步骤:
- 扫描工具类 - 扫描指定的包,并返回相关的 Class对象
- 定义注解 - 对想要操作的对象进行说明、注释。
- 定义一个容器工厂 - 为后续所有的业务做支撑。
- 定义两个容器:容器(单例) - 存放事先创建好的对象;容器(原型) - 存放Class对象,需要时再创建
- 初始化参数 - 扫描包路径
- 解析Class对象集合,找到带有注解的类
- 第一:判断是否声明了注解
- 第二:获取注解的value属性值,这个值作为容器的key
- 第三:获取scope注解来决定保存的是单例还是原型
- 第四:利用class创建实例
- 第五:将实例和key保存在容器中
- 从容器中获取对象
实现代码
第一:扫描器
<!-- 在maven项目中添加classGraph:扫描器的依赖 -->
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<version>4.8.157</version>
</dependency>
/**
* 说明:扫描的工具类
*
* @Author w
* @Date 2023-06-01
*/
public class ScanUtil {
/**
* 扫描指定的包,并返回相关的 Class对象
* @param packages
* @return
*/
public static List<Class<?>> scan(String... packages){
// 第一:创建核心的类型对象(它是扫描的核心类)
ClassGraph graph = new ClassGraph();
// 第二:启用所有(enableAllInfo)的扫描机制(支持包、类、方法、注解级别的扫描)
graph.enableAllInfo();
// 第三:设置要扫描的包路径
graph.acceptPackages(packages);
// 第四:执行扫描并返回的结果集(总部一:结果集需要放在try资源中用完需要关闭)
try(ScanResult result = graph.scan()) {
// 第五:从结果集中获取所有的class信息,并加载到JVM中
return result.getAllClasses().loadClasses();
} catch (Exception e){
throw new RuntimeException("",e);
}
}
/**
* 对扫描工具类进行测试
* @param args
*/
public static void main(String[] args) {
List<Class<?>> list = scan("org.nf.container");
for (Class<?> aClass : list) {
System.out.println(aClass);
}
}
}
第二:定义注解
// 该注解只能用在类上
@Target({ElementType.TYPE})
// 该注解在运行时一直保留
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {
/**
* 声明一个value属性,用于定义Bean的别名
* 用作容器中的 key
* @return
*/
String value();
/**
* scope属性用来标识容器在保存对象时是否是单例还是原型
* 默认为true就表示创建的单例
* @return
*/
boolean scope() default true;
}
第三:定义容器工厂类
/**
* 说明:容器工厂:每次在容器中获取的对象都是唯一的 - 单例
* <p>
* 如果我每次在容器中获取对象,希望创建是一个新的实例,如何解决?
* 定义两个容器:
* 一个保存单例对象
* 一个保存原型(Class对象) - 创建的实例
*
* @Author w
* @Date 2023-06-01
*/
public class ContainerFactory {
/**
* 核心容器(单例) - 存放事先创建好的对象
*/
private static final Map<String, Object> singleton = new HashMap<>();
/**
* 核心容器(原型) - 存放Class对象,需要时再创建实例
*/
private static final Map<String, Class<?>> productType = new HashMap<>();
/**
* 初始化参数
*
* @param packages 表示要扫描的包路径
*/
public ContainerFactory(String... packages) {
// 执行扫描,返回class集合
List<Class<?>> list = ScanUtil.scan(packages);
// 解析所有的class对象,找到带有@Bean注解的类
resolveClass(list);
}
/**
* 解析所有的Class对象,找到带有@Bean注解的类
* @param list Class集合
*/
private void resolveClass(List<Class<?>> list) {
// 循环遍历Class集合
list.forEach((clazz) -> {
// 第一:判断是否声明了Bean注解 - 是否存在注解
if (clazz.isAnnotationPresent(Bean.class)) {
// 第二:获取@Bean注解的value属性值,这个值就是作为容器的key
// 1.先得到@Bean注解对象
// Bean bean = clazz.getAnnotation(Bean.class);
// 2.得到注解的value属性的值
// String value = bean.value();
String value = clazz.getAnnotation(Bean.class).value();
// 第三:获取scope注解来决定保存的是单例还是原型
if (clazz.getAnnotation(Bean.class).scope()) {
// 第四:利用class创建实例
Object instance = newInstance(clazz);
// 第五:将实例和kay保存在容器中
singleton.put(value, instance);
} else {
// 否则只保存当前的class对象到原型的容器中
productType.put(value, clazz);
}
}
});
}
/**
* 创建对象实例 - 封装成一个方法 - 达到代码的复用
*
* @param clazz
* @return
*/
private Object newInstance(Class<?> clazz) {
try {
return clazz.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("实例化失败", e);
}
}
/**
* 从容器中获取对象
*
* @param name 根据@Bean注解的 value 属性,从而获取容器中的key
* @param <T> 不需要强转类型
* @return
*/
public <T> T getBean(String name) {
// 先从单例的容器中获取,如果存在则直接返回
Object instance = singleton.get(name);
// 如果在单例容器中为null,则从原型的容器中获取Class对象来新建实例
// 否则就直接返回单例的对象即可
if (instance == null) {
// 否则取出的原型的Class对象创建实例再返回
Class<?> aClass = productType.get(name);
// 新建实例
return (T) newInstance(aClass);
}
return (T) instance;
}
}
第四:测试
@Test
public void test01(){
ContainerFactory factory = new ContainerFactory("org.nf.container");
Phone phone1 = factory.getBean("iPhone");
Phone phone2 = factory.getBean("iPhone");
Phone phone3 = factory.getBean("miPhone");
Phone phone4 = factory.getBean("miPhone");
System.out.println(phone1);
System.out.println(phone2);
System.out.println(phone3);
System.out.println(phone4);
/**
* org.nf.container.product.impl.IPhone@105fece7
* org.nf.container.product.impl.IPhone@105fece7
* org.nf.container.product.impl.MiPhone@3ec300f1
* org.nf.container.product.impl.MiPhone@482cd91f
*/
}
二、单例模式
1、单例模式
- 属于设计模式中的创建类
- 创建方式有两种,饿汉式与懒汉式
- 通常会将构造器私有化,并对外提供一个公开访问
保证某一个类在系统中只能创建一个(唯一)实例。
2、饿汉式
饿汉式:不管你需不需要都先将实例创建好,通过公开访问进行获取对象实例
缺点:每次启动的时候就将其对象创建好,内存占用。
实现方法:
- 把构造方法私有化 - 让外界不能随意实例化
- 在类的内部创建一个自身的静态实例
- 提供一个公开的静态方法
第一种实现
传统饿汉式
class People {
/**
* 创建一个实例
*/
private static final People INSTANCE = new People();
/**
* 第一:把构造方法私有化,不让外部的类来创建实例
*/
private People(){}
/**
* 对外部提供一个公开的方法获取实现
* @return
*/
public static People getInstance(){
// 当使用InnerClass时才会加载这个内部类,从而初始化People实例
return INSTANCE;
}
}
第二种实现
通过饿汉式结合静态内部类进行创建 - 解决了饿汉式内存占用的问题,同时也解决了懒汉式线程安全问题
public class People {
/**
* 第一:把构造方法私有化,不让外部的类来创建实例
*/
private People(){}
/**
* 使用私有静态内部类来创建一个唯一的外部类的实例
* 内部类:类中还有一个类
*/
private static class InnerClass{
// 创建一个唯一的外部类实例
private static final People INSTANCE = new People();
}
/**
* 对外部提供一个公开的方法获取实现
* @return
*/
public static People getInstance(){
// 当使用InnerClass时才会加载这个内部类,从而初始化People实例
return InnerClass.INSTANCE;
}
public void say(){
System.out.println("Hello word");
}
}
3、懒汉式
懒汉式:等需要的时候再来创建。
缺点:线程安全问题
与饿汉式相反,我们先定义对象,但不进行实例化,在公开访问中进行判断是否以进行实例化,若是实例化了,我们便将对象返回,若是没有实例化我们便进行new。
我们使用懒汉式时,我们常常也会考虑到一个线程安全问题,假定我们有N个线程通过调用改公开访问方法,不排除某个线程执行到判断就开始
public class Main {
public static void main(String[] args) {
Person p1 = Person.person();
Person p2 = Person.person();
System.out.println(p1 == p2);
}
}
class Person {
private static Person person;
private Person() {
}
public synchronized static Person person() {
// 懒汉式创建实例时,必须进行判断为null,为null表示没有创建,反之表示已经创建好了实例
synchronized (Person.class) {
if (person == null) {
person = new Person();
}
return person;
}
}
}
三、策略模式
1、概念
不同的场景有不同的表现
封装了不同的算法
什么时候需要使用到策略模式:有多种业务实现,但是只能选择一种实现的情况下 - 多选一
2、核心
策略上下文:主要用于策略的选择和创建(内部隐含一个简单工厂)
比如在算法策略类中:有原价策略、折扣策略、返利策略等,
然后使用上下文维护一个策略类对象的引用(选择哪个策略)
策略模式就是基于简单工厂模式,结合反射(容器工厂),很好的解决开闭原则的问题!
要点:只要在分析过程中需要在不同时间应用不同的业务规则。
四、模板方法模式
模板方法模式:当任务需求按顺序执行 (业务有很多步骤按顺序执行的)
例如:有三个步骤:step1、step2、step3;把它们放在一个大的‘盒子’里(模块方法),依次去执行它;这样就不需要客户端一个一个的去实现这些步骤。
优点
- 把步骤封装起来,按顺序执行的时候较为便捷。
- 遵循了开闭原则
- 提高代码的复用性
- 简化代码逻辑
案例
**
* 说明:抽象老师类
*
* @Author w
* @Date 2023-06-05
*/
public abstract class Teacher {
/**
* 所有老师上课都是一样的点名
*/
public void call(){
System.out.println("上课点名");
}
/**
* 上课,不同的老师做不同的实现(课程)
*/
public abstract void lesson();
/**
* 布置作业,不同的专业布置不同的内容
*/
public abstract void homework();
/**
* 这就是模板方法,将业务执行的步骤顺序封装在这个方法中
* 并暴露给客户端调用
*/
public void work(){
call();
// 调用的是子类实现的方法
lesson();
// 判断钩子是否剔除了此方法
if (hock()){
// 调用的是子类实现的方法
homework();
}
}
/**
* 钩子方法,子类负责重写这个方法来剔除不需要的步骤
* 默认值为true表示不剔除
* @return
*/
protected boolean hock(){
return true;
}
}
public class EnglisTeacher extends Teacher{
@Override
public void lesson() {
System.out.println("上英语课");
}
@Override
public void homework() {
System.out.println("布置英语作业");
}
/**
* 剔除方法
* @return
*/
@Override
protected boolean hock(){
return false;
}
}
public class Main {
public static void main(String[] args) {
Teacher teacher = new EnglisTeacher();
/*teacher.call();
teacher.lesson();
teacher.homework();*/
// 上课点名
// 上英语课
teacher.work();
}
}
注意:使用到了钩子方法
/** * 钩子方法,子类负责重写这个方法来剔除不需要的步骤 * 默认值为true表示不剔除 * @return */ protected boolean hock(){ return true; }
五、适配器模式
适配器模式:是一种结构型设计模式,它允许将不兼容的接口转换为可兼容的接口,以便不同的类之间能够协同工作。
思想:把一个类的接口编程客户端所期待的另一个接口,从而使原本的两个类能够在一起工作。
理解:
也就是说,当前系统存在两种接口A和B,客户只支持访问A接口,但是当前系统没有A接口对象,但是有B接口对象,但客户无法识别B接口,因此需要通过一个适配器C, 将B接口内容转换成A接口,从而使得客户能够从A接口获取得到B接口内容。在软件开发中,基本上任何问题都可以通过增加一个中间层进行解决。适配器模式其实就是一个中间层。综上,适配器模式其实起着转化/委托的作用,将-种接口转化为另-种符合需求的接口。
优点
- 优点:增加了类的透明性和复用性,使得适配器的灵活性更强。
- 缺点:一次最多只能适配一个适配者类,而且目标抽象类只能为接口,不能为类,其使用有一定的局限性,不能将一个适配者类和他的子类同时适配到目标接口。
六、责任链
在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员I必须记住每个领导的姓名、电话和地址等信息,这增加了难度。这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等。
缺点:
- 违背了开闭原则
- 产生耦合:处理器A一定要知道下一个是谁,明确知道下一个是谁。
- @return
*/
protected boolean hock(){
return true;
}
五、适配器模式
适配器模式:是一种结构型设计模式,它允许将不兼容的接口转换为可兼容的接口,以便不同的类之间能够协同工作。
思想:把一个类的接口编程客户端所期待的另一个接口,从而使原本的两个类能够在一起工作。
理解:
也就是说,当前系统存在两种接口A和B,客户只支持访问A接口,但是当前系统没有A接口对象,但是有B接口对象,但客户无法识别B接口,因此需要通过一个适配器C, 将B接口内容转换成A接口,从而使得客户能够从A接口获取得到B接口内容。在软件开发中,基本上任何问题都可以通过增加一个中间层进行解决。适配器模式其实就是一个中间层。综上,适配器模式其实起着转化/委托的作用,将-种接口转化为另-种符合需求的接口。
优点
- 优点:增加了类的透明性和复用性,使得适配器的灵活性更强。
- 缺点:一次最多只能适配一个适配者类,而且目标抽象类只能为接口,不能为类,其使用有一定的局限性,不能将一个适配者类和他的子类同时适配到目标接口。
六、责任链
在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员I必须记住每个领导的姓名、电话和地址等信息,这增加了难度。这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等。
缺点:
- 违背了开闭原则
- 产生耦合:处理器A一定要知道下一个是谁,明确知道下一个是谁。
- 如何解耦:将维护职责剥离出去!