设计模式篇(三):一文读懂结构型模式

本文只讲解常用的设计模式,有的不太好理解,文章需要慢慢读

代理模式

概念

Proxy(代理)模式是常见设计模式之一,"代理"顾名思义就是"替代"的意思,很容易理解。根据GoF(《Design Patterns: Elements of Reusable Object-Oriented Software》的四位作者)的代理设计模式意图是:Provide a surrogate or placeholder for another object to control access to it.就是说为另一个对象提供代理或占位符,以控制对其的访问。定义本身非常清晰,当我们要提供功能的受控访问时,将使用代理设计模式

先举个例子: 比如你现在拥有一套多余的房子想卖掉,但是你不知道谁要买,你不可能在大街上见到一个人就问"兄弟要买房吗?";我们肯定想到了卖房中介,是的,我们把我们房子的一些信息告诉中介公司然后让他帮我们卖,然后想买房的人肯定不会直接找到你而是去了中介公司,为啥?因为中介的房子多啊有的挑,况且想买房的人根本就不认识你。于是他通过中介公司心意你的房子然后买下。 分析:在上面这个例子中,要卖房子的是你,而执行的人确实中介公司,于是中介公司就充当了"代理"的身份,客户(买房子的人)就不需要直接跟你对接

静态代理

我们定义了一个House接口里面有一个sale()方法,有两个实现类,一个是实体类(真实卖房子的人)RealManOfSaleHouse与代理类SaleHouseProxy,其中代理类SaleHouseProxy还有一个额外的宣传装修房子的方法decorate(),而且代理类保存一个引用使得代理可访问实体,这样代理就可以来代替实体来卖房。下面上代码:

java 复制代码
//House接口

public interface House {

    void sale();
}

//RealManOfSaleHouse类

public class RealManOfSaleHouse implements House {

    @Override
    public void sale() {
        System.out.println("售房啦,售房啦,130平米冬暖夏凉首付仅需20W,瞧一瞧看一看啦");
    }
}

//SaleHouseProxy类

public class SaleHouseProxy implements House{

    private RealManOfSaleHouse owner ;

    public SaleHouseProxy(RealManOfSaleHouse owner){
        this.owner = owner;
    }

    @Override
    public void sale() {
        owner.sale();
        decorate();
    }

    public void decorate(){
        System.out.println("装修房子喽,专业设计师保你冬暖夏凉,24期免息,瞧一瞧看一看啦");
    }

}

以上就是一个静态代理的例子,在这里代理类对接口实现了扩展,加了一个decorate()方法,这表明静态代理有一个优点:可以进行一些功能的附加与增强。但是这种静态代理缺点很多,比如虽然能够扩展,但是任何实现类都需要实现全部抽象方法,而且代理类也需要实现全部抽象方法,维护成本太高

动态代理

静态代理与动态代理有什么区别?从字面上看就是"静态"与"动态"的区别。以上面的例子来说,我们使用SaleHouseProxy proxy = new SaleHouseProxy(owner);这行代码手动new了一个代理对象然后调用业务方法,在动态代理中就不需要我们手动去写而是由Proxy.newProxyInstance()这个方法在程序运行时期来动态创建

我们举个例子理解下:我们模拟一个信息发送的过程:假设小红要向小明发送信息,小红与小明相隔千里,于是小红打开了微信发送给小明。 下面代码模拟下:

java 复制代码
//发送消息接口
public interface IMessage {
    //核心业务接口 
    public void message(String message); 
}

//接口实现,也是目标类
public class MessageImpl implements IMessage {

    @Override
    public void message(String message) {
        System.out.println("发送内容:"+message);
    }
}

上面和静态代理没什么区别,区别在下面的代理类里

java 复制代码
public class ServerProxy implements InvocationHandler {

    private Object object; //核心业务对象

    public  Object bind(Object object){

        //保存真实业务对象
        this.object = object;

        //获取真实业务主题所在类对应的类加载器,因为需要分析真实业务主题接口中所拥有的方法,才可以构建动态子类
        //所有的动态代理都是基于接口的设计应用,那么此时就要获取全部的接口信息
        //当前的类为InvocationHandler接口子类,所以使用this描述的是本类的实例化对象
        return Proxy.newProxyInstance(object.getClass().getClassLoader(),object.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result=null;
        if (before()){
            //此时代理方法中需要进业务方法的反射调用,需要提供实例化对象(this.object),Method对象(method),参数(args)
            result = method.invoke(this.object,args);
            after();
        }
        return result;
    }

    public boolean before(){
        System.out.println("建立连接*****连接成功");
        return true;
    }

    public void after(){
        System.out.println("接收成功*****关闭连接");
    }

}

我简单说下这个代理类,首先实现接口InvocationHandler,并实现其中invoke方法,然后在bind方法中使用Java的Proxy.newProxyInstance(object.getClass().getClassLoader(),object.getClass().getInterfaces(), this) 创建代理对象返回,严格意义上来讲我们写的ServerProxy并不算是代理类,他其实是一个handler类,当代理对象调用方法时,走这个类的invoke方法,对方法进行反射,从而实现增强的效果,我们看下调用的代码:

java 复制代码
IMessage iMessage = (IMessage) new ServerProxy().bind(new MessageImpl()); iMessage.message("Hi ,小明");

上面的iMessage就是Proxy.newInstance创建返回的目标对象的代理,代理类中包含目标类中的方法,调用时,这个方法的做的事情是:反射获取目标类的该方法对象,然后调用ServerProxy的invoke方法方法,将目标的该方法method对象传进去,然后反射执行,并且再反射执行前后,进行增强

装饰器模式

我们通常在编码的时候为了扩展一个类的功能往往用的是继承来实现,但是继承的缺点主要是单继承的局限性和可能产生类爆炸的后果。

装饰模式(Decorator),动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。装饰模式属于结构型模式,它是作为现有的类的一个包装

设想一个场景,某天你去吃面,点了一碗面,然后你发现可以加蛋,加牛肉,加鸡腿,你是一个程序员,你会装饰器模式,于是你想秀一手:

基本接口:

java 复制代码
public interface INoodles { 
    public void cook(); 
}

基本实现类:

java 复制代码
public class Noodles implements INoodles {
    @Override public void cook() { 
        System.out.println("的面条"); 
    } 
}

接下来就是装饰器模式的实现,装饰器模式的思想,就是再加一个装饰器的抽象类,实现目标类中所有的方法,并且方法中也是使用目标类对象调用自身的方法,这里并没有进行增强,我们先实现这个:

java 复制代码
public abstract class NoodlesDecorator implements INoodles {

    private INoodles noodles;    //添加一个INoodles 的引用

    public NoodlesDecorator(INoodles noodles){     //通过构造器来设置INoodles 
        this.noodles = noodles;
    }

    @Override
    public void cook() {
        if (noodles!=null){
            noodles.cook();
        }
    }

}

接下来就是装饰器抽象类的实现方法,不同增强使用不同实现方法即可

加鸡蛋:

java 复制代码
public class EggDecorator extends NoodlesDecorator {

    public EggDecorator(INoodles noodles) {
        super(noodles);
    }

    /**
     * 重写父类的cook方法,并添加自己的实现,调用父类的cook方法,此cook方法是通过本类的构造器
     * EggDecorator(INoodles noodles)传入的noodles的cook操作
     */
    @Override
    public void cook() {
        System.out.println("加了一个荷包蛋");
        super.cook();
    }
}

剩下的加鸡腿还是其他,实现装饰器抽象类即可,在调用时创建目标对象,然后再传进这些实现类的构造方法中创建这些目标对象,调用方法,就可以对原来目标对象的方法进行增强了

虽然装饰器会增加代码复杂度,但是实际用的也挺多的,如果你希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为, 可以使用装饰模式

适配器模式

适配器模式(Adapter Pattern),将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能在一起工作的那些类可以在一起工作

举个形象点的例子:有些国家用110V电压,而我们国家用的是220V电压,但是我们的电器,比如笔记本电脑是不能什么电压都能用的,于是我们就用一个电源适配器,只要有电,无论是110V还是220V,都能把电源转换成我们需要的电压,这就是电源适配器的作用,适配器的意思就是使得一个东西适配另外一个东西

如果大家熟悉Spring的AOP的话就知道,所有的代理增强,不管是前置增强还是后置增强,都会被适配为环绕增强,下面我们举个例子来实现下适配器模式:

我们以电源适配器为例来编写实例代码。

假设你的华为手机快没电了,你下意识地往房间的墙上扫了一圈并发现了一个插座(220V),于是你拿出了你的手机充电器一头插在墙上,一头插在手机上开始充电了。这个手机充电器其实就是一个电压适配器,将220V电压转换成我们手机需要的5V电压

目标接口(5v):

java 复制代码
public interface Target { 
//我们所期待的接口 
    public Integer outPut5V();     //我们手机所需要的5V电压 
}

需要适配的类(220v):

java 复制代码
public class Adaptee {  //需要适配的类
    public Integer outPut220V(){   //该类提供220V电压,我们手机不能直接使用,所以该类需要被适配
        System.out.println("这是一个220V的电压");
        return 220;
    }
}

适配器:

java 复制代码
public class Adapter implements Target {  //适配器类
    private Adaptee adaptee = new Adaptee();   //建立一个私有的Adaptee对象

    @Override
    public Integer outPut5V() {         //这样就可以把表面上调用outPut5V方法变成实际上调用了outPut220V方法,也就完成了适配
        return adaptee.outPut220V()/44;
    }
}

这样调用适配器的outPut5V方法时就实现了适配的效果

组合模式

组合模式(Composite Pattern)属于结构型模式,将对象组合成树形结构以表示"部分"---"整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性

什么叫"部分"---"整体"的层次结构呢,举个例子:

假如我们在网上购物买了很多的东西,仓库收到订单后按照订单明细给我们的东西打包,由于东西很多他们用了很多的包装盒,并最终用一个巨大的盒子来包裹每一个订单的商品

下面我们举例子来详细解释,看下图:

图中整体是销售部门,销售部门下面有子节点国内销售部门和国际销售部门,其中国内销售部门下面还有子节点,因此国内销售部门是又一个的整体,而国际销售部门下面不存在子节点,因此是一个部分

部门接口:

java 复制代码
public interface Department { void printDepartmentName(); //输出部门名称的方法 }

销售部门实现类:

java 复制代码
public class SaleDepartment implements Department {

	//部门名称
    private  String name;  

	//相当于一个Composite,下面可以拥有其他的子部门,而不是一个叶子节点
    List<Department> departments = new ArrayList<>();  

	//用于增加子部门
    public void addDepartment(Department department){    
        departments.add(department);
    }

	//删除子部门
    public void remove(Department department){     
        departments.remove(department);
    }

    @Override
    public void printDepartmentName() {
    	//打印本部门的名称
        System.out.println(name);
        //打印本部门下的子部门的名称
        for (Department department:departments){
            department.printDepartmentName();
        }
    }

    public SaleDepartment(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

其中departments集合是添加子节点使用的

国内销售部门:

java 复制代码
public class DomesticSaleDepartment implements Department {

    private String name;

	//相当于一个Composite,下面可以拥有其他各个省份的子部门
    List<Department> list = new ArrayList<>();

    public DomesticSaleDepartment(String name) {
        this.name = name;
    }
	//用于增加子部门
    public void add(Department department){
        list.add(department);
    }
	//用于删除子部门
    public void remove(Department department){
        list.remove(department);
    }

    @Override
    public void printDepartmentName() {
        System.out.println(name);
        for (Department department:list){
            department.printDepartmentName();
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

国际销售部门:

java 复制代码
public class InternationalSaleDepartment implements Department {
    private String name;

    public InternationalSaleDepartment(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

	
    @Override
    public void printDepartmentName() {
        System.out.println(name);
    }
}

使用时先创建叶子节点对象,然后将部分放到上层的整体中,当调用整体中对应的方法时,会将这些部分对象的这个方法挨个调用。Spring框架中就很多地方使用这个模式,比如参数解析器,需要有请求头拼接参数处理,请求体参数处理,json类型参数处理等等,使用组合器组合成一个整体,当有参数需要解析时,调用组合器(整体)的解析方法,这样会遍历所有(部分)对象,看哪个符合该参数的解析,符合的话使用该解析器解析参数

门面模式

假设有一个系统 A,提供了 a、b、c、d 四个接口。系统 B 完成某个业务功能,需要调用A 系统的 a、b、d 接口。利用门面模式,我们提供一个包裹 a、b、d 接口调用的门面接口x,给系统 B 直接使用。

不知道你会不会有这样的疑问,让系统 B 直接调用 a、b、d 感觉没有太大问题呀,为什么还要提供一个包裹 a、b、d 的接口 x 呢?关于这个问题,我通过一个具体的例子来解释一下

假设我们刚刚提到的系统 A 是一个后端服务器,系统 B 是 App 客户端。App 客户端通过后端服务器提供的接口来获取数据。我们知道,App 和服务器之间是通过移动网络通信 的,网络通信耗时比较多,为了提高 App 的响应速度,我们要尽量减少 App 与服务器之间的网络通信次数

如果我们现在发现 App 客户端的响应速度比较慢,排查之后发现,是因为过多的接口调用过多的网络通信。针对这种情况,我们就可以利用门面模式,让后端服务器提供一个包裹a、b、d 三个接口调用的接口 x。App 客户端调用一次接口 x,来获取到所有想要的数 据,将网络通信的次数从 3 次减少到 1 次,也就提高了 App 的响应速度

这里举的例子只是应用门面模式的其中一个意图,也就是解决性能问题。实际上,不同的应用场景下,使用门面模式的意图也不同

我们举个例子讲解下: 假设我们去一个餐厅吃饭,服务员写下了我们点的菜单,然后通知厨师按照订单来做菜,厨师做好后就通知服务员,然后服务员将饭菜给我们端上来,我们吃好后,清洁员来收拾餐桌并洗碗筷

先写一个服务员类Waiter:

java 复制代码
public class Waiter {

	//用了单例模式
    private static Waiter instance = new Waiter();

    public static Waiter getInstance(){
        return instance;
    }

    public void writeOrder(){
        System.out.println("服务员写下顾客点的菜品");
    }

    public void notifyCook(){
        System.out.println("服务员通知厨师");
    }

    public void serveCustomer(){
        System.out.println("服务员为顾客端菜");
    }

    public void cleanUp(){
        System.out.println("服务员收拾餐桌");
    }
}

再写一个厨师类Cook:

java 复制代码
public class Cook {

    private static Cook instance = new Cook();

    public static Cook getInstance(){
        return instance;
    }

    public void prepareFood(){
        System.out.println("厨师准备菜品");
    }

    public void notifyWaiter(){
        System.out.println("厨师通知服务员");
    }

}

再写清洁工类:

java 复制代码
public class DishWasher {
    private static DishWasher instance = new DishWasher();

    public static DishWasher getInstance() {
        return instance;
    }
    
    public void washDishes() {
        System.out.println("清洁员洗刷碗筷");
    }
}

每次顾客都需要依次调用这三个类中的方法完成就餐动作,我们下面使用门面就行包裹:

java 复制代码
public class Facade {

    private Waiter waiter;
    private Cook cook;
    private DishWasher dishWasher;

    public Facade(){
        this.waiter = Waiter.getInstance();
        this.cook = Cook.getInstance();
        this.dishWasher = DishWasher.getInstance();
    }

    public void orderFood(){
        waiter.writeOrder();
        waiter.notifyCook();
        cook.prepareFood();
        cook.notifyWaiter();
        waiter.serveCustomer();
    }

    public void leave(){
        waiter.cleanUp();
        dishWasher.washDishes();
    }
}

这样的话看下我们顾客调用就会很轻松:

java 复制代码
public static void main(String[] args) {
    	//创建一个Facade对象
        Facade facade = new Facade();
        //顾客点餐
        facade.orderFood();
        //顾客吃完离开了
        facade.leave();

    }

门面模式再很多项目中都有使用,比如Spring框架的类型转换时就有使用,定义一个高层类型转换器类,然后需要类型转换时调用这个高层转换的接口,这个高层转换接口内部就会按照顺序调用底层类型转换接口去做真正的类型转换操作

总结

以上就是常用的几种结构性模式,当然还有享元模式和桥接模式,但是这两种模式使用真的不多,而且设计的过于复杂,对整个系统的维护不利,有兴趣的大佬可以去学习学习,这里我就不多讲了。还是那句话,如果文章里有任何问题,欢迎大佬们给指正

相关推荐
2401_8576100310 分钟前
Spring Boot框架:电商系统的技术优势
java·spring boot·后端
希忘auto26 分钟前
详解MySQL安装
java·mysql
冰淇淋烤布蕾37 分钟前
EasyExcel使用
java·开发语言·excel
拾荒的小海螺43 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
Jakarta EE1 小时前
正确使用primefaces的process和update
java·primefaces·jakarta ee
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
java—大象1 小时前
基于java+springboot+layui的流浪动物交流信息平台设计实现
java·开发语言·spring boot·layui·课程设计
杨哥带你写代码2 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_2 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
布川ku子2 小时前
[2024最新] java八股文实用版(附带原理)---Mysql篇
java·mysql·面试