设计模式
设计模式
一、适配器模式
我们想用苹果的充电线给安卓的手机充电时。由于两者的接口不一样,会导致充电口无法匹配。这时,我们就需要适配器,将安卓的充电接口转化为苹果的接口,这样,就可以进行充电了。
适配器模式定义
适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
在适配器模式中,存在两种不同的模式结构:类的适配器模式和对象的适配器模式
对象的适配器模式
我们以上面的苹果充电线给安卓手机充电的为例,如果是你来写代码,把他们进行兼容适配,你会怎么写呢?
对于这个问题,我们首先先来看一下适配器模式中涉及到的三个角色:
1、Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。例如对于上面的第二个例子,客户端的目标是要给只接受安卓充电口的安卓手机充电,所以我们的目标抽象类就是安卓充电线的接口。
2、Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法。例如上面苹果充电线就是适配者类。
3、Adapter(适配器类):通过包装一个需要适配的对象,把原接口转换成目标接口。例如为了可以充电,我们需要一个适配器,使之一边可以连接安卓充电的接口,一边可以连接苹果充电线的接口。
UML图
下面我们用代码来做个实例:
(1)Target类
public class Android {
public void isAndroid(){
System.out.println("这是一个只接受安卓充电线的插口");
}
}
(2)Adaptee类
public class Iphone {
public void isIphone(){
System.out.println("这是一个适配苹果充电线的插口");
}
}
(3)Adapter类:把他们进行适配
/**
* 适配器,作为中间件,把他们进行适配
*/
public class Adapter extends Android{
private Iphone iphone;
public Adapter(Iphone iphone){
this.iphone = iphone;
}
@Override
public void isAndroid() {
iphone.isIphone();
}
}
(4)测试类
public class Demo {
public static void main(String[] args){
Android android = new Adapter(new Iphone());
//调用的是安卓的接口,但实际上
//确实一个可以接受苹果充电器的接口
android.isAndroid();
}
}
(5)打印结果
这是一个适配苹果充电线的插口
我们通过适配器的作用,就可以让苹果充电线给安卓手机充电了。。
对于这种对象的适配器模式,实际上就是通过一个适配器类,把目标类和需要被适配的类进行组合。所以适配器类Adapter一般需要继承或实现Target,并且还得持有Adaptee的实例引用。
public class Adapter extends Android{ //继承或实现Target
private Iphone iphone;
public Adapter(Iphone iphone){
this.iphone = iphone;
}
@Override
public void isAndroid() {
iphone.isIphone();//持有Adaptee的实例引用
}
}
类的适配器模式
除了对象的适配器模式,还有另外一种类的适配器模式。在这种模式中,Adapter不持有Adaptee的实例引用,而是直接继承Adaptee类,然后再实现Target接口。或者直接继承Adaptee类和Target类,但由于Java中不支持多重继承,所以只能用实现Target的方式。
这也导致了在Java类的适配器模式中,Target必须是一个接口而不可以是抽象类和具体类(因为Java不支持多重继承)。
把上面那个例子改写一下,用类的适配器模式来实现:
(1)Target接口类
interface Android {
void isAndroid();
}
(2)Adaptee类
public class Iphone {
public void isIphone(){
System.out.println("这是一个适配苹果充电线的接口");
}
}
(3).Adapter类:继承Adaptee,实现Target
/**
* 适配器,把安卓手机的插口转化为可以用苹果充电线充电的接口
*/
public class Adapter extends Iphone implements Android{
@Override
public void isAndroid() {
//直接调用
isIphone();
}
}
(4).测试类
public class Demo {
public static void main(String[] args){
Android android = new Adapter();
android.isAndroid();
}
}
(5).打印结果
这是一个适配苹果充电线的接口
对于这种类的适配器模式,在Java中是使用的比较少的。
这两种方式最重要的区别就是:
对象适配器模式通过组合来实现适配器功能,而类的适配器模式通过多继承或实现来实现适配器功能。
适配器模式的一些优缺点
感觉,优缺点没什么好说的,只有实际上去用过才能体会。反正最重要的优点就是将不兼容的几个接口通过一个中间类,把他们进行适配,并且这个适配的过程对于客户端来说是透明的,客户端并不知道发生了啥,只知道它通过一个适配器,就可以获取到目标接口了。
至于缺点嘛,如果动不动就使用适配器,那么我觉得会使整个类系统变的有点零散,并且以后要重构了会更加复杂......
二、代理模式
定义:为其他对象提供一种代理以控制对这个对象的访问
代理模式的通用类图
上图中,Subject是一个抽象类或者接口,RealSubject是实现方法类,具体的业务执行,Proxy则是RealSubject的代理,直接和client接触的。
代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。
代理模式优点
- 职责清晰
- 高扩展,只要实现了接口,都可以用代理。
- 智能化,动态代理。
分类
1、静态代理
以租房为例,我们一般用租房软件、找中介或者找房东。这里的中介就是代理者。
首先定义一个提供了租房方法的接口。
public interface IRentHouse {
void rentHouse();
}
定义租房的实现类
public class RentHouse implements IRentHouse {
@Override
public void rentHouse() {
System.out.println("租了一间房子。。。");
}
}
我要租房,房源都在中介手中,所以找中介
public class IntermediaryProxy implements IRentHouse {
private IRentHouse rentHouse;
public IntermediaryProxy(IRentHouse irentHouse){
rentHouse = irentHouse;
}
@Override
public void rentHouse() {
System.out.println("交中介费");
rentHouse.rentHouse();
System.out.println("中介负责维修管理");
}
}
这里中介也实现了租房的接口。
在main方法中测试
public class Main {
public static void main(String[] args){
//定义租房
IRentHouse rentHouse = new RentHouse();
//定义中介
IRentHouse intermediary = new IntermediaryProxy(rentHouse);
//中介租房
intermediary.rentHouse();
}
}
返回信息
交中介费
租了一间房子。。。
中介负责维修管理
这就是静态代理,因为中介这个代理类已经事先写好了,只负责代理租房业务
2、强制代理
如果我们直接找房东要租房,房东会说我把房子委托给中介了,你找中介去租吧。这样我们就又要交一部分中介费了,真坑。
来看代码如何实现,定义一个租房接口,增加一个方法。
public interface IRentHouse {
void rentHouse();
IRentHouse getProxy();//获取代理
}
这时中介的方法也稍微做一下修改
public class IntermediaryProxy implements IRentHouse {
private IRentHouse rentHouse;
public IntermediaryProxy(IRentHouse irentHouse){
rentHouse = irentHouse;
}
@Override
public void rentHouse() {
rentHouse.rentHouse();
}
//实现获取代理
@Override
public IRentHouse getProxy() {
return this;//返回中介的代理类对象
}
}
其中的getProxy()方法返回中介的代理类对象
我们再来看房东是如何实现租房:
public class LandLord implements IRentHouse {
private IRentHouse iRentHouse = null;
@Override
public void rentHouse() {
if (isProxy()){
System.out.println("租了一间房子。。。");
}else {
System.out.println("请找中介");
}
}
@Override
public IRentHouse getProxy() {
iRentHouse = new IntermediaryProxy(this);
return iRentHouse;
}
/**
* 校验是否是代理访问
* @return
*/
private boolean isProxy(){
if(this.iRentHouse == null){
return false;
}else{
return true;
}
}
}
房东的getProxy方法返回的是代理类,然后判断租房方法的调用者是否是中介,不是中介就不租房。
main方法测试:
public static void main(String[] args){
IRentHouse iRentHosue = new LandLord();
//租客找房东租房
iRentHouse.rentHouse();
//找中介租房
IRentHouse rentHouse = iRentHouse.getProxy();
rentHouse.rentHouse();
}
}
输出结果:
请找中介
租了一间房子。。。
看,这样就是强制你使用代理,如果不是代理就没法访问。
3、动态代理
我们知道现在的中介不仅仅是有租房业务,同时还有卖房、家政、维修等得业务,只是我们就不能对每一个业务都增加一个代理,就要提供通用的代理方法,这就要通过动态代理来实现了。
中介的代理方法做了一下修改
public class IntermediaryProxy implements InvocationHandler {
private Object obj;
public IntermediaryProxy(Object object){
obj = object;
}
/**
* 调用被代理的方法
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(this.obj, args);
return result;
}
}
在这里实现InvocationHandler接口,此接口是JDK提供的动态代理接口,对被代理的方法提供代理。其中invoke方法是接口InvocationHandler定义必须实现的, 它完成对真实方法的调用。动态代理是根据被代理的接口生成所有的方法,也就是说给定一个接口,动态代理就会实现接口下所有的方法。通过 InvocationHandler接口, 所有方法都由该Handler来进行处理, 即所有被代理的方法都由 InvocationHandler接管实际的处理任务。
这里增加一个卖房的业务,代码和租房代码类似。
main方法测试:
public static void main(String[] args){
IRentHouse rentHouse = new RentHouse();
//定义一个handler
InvocationHandler handler = new IntermediaryProxy(rentHouse);
//获得类的class loader
ClassLoader cl = rentHouse.getClass().getClassLoader();
//动态产生一个代理者
IRentHouse proxy = (IRentHouse) Proxy.newProxyInstance(cl, new Class[]{IRentHouse.class}, handler);
proxy.rentHouse();
ISellHouse sellHouse = new SellHouse();
InvocationHandler handler1 = new IntermediaryProxy(sellHouse);
ClassLoader classLoader = sellHouse.getClass().getClassLoader();
ISellHouse proxy1 = (ISellHouse) Proxy.newProxyInstance(classLoader, new Class[]{ISellHouse.class}, handler1);
proxy1.sellHouse();
}
输出:
租了一间房子。。。
买了一间房子。。。
在main方法中我们用到了Proxy这个类的方法,
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
loder:类加载器,interfaces:代码要用来代理的接口, h:一个 InvocationHandler 对象 。
InvocationHandler 是一个接口,每个代理的实例都有一个与之关联的 InvocationHandler 实现类,如果代理的方法被调用,那么代理便会通知和转发给内部的 InvocationHandler 实现类,由它决定处理。
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
InvocationHandler 内部只是一个 invoke() 方法,正是这个方法决定了怎么样处理代理传递过来的方法调用。
因为,Proxy 动态产生的代理会调用 InvocationHandler 实现类,所以 InvocationHandler 是实际执行者。
总结
- 静态代理,代理类需要自己编写代码写成。
- 动态代理,代理类通过 Proxy.newInstance() 方法生成。
- JDK实现的代理中不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,它们的实质是面向接口编程。CGLib可以不需要接口。
- 动态代理通过 Proxy 动态生成 proxy class,但是它也指定了一个 InvocationHandler 的实现类。
三、组合模式
正文
我们还是老规矩,用一个具体案例开始我们的设计模式之旅
假如有一个公司,要统计这个公司里所有员工的年龄总和
我们先来看一下公司的人员组成结构
部门A下面有两个员工和一个部门B。部门B下面有两个员工
图中画的只有两个部门,每个部门下两个员工。实际情况是一个公司有很多部门,每个部门下有很多员工
想象一下,如果让某个人去统计全公司所有人的年龄的话,那对这个人来说工作量太大了
我们可以采取这样的方式,让每个部门去统计各自部门下的员工年龄的总和,然后再一层一层往上汇报
比如,部门B先统计它部门下员工的年龄总和上报给部门A;部门A再统计它部门下的直属员工的年龄加上部门B的员工年龄,汇总以后再向上汇报
根据以上思路可以抽象出我们的代码结构
一个公司接口类 Company
,里面定义一个 getAge()
方法,用来获取员工年龄
然后是员工类 Employee
和部门类 Department
,分别实现公司接口类,然后重写 getAge()
方法
下面开始写代码
首先是公司接口类
public interface Company {
int getAge();
}
员工类,这里只放员工A1的代码实现,其他员工的代码和这个一样
public class EmployeeA1 implements Company {
private int age;
public EmployeeA1(int age) {
this.age = age;
}
@Override
public int getAge() {
return this.age;
}
}
部门类,这里只放部门A的代码实现,部门B和这个一样
public class DepartmentA implements Company {
private List<Company> list = new ArrayList<>();
private String name;
public void add(Company company) {
list.add(company);
}
@Override
public int getAge() { // 统计部门下所有员工的年龄总和
int age = 0;
for (Company company : list) {
age = age + company.getAge();
}
return age;
}
}
在客户端调用之前要把公司的人员结构和年龄设置好
public static Company getCompany() {
EmployeeA1 employeeA1 = new EmployeeA1(25); // 部门A下的1号员工
EmployeeA2 employeeA2 = new EmployeeA2(26); // 部门A下的2号员工
EmployeeB1 employeeB1= new EmployeeB1(30); // 部门B下的1号员工
EmployeeB2 employeeB2= new EmployeeB2(30); // 部门B下的2号员工
DepartmentB departmentB = new DepartmentB(); // 组装部门B
departmentB.add(employeeB1);
departmentB.add(employeeB2);
DepartmentA departmentA = new DepartmentA(); // 组装部门A
departmentA.add(employeeA1);
departmentA.add(employeeA2);
departmentA.add(departmentB);// 部门B也属于部门A
return departmentA;
}
下面开始统计公司员工的年龄
Company company = getCompany(); // 获取公司的人员架构
System.out.println(company.getAge()); // 统计年龄
最后输出的结果是 111
,公司员工的年龄总和就统计出来了
其实,这就是组合模式一个具体实现
组合模式基本介绍
组合模式在特定场景下还是比较实用的,在创建型模式、结构型模式和行为型模式分类中,组合模式归属于结构型模式
组合模式也叫部分-整体模式、合成模式或对象树
如果要实现的功能的结构可以被抽象成树状结构,就非常适合使用组合模式
在我们上文例子中,员工就是树状结构的叶子节点,部门就是树状结构的树枝节点
组合模式的结构如下图
组合模式的实现方式主要分为三步
- 定义一个接口,并在接口中定义要实现的功能
- 叶子节点实现这个接口,并重写接口中的方法
- 树枝节点中有一个集合或者数组,可以对接口对象进行管理。同时,树枝节点还要实现这个接口,在重写接口的方法时可以循环集合或数组得到接口对象,并对其进行调用
通用模式和安全模式
组合模式又分为通用模式和安全模式,上文中使用的是安全模式
从它们的结构图可以看出它们的区别
图中标黄的部分就是透明模式和安全模式的主要区别
透明模式中,顶级类一般是抽象类,会定义所有的方法,通用的业务逻辑也会在这里面进行实现
在叶子节点或树枝节点中,根据自己的需要重写对应的方法
在叶子节点中,一般不会重写 add()
或 remove()
方法,这些方法往往都是空实现
调用者对叶子节点调用add()
或 remove()
方法时会出现运行异常,比较不安全
安全模式就不会出现这样的问题,因为在安全模式中 add()
或 remove()
这一类对集合操作的方法都只会出现在树枝节点中,叶子节点没有这一类的方法
安全模式也有一定的缺点,没有把对象的所有行为抽象到接口类中,不符合依赖倒置原则。后续如果出现业务变动,会引起一系列的改变,不利于程序的扩展
组合模式优点
调用者调用简单,不管针对叶子节点还是树枝节点,调用者的调用逻辑是一样的
方便扩展,无论是新增叶子节点还是树枝节点,都可以很方便的扩展
组合模式缺点
对于跨度较大或很复杂的功能,进行树状结构抽象时会比较困难。对应的,在代码实现后,代码的理解成本也会比较高
在透明模式下,可能会出现运行异常
适用场景
如果要实现的核心功能可以抽象为树状结构,那么就该首先考虑使用组合模式
如果希望调用者,在针对叶子节点和树枝节点的调用逻辑上保持一致,可以考虑使用组合模式
与其他模式关系
- 与装饰器模式对比
组合模式和装饰器模式类似,都是利用递归的方式来生成N多个对象
不同的是,装饰器模式的子类结构相同。而组合模式的子类分为叶子节点和树枝节点
装饰器模式为原对象增加了额外的功能,组合模式仅仅是对原对象进行的组合调用
装饰器模式回顾:设计模式(六):装饰器模式
- 与其他模式对比
在设计模式中,有很多模式都和组合模式类似,都是将一部分工作委派给一个子类对象。
比如,组合模式中的树枝节点,生成器模式中的导演类,适配器模式中适配对象等
区别在于,子类对象要实现的功能是不一样的
最后还是那句话,设计模式不是万能的,只有合理利用设计模式才能写出合理的代码
参考:
https://zhuanlan.zhihu.com/p/483803797