Java 框架基础技术之 03-设计模式
AI 摘要
设计模式概述
设计模式简介
设计模式是软件设计中的 “三十六计”,是人们在长期的软件开发中对一些经验的总结,是对某些特定问题经过时间检验的特定解决方法。
设计模式可以帮助开发者写出更清晰、更可维护的代码,更加简单方便地复用成功的设计和体系结构。
目前所说的设计模式通常是指由 4 位作者在一本书中总结的 23 种经典的设计模式,也被称为 GoF (Gang of Four,4 人组)设计模式。
设计模式分类
设计模式根据目的划分:
- 创建型模式:用于描述 “如何创建对象”。
- 结构型模式:用于描述 “如何将类或对象按照某种布局组成更大的结构”。
- 行为型模式:用于描述 “类或对象之间如何相互协作”。
设计模式根据作用范围划分:
- 类模式:用于处理 “类与子类之间的关系”。
- 对象模式:用于处理 “对象之间的关系”。
GoF 设计模式的分类:
| 范围 | 创建型模式 | 结构型模式 | 行为型模式 |
|---|---|---|---|
| 类模式 | 工厂方法 | (类)适配器 | 模板方法 解释器 |
| 对象模式 | 单例 原型 抽象工厂 建造者 | 代理 (对象)适配器 桥接 装饰 外观 享元 组合 | 策略 命令 职责链 状态 观察者 中介者 迭代器 访问者 备忘录 |
软件设计原则
软件可复用问题
大多数的软件应用是由多个类通过彼此合作,如三层架构 Service 层依赖 Dao 层,才能实现完整的功能,对于组件化开发的软件来说,组件之间会存在各种依赖关系。
示例:软件可复用问题
新闻数据访问接口(cn.duozai.dao.NewsDao):
public interface NewsDao {
/**
* 新增新闻
*
* @return void
*/
void save();
}新闻数据访问接口 MySQL 实现类(cn.duozai.dao.NewsDaoMySQLImpl):
public class NewsDaoMySQLImpl implements NewsDao {
/**
* 新增新闻
*
* @return void
*/
@Override
public void save() {
System.out.println("在MySQL中添加新闻");
}
}新闻数据访问接口 SQLServer 实现类(cn.duozai.dao.NewsDaoSQLServerImpl):
public class NewsDaoSQLServerImpl implements NewsDao {
/**
* 新增新闻
*
* @return void
*/
@Override
public void save() {
System.out.println("在SQLServer中添加新闻");
}
}新闻业务逻辑接口(cn.duozai.service.NewsService):
public interface NewsService {
/**
* 新增新闻
*
* @return void
*/
void save();
}新闻业务逻辑接口实现类(cn.duozai.service.NewsServiceImpl):
public class NewsServiceImpl implements NewsService {
/**
* 新增新闻
*
* @return void
*/
@Override
public void save() {
// Service层依赖Dao层
// Service层对Dao层强依赖,Dao层需要变更,Service层对应的位置也要变更
NewsDao newsDao = new NewsDaoMySQLImpl();
// 调用Dao层方法
newsDao.save();
}
}Service 层与 Dao 层高度耦合,类与类之间的依赖关系增加了程序开发的复杂程度,一个类的变更,可能会对正在使用该类的所有类产生影响,这就是软件可复用问题。
面向对象设计原则
对于如何设计易于维护和扩展的软件系统,面向对象的软件设计提出了几大原则。
单一职责原则:
- 一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。
- A 类负责存储员工基本信息,B 类负责计算员工薪资,C 类负责登记员工考勤,而不能混在一起。
- 拆分粒度视情况而定,有时候并不需要拆分得太细。
开闭原则:
- 软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。
- A 类最开始支持绘制圆形和长方形,当需要添加绘制三角形的功能时,不能修改原有绘制圆形和长方形的代码,而是创建一个新的 B 类来扩展功能。
里氏替换原则:
- 子类应该能够替换它们的父类而不会破坏程序的正确性。
- 子类应该尽量不重写父类已实现的方法,或者在重写时保证方法的行为不发生改变,使得在运行时,用子类对象替换父类对象后,程序的行为和预期一致。
- 有一个父类 “动物”,有一个方法 “移动“,子类 “狗” 继承自 “动物”,如果 “狗” 重写了 “移动” 方法,但行为和父类的 “移动” 方法完全不同,可能就会导致在一些依赖 “动物” 类的代码中出现问题,就违反了里氏替换原则。
依赖倒置原则:
- 依赖于约定而不依赖于实现,即面向接口编程。
- 假定要盖一座房子,有房屋设计师(高层模块)和而各种建筑材料和工人(低层模块),在不遵循依赖倒置原则,房屋设计师可能会直接指定要用某个品牌的砖头、某个厂家的水泥等具体材料,当这些材料供应不上或者质量出问题了,设计师就得重新设计整个房屋的建造方案。
- 在遵循依赖倒置原则时,房屋设计师只规定房子需要什么样的功能,如需要能承重的材料、能防水的材料等,而不具体指定是哪个品牌的材料,此时不管市场上的材料怎么变化,只要有能满足规定功能的材料出现,就可以直接用,而不需要改变设计方案。
- 高层模块(设计师)不要直接依赖低层模块(具体的材料和工人),而是要依赖抽象(材料的功能要求),当低层模块有变化时,高层模块不会受到太大影响,保证了系统的稳定性和可扩展性。
接口隔离原则:
- 尽量将庞大臃肿的接口拆分成更小、更具体的接口,接口应该尽可能小,只包含客户端需要的方法。
- 有一个动物行为的接口,里面有吃、睡、飞、跑等各种方法,如果有一个只需要会跑的动物(比如马)来实现这个接口,那么它就被迫实现了飞等它不需要的方法。
- 遵循接口隔离原则的情况下,会跑的动物实现一个 “跑的行为接口”,会飞的动物实现一个 “飞的行为接口”,这样每个类只依赖它真正需要的接口,代码更加清晰和易于维护。
迪米特法则:
- 也称最少知识原则,一个软件实体应当尽可能少地与其他实体发生相互作用。
- 员工、部门经理、公司领导只和直接相关的对象交流,使得办公室的管理更加清晰,职责明确,降低了整体的复杂性和耦合度。
合成复用原则:
- 尽量使用组合/聚合的方式,而不是继承关系达到软件复用的目的。
- 假定需要一个既可以当书桌又可以当床的家具,在不遵循合成复用原则的情况下可以让一个现有的床制造商生产一种特殊的床,这种床继承了普通床的功能,同时又新增了书桌的功能。
- 在遵循合成复用原则的情况下,可以分别购买一张普通的床和一个独立的书桌,然后根据需求将它们摆放在房间里,组合使用。
简单工厂模式应用
简单工厂模式概述
Service 层与 Dao 层高度耦合的问题,可以使用简单工厂模式来解耦。
示例:简单工厂模式
新闻数据访问层工厂(cn.duozai.factory.NewsDaoFactory):
public class NewsDaoFactory {
/**
* 生产新闻数据访问层实现类
*
* @return 新闻数据访问层实现类
*/
public static NewsDao getInstance() {
// 需要哪个实现类,就返回哪个实现类
return new NewsDaoSQLServerImpl();
}
}新闻业务逻辑接口实现类(cn.duozai.service.NewsServiceImpl):
public class NewsServiceImpl implements NewsService {
/**
* 新增新闻
*
* @return void
*/
@Override
public void save() {
// 通过工厂提供的静态方法对Dao和Service进行解耦工作
// 调用工厂获得Dao对象
NewsDao newsDao = NewsDaoFactory.getInstance();
// 调用Dao层方法
newsDao.save();
}
}测试类(cn.duozai.TestMain):
public class TestMain {
public static void main(String[] args) {
// 调用Service层添加新闻
NewsService newsService = new NewsServiceImpl();
newsService.save();
}
}示例效果:
简单工厂 + 依赖倒置
使用简单工厂模式,可以解决 Service 层与 Dao 层高度耦合的问题。
此时 Service 层需要直接调用工厂类的方法来获取 Dao 对象,Service 层与工厂类在代码层面上有直接的依赖关系,当工厂类的名称、方法或者创建对象的逻辑发生变化,Service 层也需要相应地进行修改,Service 层与工厂类高度耦合。
解决 Service 层与工厂类高度耦合的问题,可以使用依赖倒置原则。
在依赖倒置原则的要求下,Service 层只依赖于 Dao 接口,而不依赖于具体的工厂实现类。当需要更换 Dao 实现时,只需要提供新的实现类满足接口约定即可,而不需要修改 Service 层的代码,从而实现了解耦,遵循了依赖倒置原则。
示例:简单工厂模式 + 依赖倒置原则
新闻业务逻辑接口实现类(cn.duozai.service.NewsServiceImpl):
public class NewsServiceImpl implements NewsService {
// 依赖倒置原则
// Service层依赖Dao接口,不依赖工厂或任何实现类
// 而是依赖某一个具体的接口
NewsDao newsDao;
public NewsServiceImpl(NewsDao newsDao) {
this.newsDao = newsDao;
}
/**
* 新增新闻
*
* @return void
*/
@Override
public void save() {
// 调用Dao层方法
newsDao.save();
}
}测试类(cn.duozai.TestMain):
public class TestMain {
public static void main(String[] args) {
// 通过工厂获取新闻Dao实现类
NewsDao newsDao = NewsDaoFactory.getInstance();
// 在创建新闻Service层时,将Dao实现类传入
NewsService newsService = new NewsServiceImpl(newsDao);
newsService.save();
}
}简单工厂 + 参数优化
在简单工厂模式中,可以根据不同的参数返回不同类的实例。
示例:简单工厂模式 + 参数优化
新闻数据访问层工厂(cn.duozai.factory.NewsDaoFactory):
public class NewsDaoFactory {
/**
* 生产新闻数据访问层实现类
*
* @param type 数据库类型
* @return 新闻数据访问层实现类
*/
public static NewsDao getInstance(String type) throws Exception {
// 根据数据库类型返回对应的实现类
if(type.equals("MySQL")) {
return new NewsDaoMySQLImpl();
} else if(type.equals("SQLServer")) {
return new NewsDaoSQLServerImpl();
} else {
throw new Exception("未知的数据库类型");
}
}
}测试类(cn.duozai.TestMain):
public class TestMain {
public static void main(String[] args) throws Exception {
// 通过工厂获取新闻Dao实现类
NewsDao newsDao = NewsDaoFactory.getInstance("MySQL");
// 在创建新闻Service层时,将Dao实现类传入
NewsService newsService = new NewsServiceImpl(newsDao);
newsService.save();
}
}简单工厂模式小结
简单工厂模式,又叫做静态工厂方法模式,不属于 GoF 的 23 种设计模式之一,简单工厂模式是工厂模式家族中最简单的一种设计模式,可以理解为工厂模式的一个特殊实现。
简单工厂模式包含的角色:
- 工厂:简单工厂模式的核心,提供静态方法,根据需要创建所需的产品实例。
- 抽象产品:工厂创建的所有实例的父类型,可以是接口或抽象类。
- 具体产品:抽象产品的实现类,是工厂的创建目标。
对于要创建的产品不多且逻辑不复杂的情况下,可以考虑简单工厂模式。复杂的产品逻辑会导致工厂方法模式难以维护,并且增加新的产品就需要修改工厂方法的判断逻辑,与开闭原则相违背。
工厂方法模式应用
工厂方法模式概述
工厂方法模式是 GoF 模式中的其中一种设计模式,是对简单工厂模式的进一步抽象化。
工厂方法模式包含的角色:
- 抽象产品:工厂创建的所有实例的父类型,可以是接口或抽象类。
- 具体产品:抽象产品的实现类,是工厂的创建目标。
- 抽象工厂:提供了创建产品的接口,声明创建产品的方法。
- 具体工厂:实现抽象工厂中的创建产品的方法,完成某个具体产品的创建。
工厂方法模式和简单工厂模式的区别:
- 在简单工厂的模式下,厨师(工厂)需要生产好吃的食品(抽象产品),当出现新品类的食品(具体产品)时,厨师(工厂)就要去学习如何制作新品。
- 在工厂方法的模式下,餐厅有多个会做食品(抽象产品)的厨师(抽象工厂),闽菜厨师(具体工厂)会做闽菜(具体产品),当餐厅需要增加川菜食品时,招募川菜厨师(具体工厂)即可。
示例:工厂方法模式
新闻数据访问层抽象工厂(cn.duozai.factory.AbstractFactory):
public interface AbstractFactory {
/**
* 生产新闻数据访问层实现类
*
* @return 新闻数据访问层实现类
*/
NewsDao getInstance();
}新闻数据访问层 MySQL 具体工厂(cn.duozai.factory.NewsDaoMySQLFactory):
public class NewsDaoMySQLFactory implements AbstractFactory {
/**
* 生产新闻数据访问层MySQL实现类
*
* @return 新闻数据访问层实现类
*/
@Override
public NewsDao getInstance() {
return new NewsDaoMySQLImpl();
}
}新闻数据访问层 SQLServer 具体工厂(cn.duozai.factory.NewsDaoSQLServerFactory):
public class NewsDaoSQLServerFactory implements AbstractFactory {
/**
* 生产新闻数据访问层SQLServer实现类
*
* @return 新闻数据访问层实现类
*/
@Override
public NewsDao getInstance() {
return new NewsDaoSQLServerImpl();
}
}测试类(cn.duozai.TestMain):
public class TestMain {
public static void main(String[] args) throws Exception {
// 创建具体工厂
AbstractFactory newsDaoMySQLFactory = new NewsDaoMySQLFactory();
// 创建新闻DAO层接口实现类
NewsDao newsDao = newsDaoMySQLFactory.getInstance();
// 在创建新闻Service层时,将Dao实现类传入
NewsService newsService = new NewsServiceImpl(newsDao);
newsService.save();
}
}工厂方法模式小结
工厂方法模式通过定义一个抽象工厂接口,将产品对象的实际创建工作推迟到具体工厂实现类中。
工厂方法模式的优缺点:
- 客户只需要知道具体工厂的名称就可以得到所要的产品,无需知道产品的具体创建过程。
- 基于多态,便于对复杂逻辑进行封装管理。
- 在系统增加新的产品时,只需要添加具体产品类和对应的具体工厂类,无需对原有工厂进行任何修改,满足开闭原则。
- 工厂方法模式下,每增加一个产品,就要增加一个具体产品类和一个对应的具体工厂类,增加了系统的复杂度。
- 工厂方法模式适用于产品种类较多,创建逻辑比较复杂的情况。
代理模式应用
代理模式概述
代理模式是一种结构型设计模式,就像是在实际对象和使用者之间的一个中间层,使用者不直接与实际对象交互,而是通过代理对象来与实际对象进行间接交互。
代理模式包含的角色:
- 抽象主题:通过接口或抽象类声明业务方法,如:无论是自己租房还是找中介租房,最终都要租到房子。
- 真实主题:实现了抽象主题中的具体业务,是实施代理的目标对象,如:要租房子的自己。
- 代理(Proxy):提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能,如:中介带自己看房子、帮自己租房子。
实现代理模式的方式:
- 静态代理:由程序员创建或特定工具生成代理类的代码,再对其编译,在编译前代理类和委托类的关系就已经确定了。
- 动态代理:在程序运行时,通过反射机制动态地生成代理类的字节码,并加载到 JVM 中,常见的有 JDK 动态代理和 CGLIB 动态代理。
基于接口的静态代理实现
基于接口,可以实现静态代理。
示例:基于接口的静态代理实现
租房服务接口(cn.duozai.RentService):
public interface RentService {
/**
* 租房
*
* @return void
*/
void rent();
}有钱人业务实现类(cn.duozai.User):
public class User implements RentService {
/**
* 租房
*
* @return void
*/
@Override
public void rent() {
System.out.println("有钱人掏钱租房");
}
}测试类(cn.duozai.TestMain):
public class TestMain {
public static void main(String[] args) {
// 创建对象-张三
User zhangsan = new User();
// 张三自己租房
zhangsan.rent();
}
}中介业务实现类(cn.duozai.Agent):
public class Agent implements RentService {
// 中介Agent帮有钱人User租房
User target;
// 要帮哪个对象租房,就传入具体的对象
public Agent(User target) {
this.target = target;
}
/**
* 租房
*
* @return void
*/
@Override
public void rent() {
// 有钱人租房前后,中介做额外的操作
System.out.println("中介开始代理租房");
// 有钱人掏钱租房
target.rent();
System.out.println("中介提供后期服务");
}
}测试类(cn.duozai.TestMain):
public class TestMain {
public static void main(String[] args) {
// 创建对象-张三
User zhangsan = new User();
// 张三自己租房
// zhangsan.rent();
// 张三让中介帮忙租房
Agent lisi = new Agent(zhangsan);
lisi.rent();
}
}示例效果:
基于继承的静态代理实现
对于没有实现接口的目标对象,基于继承也可以实现静态代理。
示例:基于继承的静态代理实现
中介业务实现类(cn.duozai.Agent):
public class Agent extends User {
/**
* 租房(重写父类方法)
*
* @return void
*/
@Override
public void rent() {
// 有钱人租房前后,中介做额外的操作
System.out.println("中介开始代理租房");
// 有钱人掏钱租房
// 调用父类的租房方法
super.rent();
System.out.println("中介提供后期服务");
}
}测试类(cn.duozai.TestMain):
public class TestMain {
public static void main(String[] args) {
// 张三让中介帮忙租房
User zhangsan = new Agent();
zhangsan.rent();
}
}静态代理模式小结
代理模式可以对目标对象的功能进行扩展,目标对象和扩展功能职责清晰,且不会产生耦合。
在静态代理中,代理类必须在编译期就确定要代理的具体目标对象,如果需要代理不同类型的对象,就需要为每种类型的对象都编写一个对应的代理类,这使得代码的可维护性和可扩展性受到限制。
随着业务的发展,可能会有越来越多的不同类型的有钱人和不同的租房场景出现,在静态代理中,每一种情况都需要单独编写一个代理类,这会导致代码量迅速增加,维护起来变得困难。
代理的需求发生变化时,需要添加新的代理逻辑或者修改现有的代理逻辑,就需要修改代理类的代码,不符合开闭原则。
JDK 动态代理
JDK 动态代理是从 JDK 1.8 版本就已经引入的特性,其利用反射机制在运行时生成代理类的字节码,为 Java 平台带来了运行时动态扩展对象行为的能力。
JDK 动态代理的核心 API:反射包下的 InvocationHandler 接口和 Proxy 类。
- InvocationHandler 接口是代理方法的调用处理程序,负责为代理方法提供业务逻辑。
- Proxy 类负责动态创建代理类及其实例。
示例:JDK 动态代理
租房服务接口(cn.duozai.RentService):
public interface RentService {
/**
* 租房
*
* @return void
*/
void rent();
}有钱人业务实现类(cn.duozai.User):
public class User implements RentService {
/**
* 租房
*
* @return void
*/
@Override
public void rent() {
System.out.println("有钱人掏钱租房");
}
}中介公司处理类(cn.duozai.AgentProxyHandler):
public class AgentProxyHandler implements InvocationHandler {
// 中介公司要帮某一个对象(具体对象不知道是谁)租房子
// 即被代理的目标对象
private Object target;
public void setTarget(Object target) {
this.target = target;
}
/**
* 中介公司生成代理
*
* @param proxy 动态生成的代理对象本身
* @param method 要代理的方法(即代理租房子)
* @param args 租房子方法的参数
* @return 方法返回值对象
*/
@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;
}
}测试类(cn.duozai.TestMain):
public class TestMain {
public static void main(String[] args) {
// 张三要租房,创建张三对象
User zhangsan = new User();
// 创建中介公司
AgentProxyHandler agentProxyHandler = new AgentProxyHandler();
// 告诉中介公司张三要租房
agentProxyHandler.setTarget(zhangsan);
// 让中介公司创建中介对象为张三服务
// 参数1:当前目标对象的类加载器(张三)
// 参数2:目标对象实现的接口(租房服务)
// 参数3:动态代理类,用来处理代理逻辑(中介公司)
RentService lisi = (RentService) Proxy.newProxyInstance(User.class.getClassLoader(), User.class.getInterfaces(), agentProxyHandler);
// 中介帮忙租房
lisi.rent();
}
}示例效果:
JDK 动态代理是面向接口的代理实现,要求被代理的目标对象必须通过抽象主题接口进行定义,否则无法实施代理。
CGLIB 动态代理
CGLIB 是一个功能强大、高性能的代码生成库,可以为没有实现接口的类提供代理,与 JDK 动态代理很好地形成互补。
使用 CGLIB 动态代理,需要先下载并导入 CGLIB 的依赖 jar 包和 ASM 组件的依赖 jar 包。
cglib-nodep 下载:https://github.com/cglib/cglib/releases
CGLIB 动态代理的核心 API:MethodInterceptor 接口和 Enhancer 类。
- MethodInterceptor 接口的实现类负责拦截父类的方法调用。
- Enhancer 类负责动态创建代理类及其实例。
CGLIB 依赖于 Java 的反射机制来动态生成和操作类,当 CGLIB 尝试通过反射调用 ClassLoader.defineClass 方法时,由于 JDK 17 引入了模块系统及更严格的访问控制策略,会导致这个调用被拒绝。
示例:CGLIB 动态代理
导入相关依赖 JAR 文件,并添加为库。
示例效果:
有钱人业务实现类(cn.duozai.User):
public class User {
/**
* 租房
*
* @return void
*/
public void rent() {
System.out.println("有钱人掏钱租房");
}
}中介公司规则类(cn.duozai.AgentInterceptor):
public class AgentInterceptor implements MethodInterceptor {
/**
* 中介代理规则
*
* @param o 被代理的目标对象实例(要租房子的人)
* @param method 被拦截的方法
* @param objects 传递给方法的参数
* @param methodProxy 父类中的原始方法
* @return 方法返回值对象
* @author 多仔ヾ
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("中介开始代理租房");
// 目标对象租房
// 通过反射让父类执行租房方法
Object result = methodProxy.invokeSuper(o, objects);
System.out.println("中介提供后期服务");
return result;
}
}测试类(cn.duozai.TestMain):
public class TestMain {
public static void main(String[] args) {
// 动态创建代理类及其实例
Enhancer enhancer = new Enhancer();
// 设置中介代理的父类
enhancer.setSuperclass(User.class);
// 设置中介代理的生成规则
enhancer.setCallback(new AgentInterceptor());
// 创建中介代理
User user = (User) enhancer.create();
user.rent();
}
}动态代理模式小结
动态代理模式是一种在运行时动态地创建代理对象来控制对目标对象的访问,并在不修改目标对象代码的情况下为目标对象添加额外功能的设计模式。
JDK 和 CGLIB 动态代理对比:
- JDK 动态代理面向接口代理,只能对基于接口设计的目标对象进行代理。
- CGLIB 动态代理可以通过继承方式实现,不依赖接口,但是不能代理 final 的类和方法。