三、设计模式
3.1 设计模式概述
3.1.1 设计模式概念
- 设计模式是对某些特定问题经过实践检验的特定解决方法
- 设计模式使得人们更加简单方便地复用成功的设计和体系结构
- 设计模式是可以复用的面向对象软件的基础
- 设计模式同城是指 GoF 设计模式,总结了 23 种经典的设计模式
3.1.2 设计模式的分类
设计模式有两种分类方式
1、根据目的划分,即设计模式用于完成何种工作来划分,可分为 创建型模式、结构型模式、行为型模式 3种
- 创建型模式:主要描述如何创建对象 ,其特点是将对象的创建与使用分离
- 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构
- 行为型模式:用于描述类或对象之间如何互相协作 ,共同完成单个对象无法独立完成的任务 ,即如何分配职责
2、根据作用范围划分,即设计模式主要作用于类上还是主要作用于对象上来划分,可分为 类模式、对象模式 2种
- 类模式:用于处理类与子类之间的关系,关系通过继承建立,是静态的,编译时便被确定下来了
- 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,运行时可变化,具有动态性
3.1.3 GoF 设计模式
GoF 设计模式的分类
范围\目的 | 创建型模式 | 结构型模式 | 行为型模式 |
---|---|---|---|
类模式 | 工厂方法 | (类)适配器 | 模板方法 解释器 |
对象模式 | 单例 原型 抽象工厂 建造者 | 代理 (对象)适配器 桥接 装饰 外观 享元 组合 | 策略 命令 职责链 状态 观察者 中介者 迭代器 访问者 备忘录 |
3.2 软件可复用问题和面向对象设计原则
软件开发和使用过程中,需求是经常变化的。面对这些变化,设计不足的软件往往难以修改甚至要重新设计
对于如何设计易于维护和扩展的软件系统,面向对象提出了几大原则
这些原则可以用来检验软件设计的合理性
3.2.1 单一职责原则
- 单一职责规定,一个类只负责一个职责,否则类应该被拆分
- 一个类不应该承担太多指责
- 一个类承担太多的职责的缺点
- 一个职责的变化可能会影响这个类实现其他职责的能力,或者引发其他职责故障
- 当客户需要该类的某一个职责时,不应该将其他不需要的职责全部包含进来,否则则会造成冗余或风险
3.2.2 开闭原则
- 开闭原则规定一个软件实体,如类、模块、函数,应该对扩展开放,对修改关闭
- 则程序需要进行拓展的时候,不能通过修改已有的代码实现变化
- 应该通过扩展软件实体的方式实现,如根据需求重新派生一个实现类
- 因为在软件的生命周期中,对原有的代码进行修改,可能会向原有代码中引入错误
- 并且原有代码修改后还要重新进行测试
3.3.3 里氏替换原则
- 里氏替换原则规定所有引用基类的地方必须能透明地使用其子类的对象
- 即所有使用基类代码的地方,如果换成子类对象还能够正常运行
- 里氏替换原则是面向对象设计的基本原则之一,是基础复用的基石
- 如果不能替换使用,则就是基础关系有问题,应取消原来的基础关系重新设计
3.3.4 依赖倒置原则
- 依赖于约定而不依赖于具体实现,即面向接口编程
- 对象的依赖关系有 3 种传递方式
- 通过构造方法传递依赖对象,即构造方法的参数是需要依赖的接口类型
- 通过 setter 方法传递依赖对象,即 setter 方法的参数是需要依赖的接口类型
- 接口声明依赖,即接口方法的参数是需要依赖的接口类型
- 依赖倒置原则是实现开闭原则的重要途径之一
3.3.5 接口隔离原则
- 接口隔离原则要求尽量将庞大臃肿的接口拆分成更小、更具体的接口,接口中只包含用户感兴趣的方法
- 一个类对另一个类的依赖应该建立在最小的接口上
- 为各个类建立他们需要的专用接口
- 接口隔离原则注重对接口的依赖隔离
- 接口隔离原则主要约束接口,针对抽象和程序整体框架的构建
3.3.6 迪米特法则
- 迪米特法则指一个软件实体应当尽可能少地与其他实体发生相互作用
- 被依赖的类应尽量将复杂逻辑封装在类的内部
- 不对外泄露任何中间信息,使客户对中间过程中的其他实体保持最少的了解
- 减少不必要的依赖,降低耦合
3.3.7 合成复用原则
- 合成复用原则指的是,尽量使用组合/聚合的方式,而不是继承关系达到软件复用的目的
- 继承复用是类型的复用,必须具备 is-a 关系才能通过继承的方式进行复用
- 并且从基类继承而来的实现是静态的,不可能在运行期间发送变化,因此缺乏足够的灵活性
- 合成复用是 has-a 关系,将已有对象纳入到新对象中使之成为新对象的一部分
- 因此新对象可以调用已有对象的功能
- 新对象可以在运行期间动态地引用与成分对象类型相同的实现
3.3 设计模式的应用
3.3.1 简单工厂模式
-
工厂方法的核心本质是
- 实例化对象不使用 new ,用工厂方法代替
- 选择实现类,创建对象统一管理和控制,从而将调用者跟我们的实现类解耦
-
简单工厂模式示例
- 例如我现在有一个名为 Animal 的接口
- 有两个 dog 和 cat 的实现类
- 现在我们需要一个工厂类,来返回这两个实现类的实例,而不是通过 new 关键字创建
- Animal 接口
javapublic interface Animal { void name(); }
- Dog 实现类
javapublic class Dog implements Animal{ @Override public void name() { System.out.println("一头狗"); } }
- Cat 实现类
javapublic class Cat implements Animal{ @Override public void name() { System.out.println("一匹猫"); } }
- Animal 工厂,用于创建接口的实现类
javapublic class AnimalFactory { public static Animal getAnimal(String name){ if (name.equals("Cat")){ return new Cat(); }else if (name.equals("Dog")){ return new Dog(); } return null; } }
- 测试类,返回 Cat 对象
javapublic class Test01 { public static void main(String[] args) { Animal cat = AnimalFactory.getAnimal("Cat"); cat.name(); } }
-
在示例当中,通过 AnimalFactory 工厂给定的参数,可以获取指定的 Animal 实例
-
以上示例可得知,简单工程模式包含如下角色
- 工厂(Factory):简单工厂模式的核心,负责实现创建所有示例的逻辑,工厂类提供静态方法,根据传入的参数创建所需的产品示例
- 抽象产品(Product):工厂创建的所有实例的父类型,是负责描述所有产品的公共接口,可以是接口或抽象类
- 具体产品(Concrete Product):抽象产品的实现类,是工厂的创建目标,工厂所创建的实例就是某个具体产品类的实例
-
使用工厂和抽象的父类产品,不需要关系具体的产品如何创建,内部如何变化
3.3.2 工厂方法模式
-
对于要创建的产品不多且不复杂的情况,可以采用简单工厂模式
-
但简单工厂并不适合逻辑比较复杂的情况,并且增加新的产品就需要修改工厂方法的逻辑判断
-
这与开闭原则相违背,而工厂方法模式 是对简单工厂模式的进一步抽象化
-
工厂方法模式的主要角色如下
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能
- 抽象工厂(Abstract Facory):提供了创建产品的接口,声明创建方法,该方法的返回值为抽象产品类型,调用者通过抽象工厂接口访问具体工厂的工厂方法来创建产品
- 具体产品(Concrete Product):实现了抽象产品所定义的接口,由具体工厂创建
- 具体工厂(Concrete Factory):实现了抽象工厂中的抽象创建方法,完成某个具体产品的创建,具体工厂和具体产品之间存在对应关系
-
工厂方法模式示例
- 继续以上面的案例为演示,这次要为每个动物都创建一个单独的工厂,并且要多创建出一个抽象工厂
- Animal 接口
javapublic interface Animal { void name(); }
- Dog 实现类
javapublic class Dog implements Animal{ @Override public void name() { System.out.println("一头狗"); } }
- Cat 实现类
javapublic class Cat implements Animal{ @Override public void name() { System.out.println("一匹猫"); } }
- Animal 抽象工厂
javapublic interface AnimalFactory { Animal getAnimal(); }
- Cat 实现类工厂
javapublic class CatFactory implements AnimalFactory{ @Override public Animal getAnimal() { return new Cat(); } }
- Dog 实现类工厂
javapublic class DogFactory implements AnimalFactory{ @Override public Animal getAnimal() { return new Dog(); } }
- 测试类
javapublic class Test02 { public static void main(String[] args) { Animal animal = new CatFactory().getAnimal(); animal.name(); Animal animal2 = new DogFactory().getAnimal(); animal2.name(); } }
-
工厂方法模式通过定义一个抽象工厂接口,将产品对象的实例创建工作推迟到具体工厂实现类中
-
工厂方法模式的优点
- 客户只需要知道具体工厂的名称就可以得到想要的产品,无须知道产品的具体创建过程
- 基于多态,便于对负责逻辑进行封装管理
- 在系统增加新的产品时,只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则
3.3.3 代理模式
生活中,我们经常听说房产中介,婚介,经纪人等,这些都是代理模式的实际体验
代理模式是单一职责原则的体现
-
代理模式包含如下角色
- **抽象主体(Subject):**通过接口或抽象类声明业务方法
- **真是主题(Real Subject):**实现了抽象主题中的具体业务,是实施代理的目标对象,即代理对象所代表的真实对象,是最终要引用的目标
- **代理(Proxy):**提供了与真实主题相同的接口,其内部含有对真实主题的引用,即一些附属操作
-
代理模式有多种方式,总体上分为 静态代理 和 动态代理
- **静态代理:**由开发者针对抽象主体编写的相关的代理类实现,编译之后生成代理类的 class 文件,代理关系在编译期就已经完成绑定
- **动态代理:**是在运行时动态产生的,编译完成后没有实际的代理类 class 文件,而是在运行时动态生成代理类字节码
静态代理
-
使用静态代理方式实现房产中介对买家的代理效果
- 定义抽象主体:买家业务接口
javapublic interface Rent { //看房 public void rent(); }
- 定义真实主题:房东业务实现
javapublic class Host implements Rent{ @Override public void rent() { System.out.println("看房去"); } }
- 定义代理,对 Rent 接口的 rent() 方法进行完善
javapublic class Proxy implements Rent{ private Host host; public Proxy(Host host) { this.host = host; } @Override public void rent() { before(); host.rent(); after(); } // 附属操作 public void before(){ System.out.println("前期准备"); } public void after(){ System.out.println("后期跟踪"); } }
- 测试类
javapublic class Test03 { public static void main(String[] args) { Rent rent1=new Proxy(new Host()); rent1.rent(); } }
-
如果抽象主体是一个没有实现任何接口的类,应该使用继承抽象主体对其代理
-
对没有实现接口的目标对象实现静态代理
- 继承 Host 并重写其中的业务方法,得到代理对象
javapublic class Proxy1 extends Host{ @Override public void rent() { before(); super.rent(); after(); } // 附属操作 public void before(){ System.out.println("前期准备"); } public void after(){ System.out.println("后期跟踪"); } }
- 测试类
javapublic class Test03 { public static void main(String[] args) { Host host1=new Proxy1(); host1.rent(); } }
-
静态代理的优点
- 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
- 公共也就就交给代理角色!实现了业务的分工!
- 公共业务发生扩展的时候,方便集中管理
动态代理
-
静态代理虽然简单直观,但是需要手动编写代理对象
- 如果被代理的目标对象中方法出现调整,要对方法进行代理就需要同时修改代理对象的定义
- 如果项目中有多个类需要代理,就要通过继承的方式为每个类定义代理对象,会非常麻烦
-
针对静态代理的问题,可以通过动态代理加以解决
-
动态代理是利用反射的机制在运行时生成代理类的字节码
-
可在运行时动态扩展对象行为的能力
-
JDK 动态代理的核心 API 是 java.lang.reflect 包下的 InvocationHandler 接口和 Proxy 类
-
InvocationHandler 接口是代理方法的调用处理程序,负责为代理方法提供业务逻辑
- Object invoke(Object proxy,Method method,Object [] args)
- 用于在代理实例上处理方法并且返回结果
- 简单来说就是负责调用方法
- 参数 proxy 是正在执行方法的代理对象
- 参数 method 是正在被调用的接口
- 参数 args 是传递给接口方法的参数
-
Proxy 类负责动态创建代理类及其实例
- static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
- 返回一个实现类指定接口的代理实例
- 对接口方法的调用会被指派到指定的调用处处理程序
- 参数 interfaces 是需要进行代理的接口类型
- 参数 h 是接口代理方法的调用处理程序
- 类加载器 loader 用来加载动态生成的代理类
JDK 动态代理 实例
-
定义一套增删改查的 service 来实现代理效果
- 定义抽象主题
javapublic interface UserService { void add(); void del(); void updata(); void query(); }
- 定义真实主题
javapackage Test04; public class UserServiceImpl implements UserService { @Override public void add() { System.out.println("执行添加"); } @Override public void del() { System.out.println("执行删除"); } @Override public void updata() { System.out.println("执行修改"); } @Override public void query() { System.out.println("执行查询"); } }
- 定义一个万能代理
javaimport java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class ProxyInvocationHlader implements InvocationHandler { private Object target; public void setTarget(Object target) { this.target = target; } public Object getTarget() { return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { method.invoke(target, args); return null; } }
- 测试类
javapublic class Test04 { public static void main(String[] args) { UserServiceImpl userServiceimpl=new UserServiceImpl(); ProxyInvocationHlader pih=new ProxyInvocationHlader(); pih.setTarget(userServiceimpl); UserService target = (UserService)pih.getTarget(); //执行添加操作 target.add(); } }