设计模式之门面模式

源码

java程序的设计原则

6大原则:

单一职责:一个类和方法只做一件事。
开闭原则:对修改关闭,对扩展开发。
里氏替换原则:子类可扩展新方法,但尽量不要重写父类已有方法(注意是尽量而非绝对不可,实际中重写父类方法还是很常见的),避免多态调用时出现程序错误。
依赖倒置:依赖于抽象,而非具体实现,即面向接口编程(如方法参数,类属性使用接口声明,这样可接收任何子类)。
接口隔离:一个接口只干一件事,降低功能耦合。
最少知道/迪米特原则:降低类之间的依赖,聚合,组合等。

1:门面设计模式

我的理解:门面设计模式的核心是降低用户对于复杂子系统的使用难度,即从要知道多个类多个方法的组合使用,到只需要知道一个类一组方法的使用,降低用户使用的难度,使用门面封装了复杂度。

门面设计模式使用的场景是,当存在多个独立的子系统时,执行某些操作可能需要子系统中的一个或者是多个,对于客户端来说具有一定的复杂度需要了解每个子系统都提供了哪些类,以及使用哪些类来完成操作,此时就可以考虑使用门面设计模式,使用门面类来封装复杂操作,对客户端只提供一个简单的方法即可。比如我们常用的日志系统 ,有log4j,log4j2,logback,jul,simplelog等,但是用户在使用时只会使用其中一种的日志实现,用户只需要使用统一的日志接口就可以了,而不需要关心具体的日志实现都有哪些类,都有哪些方法,可以参考如下2个图:

在这里插入图片描述

在这里插入图片描述

与之类似的场景我们都可以优先考虑使用门面设计模式,对用户隐藏底层复杂度,降低用户使用难度。

1.1:例子(★★★★)

1.1.1:业务场景描述

比如一件房子,有1个厕所,厕所都有若干个灯,有1个厨房,厨房也有若干个灯,有一个客厅,客厅也有若干个灯,然后呢还有几件卧室,也都有若干个灯,对于每个灯都有自己独立的开关,但是呢为了方便开关灯,会有一个总的开关来控制所有灯的开关,对于厨房,厕所,客厅,卧室也都有控制各自区域的开关,这些控制一批灯状态的开关,就类似于本文要分析的门面设计模式中的门面,如下图:

在这里插入图片描述

下面我们来看下如何通过程序实现。

1.1.2:程序实现
1.1.2.1:定义开关接口

为了满足依赖倒置肯定要先定义接口。

public interface OnOff {
    int ON_OFF_ON = 0;
    int ON_OFF_OFF = 1;
    // 开灯
    void on();
    // 关灯
    void off();
}
1.1.2.2:定义各个区域灯开关子系统
public class KitchenOnOff implements OnOff {
    private String area = "厨房";
    // 厕所所有的灯
    private List<Lamp> lampList = new ArrayList() {
        {
            // 所有的灯默认关闭
            add(new Lamp(OnOff.ON_OFF_OFF, area + "1号灯"));
            add(new Lamp(OnOff.ON_OFF_OFF, area + "2号灯"));
        }
    };
    @Override
    public void on() {
        System.out.println("打开" + area + "所有灯开始!");
        lampList.stream().forEach(o -> o.setOnOff(OnOff.ON_OFF_ON));
        System.out.println("打开" + area + "所有灯结束!");
    }

    @Override
    public void off() {
        System.out.println("关闭" + area + "所有灯开始!");
        lampList.stream().forEach(o -> o.setOnOff(OnOff.ON_OFF_OFF));
        System.out.println("关闭" + area + "所有灯结束!");
    }
}
public class SittingRoomOnOff implements OnOff {
    private String area = "客厅";
    // 厕所所有的灯
    private List<Lamp> lampList = new ArrayList() {
        {
            // 所有的灯默认关闭
            add(new Lamp(OnOff.ON_OFF_OFF, area + "1号灯"));
            add(new Lamp(OnOff.ON_OFF_OFF, area + "2号灯"));
        }
    };
    @Override
    public void on() {
        System.out.println("打开" + area + "所有灯开始!");
        lampList.stream().forEach(o -> o.setOnOff(OnOff.ON_OFF_ON));
        System.out.println("打开" + area + "所有灯结束!");
    }

    @Override
    public void off() {
        System.out.println("关闭" + area + "所有灯开始!");
        lampList.stream().forEach(o -> o.setOnOff(OnOff.ON_OFF_OFF));
        System.out.println("关闭" + area + "所有灯结束!");
    }
}
public class WcOnOff implements OnOff {
    private String area = "厕所";
    // 厕所所有的灯
    private List<Lamp> lampList = new ArrayList() {
        {
            // 所有的灯默认关闭
            add(new Lamp(OnOff.ON_OFF_OFF, area + "1号灯"));
            add(new Lamp(OnOff.ON_OFF_OFF, area + "2号灯"));
        }
    };
    @Override
    public void on() {
        System.out.println("打开" + area + "所有灯开始!");
        lampList.stream().forEach(o -> o.setOnOff(OnOff.ON_OFF_ON));
        System.out.println("打开" + area + "所有灯结束!");
    }

    @Override
    public void off() {
        System.out.println("关闭" + area + "所有灯开始!");
        lampList.stream().forEach(o -> o.setOnOff(OnOff.ON_OFF_OFF));
        System.out.println("关闭" + area + "所有灯结束!");
    }
}
1.1.2.3:定义门面类
public class OnOffFacade {
    // 使用接口定义引用,满足依赖倒置原则
    private /*KitchenOnOff*/ OnOff kitchenOnOff;
    // 使用接口定义引用,满足依赖倒置原则
    private /*SittingRoomOnOff*/ OnOff sittingRoomOnOff;
    // 使用接口定义引用,满足依赖倒置原则
    private /*WcOnOff*/ OnOff wcOnOff;

    public OnOffFacade() {
        // 创建相关区域的开关类
        this.kitchenOnOff = new KitchenOnOff();
        this.sittingRoomOnOff = new SittingRoomOnOff();
        this.wcOnOff = new WcOnOff();
    }

    // 关闭厕所灯
    public void turnOffWcLamp() {
        this.wcOnOff.off();
    }

    // 注意:该方法就体现了门面的真正价值,用户只需要知道要关闭厕所灯和客厅灯时调用该方法即可,而不需要知道厕所灯子系统和客厅灯子系统的存在,降低了使用者的复杂度
    // 关闭厕所灯,客厅灯
    public void turnOffWcAndSittingRoomLamp() {
        this.wcOnOff.off();
        this.sittingRoomOnOff.off();
    }

    // 注意:该方法就体现了门面的真正价值,用户只需要知道要关闭厕所灯和客厅灯时调用该方法即可,而不需要知道都有哪些灯子系统的存在,降低了使用者的复杂度
    // 关闭所有区域的灯
    public void turnOffAllLamp() {
        this.wcOnOff.off();
        this.sittingRoomOnOff.off();
        this.kitchenOnOff.off();
    }
}

这样通过门面类就对客户端隐藏了底层复杂的系统调用即不需要创建各种类来完成操作,而只需要调用门面类的某个方法就可以了

1.1.2.4:测试
public class FacadeMain {
    public static void main(String[] args) {
        OnOffFacade onOffFacade = new OnOffFacade();
        onOffFacade.turnOffWcLamp();
        System.out.println("****************");
        onOffFacade.turnOffWcAndSittingRoomLamp();
        System.out.println("****************");
        onOffFacade.turnOffAllLamp();
    }
}

运行:

关闭厕所所有灯开始!
关闭厕所所有灯结束!
****************
关闭厕所所有灯开始!
关闭厕所所有灯结束!
关闭客厅所有灯开始!
关闭客厅所有灯结束!
****************
关闭厕所所有灯开始!
关闭厕所所有灯结束!
关闭客厅所有灯开始!
关闭客厅所有灯结束!
关闭厨房所有灯开始!
关闭厨房所有灯结束!

1.2:工作中的应用(★★★★★)

1.2.1:业务场景描述

之前在一家板车发货找货公司工作,需要发货的货主可以在我们的平台发布货源,板车司机可以在我们的平台查找货源,当板车司机找到感兴趣的货源,有如下的部分场景需要发送消息通知:

1:车主支付了货主的货物,需要发消息给货主告知货主尽快同意货主承运货物。
2:接1,不管是货主同意了车主的承运还是拒绝了承运都会发送消息告知车主。
3:接2,假设货主同意了车主的承运,车主装货完毕,会发消息给货主。
4:...其他发消息场景

当时,支持的消息类型有站内信,短信,推送,不同场景下的消息通知会使用其中的一种或者是多种消息通道进行消息通知。

1.2.2:程序实现

源码

1.2.2.1:基础代码

为了满足依赖倒置原则,我们先来定义发送消息的规范接口,源码如下:

public interface MessageSender {
    void send();
}

然后定义三个实现类:

/**
 * 发送推送
 */
public class PushMessageSender implements MessageSender {
    @Override
    public void send() {
        System.out.println("推送发送消息了。。。");
    }
}
/**
 * 发送短信
 */
public class SmsMessageSender implements MessageSender {
    @Override
    public void send() {
        System.out.println("短信发送消息了。。。");
    }
}
/**
 * 发送站内信
 */
public class StationMessageSender implements MessageSender {
    @Override
    public void send() {
        System.out.println("站内信发送消息了。。。");
    }
}

测试类:

public class MockUseNoFacadeTest {
    public static void main(String[] args) {
        MockUseNoFacade mockUseNoFacade = new MockUseNoFacade();
        mockUseNoFacade.pay();
        System.out.println("------------------");
        mockUseNoFacade.agree();
        System.out.println("------------------");
        mockUseNoFacade.loadOver();
    }
}

运行:

车主支付完成了。。。开始发送消息
推送发送消息了。。。
短信发送消息了。。。
站内信发送消息了。。。
------------------
货主同意装货了。。。开始发送消息
短信发送消息了。。。
------------------
车主装货完毕了。。。开始发送消息
推送发送消息了。。。
短信发送消息了。。。
站内信发送消息了。。。

接下来我们使用门面设计模式,来对消息发送进行封装代理,提供发送各种组合消息的功能,具体参考1.2.2.2:消息发送门面类

1.2.2.2:消息发送门面类

我们提供推送,短信,站内信任意组合的消息发送,源码如下:

/**
 * 封装消息发送细节门面类
 */
public class MessageSenderFacade {
    // 发送短信
    void sendSms() {
        new SmsMessageSender().send();
    }

    // 发送站内信
    void sendStationMsg() {
        new StationMessageSender().send();
    }

    // 发送推送
    void sendPush() {
        new PushMessageSender().send();
    }

    // 发送短信,站内信,推送
    void sendSmsAndStationAndPush() {
        new SmsMessageSender().send();
        new StationMessageSender().send();
        new PushMessageSender().send();
    }

    // 发送短信,站内信
    void sendSmsAndStation() {
        new SmsMessageSender().send();
        new StationMessageSender().send();
    }

    // 发送短信,推送
    void sendSmsAndPush() {
        new SmsMessageSender().send();
        new PushMessageSender().send();
    }

    // 发送站内信,推送
    void sendStationAndPush() {
        new StationMessageSender().send();
        new PushMessageSender().send();
    }
}

然后定义模拟的业务类:

// 模拟使用门面发送消息的业务类
public class MockBusinessUseFacade {
    // 模拟车主支付货源发送站内信,推送,短信
    void pay() {
        System.out.println("车主支付完成了。。。开始发送消息");
        /*new PushMessageSender().send();
        new SmsMessageSender().send();
        new StationMessageSender().send();*/
        new MessageSenderFacade().sendSmsAndStationAndPush();
    }

    // 模拟货主同意车主装货,发送短信
    void agree() {
        System.out.println("货主同意装货了。。。开始发送消息");
        /*new SmsMessageSender().send();*/
        new MessageSenderFacade().sendSms();
    }

    // 模拟车主装货完毕,发送推送,短信给货主
    void loadOver() {
        System.out.println("车主装货完毕了。。。开始发送消息");
        /*new PushMessageSender().send();
        new SmsMessageSender().send();
        new StationMessageSender().send();*/
        new MessageSenderFacade().sendSmsAndPush();
    }

}

测试模拟的业务类:

public class MockBusinessUseFacadeTest {
    public static void main(String[] args) {
        MockBusinessUseFacade mockBusinessUseFacade = new MockBusinessUseFacade();
        mockBusinessUseFacade.pay();
        System.out.println("--------------");
        mockBusinessUseFacade.agree();
        System.out.println("--------------");
        mockBusinessUseFacade.loadOver();
    }
}

运行:

车主支付完成了。。。开始发送消息
短信发送消息了。。。
站内信发送消息了。。。
推送发送消息了。。。
--------------
货主同意装货了。。。开始发送消息
短信发送消息了。。。
--------------
车主装货完毕了。。。开始发送消息
短信发送消息了。。。
推送发送消息了。。。

此时,有2个问题需要优化,第一个是MessageSenderFacade获取消息发送是直接new的,第二个是MessageSenderFacade类我们是针对各种消息进行了排列组合提供了不同的方法,这种方式在消息比较少时还问题不大,但当消息类型多了之后,排列组合的方法就会变的非常多,难以维护,并且对于用户的使用难度也大,不容易找到自己需要的方法,可能还没有一个个调用消息发送类来的简单,针对第一个问题,我们可以使用简单工厂模式来优化,针对第二个问题可以通过对每种消息发送方式增加一个唯一编码的方式来优化,具体我们通过1.2.3:优化来分析。

1.2.3:优化

源码

1.2.3.1:定义消息类型编码

每种消息对应一种编码,后续发送消息就可以使用编码了,源码如下:

public class MessageSendCode {
    public static final int PUSH_CODE = 1 << 0;
    public static final int SMS_CODE = 1 << 1;
    public static final int STATION_CODE = 1 << 2;
}
1.2.3.2:定义注解

定义使用在类上的注解,设置消息类对应的消息编码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Key {
    int id();
}
1.2.3.3:在消息类上使用注解

在消息类上使用注解设置消息编码,如下:

/**
 * 发送推送
 */
@Key(id = MessageSendCode.PUSH_CODE)
public class PushMessageSender implements MessageSender {

    @Override
    public void send() {
        System.out.println("推送发送消息了。。。");
    }
}
/**
 * 发送短信
 */
@Key(id = MessageSendCode.SMS_CODE)
public class SmsMessageSender implements MessageSender {
    @Override
    public void send() {
        System.out.println("短信发送消息了。。。");
    }
}
/**
 * 发送站内信
 */
@Key(id = MessageSendCode.STATION_CODE)
public class StationMessageSender implements MessageSender {
    @Override
    public void send() {
        System.out.println("站内信发送消息了。。。");
    }
}
1.2.3.4:使用SPI定义实现类

为了方便消息发送类的动态获取,这里使用到jdk SPI,关于SPI不清楚的朋友可以参考SPI的简单介绍

  • service文件
    dongshi.daddy.facade.workuse.v2.MessageSender:
dongshi.daddy.facade.workuse.v2.PushMessageSender
dongshi.daddy.facade.workuse.v2.SmsMessageSender
dongshi.daddy.facade.workuse.v2.StationMessageSender
1.2.3.5:工厂类
public class MessageSenderFactory {
    private static Map<Integer, MessageSender> productMap = new HashMap<>();

    static {
        ServiceLoader<MessageSender> load = ServiceLoader.load(MessageSender.class);
        Iterator<MessageSender> iterator = load.iterator();
        while (iterator.hasNext()) {
            MessageSender next = iterator.next();
            Class<? extends MessageSender> aClass = next.getClass();
            if (!aClass.isAnnotationPresent(Key.class)) throw new IllegalStateException("class: " + aClass + " expect @com.jh.framework.page.annotation.Key, but not found!");
            int id = aClass.getAnnotation(Key.class).id();
            productMap.put(id, next);
        }
    }

    public static MessageSender makeProduct(int code) {
        return productMap.get(code);
    }

}
1.2.3.6:门面类(重要!!!)
public class MessageSenderFacade {
    void send(Set<Integer> codeSet) {
        // 根据用户指定的消息code集合,从工厂中获取消息发送类发送消息
        codeSet.stream().forEach(v -> MessageSenderFactory.makeProduct(v).send());
    }
}

代码虽然少,但是却是核心

1.2.3.7:业务模拟类
// 模拟使用门面发送消息的业务类
public class MockBusinessUseFacade {
    private MessageSenderFacade messageSenderFacade = new dongshi.daddy.facade.workuse.v2.MessageSenderFacade();
    // 模拟车主支付货源发送站内信,推送,短信
    void pay() {
        System.out.println("车主支付完成了。。。开始发送消息");
        /*new PushMessageSender().send();
        new SmsMessageSender().send();
        new StationMessageSender().send();*/
        messageSenderFacade.send(new HashSet() {
            {
                add(MessageSendCode.PUSH_CODE);
                add(MessageSendCode.STATION_CODE);
                add(MessageSendCode.SMS_CODE);
            }
        });
    }

    // 模拟货主同意车主装货,发送短信
    void agree() {
        System.out.println("货主同意装货了。。。开始发送消息");
        /*new SmsMessageSender().send();*/
        messageSenderFacade.send(new HashSet() {
            {
                add(MessageSendCode.SMS_CODE);
            }
        });

    }

    // 模拟车主装货完毕,发送推送,短信给货主
    void loadOver() {
        System.out.println("车主装货完毕了。。。开始发送消息");
        /*new PushMessageSender().send();
        new SmsMessageSender().send();
        new StationMessageSender().send();*/
        messageSenderFacade.send(new HashSet() {
            {
                add(MessageSendCode.PUSH_CODE);
                add(MessageSendCode.SMS_CODE);
            }
        });

    }

}
1.2.3.8:测试业务模拟类
public class MockBusinessUseFacadeTest {
    public static void main(String[] args) {
        MockBusinessUseFacade mockBusinessUseFacade = new MockBusinessUseFacade();
        mockBusinessUseFacade.pay();
        mockBusinessUseFacade.agree();
        mockBusinessUseFacade.loadOver();
    }
}

运行:

车主支付完成了。。。开始发送消息
推送发送消息了。。。
短信发送消息了。。。
站内信发送消息了。。。
货主同意装货了。。。开始发送消息
短信发送消息了。。。
车主装货完毕了。。。开始发送消息
推送发送消息了。。。
短信发送消息了。。。

参考文章列表

设计模式(九)外观模式Facade(结构型)

设计模式之九 — 外观(Facade)模式

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值