设计模式 - 结构型模式_外观模式

简介: 设计模式是解决程序中不合理、不易于扩展、不易于维护的问题,也是⼲掉⼤部分 ifelse 的利器,在我们常⽤的框架中基本都会⽤到⼤量的设计模式来构建组件,这样也能⽅便框架的升级和功能的扩展。但如果不能合理的设计以及乱⽤设计模式,会导致整个编程变得更加复杂难维护,也就是我们常说的: 反设计 、 过渡设计 。⽽这部分设计能⼒也是从实践的项⽬中获取的经验,不断的改造优化摸索出的最合理的⽅式,应对当前的服务体量。

@[toc]

在这里插入图片描述


结构型模式

结构型模式主要是解决如何将对象和类组装成较大的结构, 并同时保持结构的灵活和⾼效。

结构型模式包括:适配器、桥接、组合、装饰器、外观、享元、代理,这7类

在这里插入图片描述


概述

设计模式是解决程序中不合理、不易于扩展、不易于维护的问题,也是⼲掉⼤部分 ifelse 的利器,在我们常⽤的框架中基本都会⽤到⼤量的设计模式来构建组件,这样也能⽅便框架的升级和功能的扩展。

但如果不能合理的设计以及乱⽤设计模式,会导致整个编程变得更加复杂难维护,也就是我们常说的: 反设计 、 过渡设计 。⽽这部分设计能⼒也是从实践的项⽬中获取的经验,不断的改造优化摸索出的最合理的⽅式,应对当前的服务体量。

在这里插入图片描述

外观模式也叫⻔⾯模式,主要解决的是降低调⽤⽅的使⽤接⼝的复杂逻辑组合。这样调⽤⽅与实际的接⼝提供⽅提供⽅提供了⼀个中间层,⽤于包装逻辑提供API接⼝。

有些时候外观模式也被⽤在中间件层,对服务中的通⽤性复杂逻辑进⾏中间件层包装,让使⽤⽅可以只关⼼业务开发。

那么这样的模式在我们的所⻅产品功能中也经常遇到,就像⼏年前注册⼀个⽹站时候往往要添加很多信息,包括: 姓名、昵称、⼿机号、QQ、邮箱、住址、单身等等,但现在注册成为⼀个⽹站的⽤户只需要⼀步即可,⽆论是⼿机号还是微信也都提供了这样的登录服务。⽽对于服务端应⽤开发来说以前是提供了⼀个整套的接⼝,现在注册的时候并没有这些信息,那么服务端就需要进⾏接⼝包装,在前端调⽤注册的时候服务端获取相应的⽤户信息(从各个渠道),如果获取不到会让⽤户后续进⾏补全(营销补全信息给奖励),以此来拉动⽤户的注册量和活跃度。


Case

在这里插入图片描述

模拟⼀个将所有服务接⼝添加⽩名单的场景。

我们知道,每⼀次发版上线都需要进⾏测试,⽽这部分测试验证⼀般会进⾏⽩名单开量或者切量的⽅式进⾏验证。那么如果在每⼀个接⼝中都添加这样的逻辑,就会⾮常麻烦且不易维护。另外这是⼀类具备通⽤逻辑的共性需求,⾮常适合开发成组件,以此来治理服务,让研发⼈员更多的关⼼业务功能开发。

⼀般情况下对于外观模式的使⽤通常是⽤在复杂或多个接⼝进⾏包装统⼀对外提供服务上,此种使⽤⽅式也相对简单在我们平常的业务开发中也是最常⽤的。我们可能经常听到把这两个接⼝包装⼀下,但在本例⼦中我们把这种设计思路放到中间件层,让服务变得可以统⼀控制。

【场景模拟⼯程】
在这里插入图片描述

SpringBoot 的 HelloWorld ⼯程,在⼯程中提供了查询⽤户信息的接⼝HelloWorldController.queryUserInfo ,为后续扩展此接⼝的⽩名单过滤做准备。


【定义基础查询接⼝】

@RestController
public class HelloWorldController {

    @Value("${server.port}")
    private int port;

    /**
     * key:需要从入参取值的属性字段,如果是对象则从对象中取值,如果是单个值则直接使用
     * returnJson:预设拦截时返回值,是返回对象的Json
     *
     * http://localhost:8080/api/queryUserInfo?userId=1001
     * http://localhost:8080/api/queryUserInfo?userId=小团团
     */
    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
    public UserInfo queryUserInfo(@RequestParam String userId) {
        return new UserInfo("虫虫:" + userId, 19, "天津市南开区旮旯胡同100号");
    }

}

这⾥提供了⼀个基本的查询服务,通过⼊参 userId ,查询⽤户信息。后续就需要在这⾥扩展⽩名单,只有指定⽤户才可以查询,其他⽤户不能查询。


【Application启动类】

@SpringBootApplication
@Configuration
public class HelloWorldApplication {

    public static void main(String[] args) {
        SpringApplication.run(HelloWorldApplication.class, args);
    }

}

通⽤的 SpringBoot 启动类。需要添加的是⼀个配置注解 @Configuration ,为了后续可以读取⽩名单配置。


Bad Impl

⼀般对于此种场景最简单的做法就是直接修改代码 . 累加 if 块⼏乎是实现需求最快也是最慢的⽅式,快是修改当前内容很快,慢是如果同类的内容⼏百个也都需要如此修改扩展和维护会越来越慢。
public class HelloWorldController {

    public UserInfo queryUserInfo(@RequestParam String userId) {

        // 做白名单拦截
        List<String> userList = new ArrayList<String>();
        userList.add("1001");
        userList.add("aaaa");
        userList.add("ccc");
        if (!userList.contains(userId)) {
            return new UserInfo("1111", "非白名单可访问用户拦截!");
        }

        return new UserInfo("虫虫:" + userId, 19, "天津市南开区旮旯胡同100号");
    }

}

以上的实现是模拟⼀个Api接⼝类,在⾥⾯添加⽩名单功能,但类似此类的接⼝会有很多都需要修改,所以这也是不推荐使⽤此种⽅式的重要原因.

  • 在这⾥⽩名单的代码占据了⼀⼤块,但它⼜不是业务中的逻辑,⽽是因为我们上线过程中需要做的开量前测试验证。
  • 如果⽇常对待此类需求经常是这样开发,那么可以按照此设计模式进⾏优化,让后续的扩展和摘除更加容易

在这里插入图片描述


Better Impl

接下来使⽤外观器模式来进⾏代码优化,也算是⼀次很⼩的重构

这次重构的核⼼是使⽤外观模式也可以说⻔⾯模式,结合 SpringBoot 中的⾃定义 starter 中间件开发的⽅式,统⼀处理所有需要⽩名单的地⽅。

  1. SpringBoot的starter中间件开发⽅式。
  2. ⾯向切⾯编程和⾃定义注解的使⽤。
  3. 外部⾃定义配置信息的透传,SpringBoot与Spring不同,对于此类⽅式获取⽩名单配置存在差异。

【⼯程结构】
在这里插入图片描述


【⻔⾯模式模型结构】

在这里插入图片描述

  • 以上是外观模式的中间件实现思路,右侧是为了获取配置⽂件,左侧是对于切⾯的处理。
  • ⻔⾯模式可以是对接⼝的包装提供出接⼝服务,也可以是对逻辑的包装通过⾃定义注解对接⼝提供服务能⼒。

【配置服务类】

public class StarterService {

    private String userStr;

    public StarterService(String userStr) {
        this.userStr = userStr;
    }

    public String[] split(String separatorChar) {
        return StringUtils.split(this.userStr, separatorChar);
    }

}

以上类的内容较简单只是为了获取配置信息。


【配置类注解定义】

@ConfigurationProperties("artisan.door")
public class StarterServiceProperties {

    private String userStr;

    public String getUserStr() {
        return userStr;
    }

    public void setUserStr(String userStr) {
        this.userStr = userStr;
    }

}

⽤于定义好后续在 application.yml 中添加 artisan.door 的配置信息。


【⾃定义配置类信息获取】

@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {

    @Autowired
    private StarterServiceProperties properties;

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "artisan.door", value = "enabled", havingValue = "true")
    StarterService starterService() {
        return new StarterService(properties.getUserStr());
    }

}

以上代码是对配置的获取操作,主要是对注解的定义: @Configuration 、 @ConditionalOnClass 、 @EnableConfigurationProperties ,这
⼀部分主要是与SpringBoot的结合使⽤.


【切⾯注解定义】

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {

    String key() default "";

    String returnJson() default "";

}

定义了外观模式⻔⾯注解,后续就是此注解添加到需要扩展⽩名单的⽅法上

这⾥提供了两个⼊参

  • key:获取某个字段例如⽤户ID
  • returnJson:确定⽩名单拦截后返回的具体内容

【⽩名单切⾯逻辑】

@Aspect
@Component
public class DoJoinPoint {

    private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);

    @Autowired
    private StarterService starterService;

    @Pointcut("@annotation(com.artisan.design.door.annotation.DoDoor)")
    public void aopPoint() {
    }

    @Around("aopPoint()")
    public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
        //获取内容
        Method method = getMethod(jp);
        DoDoor door = method.getAnnotation(DoDoor.class);
        //获取字段值
        String keyValue = getFiledValue(door.key(), jp.getArgs());
        logger.info("artisan door handler method:{} value:{}", method.getName(), keyValue);
        if (null == keyValue || "".equals(keyValue)) return jp.proceed();
        //配置内容
        String[] split = starterService.split(",");
        //白名单过滤
        for (String str : split) {
            if (keyValue.equals(str)) {
                return jp.proceed();
            }
        }
        //拦截
        return returnObject(door, method);
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }

    private Class<? extends Object> getClass(JoinPoint jp) throws NoSuchMethodException {
        return jp.getTarget().getClass();
    }

    //返回对象
    private Object returnObject(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {
        Class<?> returnType = method.getReturnType();
        String returnJson = doGate.returnJson();
        if ("".equals(returnJson)) {
            return returnType.newInstance();
        }
        return JSON.parseObject(returnJson, returnType);
    }

    //获取属性值
    private String getFiledValue(String filed, Object[] args) {
        String filedValue = null;
        for (Object arg : args) {
            try {
                if (null == filedValue || "".equals(filedValue)) {
                    filedValue = BeanUtils.getProperty(arg, filed);
                } else {
                    break;
                }
            } catch (Exception e) {
                if (args.length == 1) {
                    return args[0].toString();
                }
            }
        }
        return filedValue;
    }

}

这⾥包括的内容较多,核⼼逻辑主要是 Object doRouter(ProceedingJoinPoint jp) ,接下来我们分别介绍。

@Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")
定义切⾯,这⾥采⽤的是注解路径,也就是所有的加⼊这个注解的⽅法都会被切⾯进⾏管理。
getFiledValue
获取指定key也就是获取⼊参中的某个属性,这⾥主要是获取⽤户ID,通过ID进⾏拦截校验。
returnObject
返回拦截后的转换对象,也就是说当⾮⽩名单⽤户访问时则返回⼀些提示信息
doRouter

切⾯核⼼逻辑,这⼀部分主要是判断当前访问的⽤户ID是否⽩名单⽤户,如果是则放⾏ jp.proceed(); ,否则返回⾃定义的拦截提示信息。


【配置文件】

server:
  port: 8080

spring:
  application:
    name: helloworld-door

# 自定义中间件配置
artisan:
  door:
    enabled: true
    userStr: 1001,aaaa,ccc #白名单用户ID,多个逗号隔开

这⾥主要是加⼊了⽩名单的开关和⽩名单的⽤户ID,逗号隔开。

【在Controller中添加⾃定义注解】

  /**
     * @DoDoor 自定义注解
     * key:需要从入参取值的属性字段,如果是对象则从对象中取值,如果是单个值则直接使用
     * returnJson:预设拦截时返回值,是返回对象的Json
     *
     * http://localhost:8080/api/queryUserInfo?userId=1001
     * http://localhost:8080/api/queryUserInfo?userId=小团团
     */
    @DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}")
    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
    public UserInfo queryUserInfo(@RequestParam String userId) {
        return new UserInfo("虫虫:" + userId, 19, "天津市南开区旮旯胡同100号");
    }
  • 这⾥核⼼的内容主要是⾃定义的注解的添加 @DoDoor ,也就是我们的外观模式中间件化实现。
  • key:需要从⼊参取值的属性字段,如果是对象则从对象中取值,如果是单个值则直接使⽤。
  • returnJson:预设拦截时返回值,是返回对象的Json。

【启动SpringBoot】

⽩名单⽤户访问

http://localhost:8080/api/queryUserInfo?userId=1001

返回

{"code":"0000","info":"success","name":"⾍⾍:1001","age":19,"address":"天津市
南开区旮旯胡同100号"}

此时的测试结果正常,可以拿到接⼝数据


⾮⽩名单⽤户访问

http://localhost:8080/api/queryUserInfo?userId=⼩团团
{"code":"1111","info":"⾮⽩名单可访问⽤户拦
截!","name":null,"age":null,"address":null}

把 userId 换成 ⼩团团 ,此时返回的信息已经是被拦截的信息。⽽这个拦截信息正式我们⾃定义注解中的信息:

@DoDoor(key = "userId", returnJson = "
{\"code\":\"1111\",\"info\":\"⾮⽩名单可访问⽤户拦截!\"}")

小结

通过中间件的⽅式实现外观模式,这样的设计可以很好的增强代码的隔离性,以及复⽤性,不仅使⽤上⾮常灵活也降低了每⼀个系统都开发这样的服务带来的⻛险。

只是⾮常简单的⽩名单控制,是否需要这样的处理。但往往⼀个⼩⼩的开始会影响着后续⽆限的扩展,实际的业务开发往往也要复杂的很多,不可能如此简单。因⽽使⽤设计模式来让代码结构更加⼲净整洁。

在这里插入图片描述

相关文章
|
2月前
|
设计模式 Java
Java设计模式-外观模式(11)
Java设计模式-外观模式(11)
|
1月前
|
设计模式 Java
Java设计模式之外观模式
这篇文章详细解释了Java设计模式之外观模式的原理及其应用场景,并通过具体代码示例展示了如何通过外观模式简化子系统的使用。
29 0
|
3月前
|
设计模式 存储 Java
【十】设计模式~~~结构型模式~~~享元模式(Java)
文章详细介绍了享元模式(Flyweight Pattern),这是一种对象结构型模式,通过共享技术实现大量细粒度对象的重用,区分内部状态和外部状态来减少内存中对象的数量,提高系统性能。通过围棋棋子的设计案例,展示了享元模式的动机、定义、结构、优点、缺点以及适用场景,并探讨了单纯享元模式和复合享元模式以及与其他模式的联用。
【十】设计模式~~~结构型模式~~~享元模式(Java)
|
3月前
|
设计模式 存储 Java
【九】设计模式~~~结构型模式~~~外观模式(Java)
文章详细介绍了外观模式(Facade Pattern),这是一种对象结构型模式,通过引入一个外观类来简化客户端与多个子系统之间的交互,降低系统的耦合度,并提供一个统一的高层接口来使用子系统。通过文件加密模块的实例,展示了外观模式的动机、定义、结构、优点、缺点以及适用场景,并讨论了如何通过引入抽象外观类来提高系统的可扩展性。
【九】设计模式~~~结构型模式~~~外观模式(Java)
|
3月前
|
设计模式 Java
【八】设计模式~~~结构型模式~~~装饰模式(Java)
文章详细介绍了装饰模式(Decorator Pattern),这是一种对象结构型模式,用于在不使用继承的情况下动态地给对象添加额外的职责。装饰模式通过关联机制,使用装饰器类来包装原有对象,并在运行时通过组合的方式扩展对象的行为。文章通过图形界面构件库的设计案例,展示了装饰模式的动机、定义、结构、优点、缺点以及适用场景,并提供了Java代码实现和应用示例。装饰模式提高了系统的灵活性和可扩展性,适用于需要动态、透明地扩展对象功能的情况。
【八】设计模式~~~结构型模式~~~装饰模式(Java)
|
3月前
|
设计模式 XML 存储
【七】设计模式~~~结构型模式~~~桥接模式(Java)
文章详细介绍了桥接模式(Bridge Pattern),这是一种对象结构型模式,用于将抽象部分与实现部分分离,使它们可以独立地变化。通过实际的软件开发案例,如跨平台视频播放器的设计,文章阐述了桥接模式的动机、定义、结构、优点、缺点以及适用场景,并提供了完整的代码实现和测试结果。桥接模式适用于存在两个独立变化维度的系统,可以提高系统的可扩展性和灵活性。
【七】设计模式~~~结构型模式~~~桥接模式(Java)
|
3月前
|
设计模式 XML 存储
【六】设计模式~~~结构型模式~~~适配器模式(Java)
文章详细介绍了适配器模式(Adapter Pattern),这是一种结构型设计模式,用于将一个类的接口转换成客户期望的另一个接口,使原本不兼容的接口能够一起工作,提高了类的复用性和系统的灵活性。通过对象适配器和类适配器两种实现方式,展示了适配器模式的代码应用,并讨论了其优点、缺点以及适用场景。
|
3月前
|
设计模式 缓存 Java
【十一】设计模式~~~结构型模式~~~代理模式(Java)
文章详细介绍了代理模式(Proxy Pattern),这是一种对象结构型模式,用于给对象提供一个代理以控制对它的访问。文中阐述了代理模式的动机、定义、结构、优点、缺点和适用环境,并探讨了远程代理、虚拟代理、保护代理等不同代理形式。通过一个商务信息查询系统的实例,展示了如何使用代理模式来增加身份验证和日志记录功能,同时保持客户端代码的无差别对待。此外,还讨论了代理模式在分布式技术和Spring AOP中的应用,以及动态代理的概念。
【十一】设计模式~~~结构型模式~~~代理模式(Java)
|
4月前
|
设计模式 JavaScript 前端开发
js设计模式【详解】—— 外观模式
js设计模式【详解】—— 外观模式
37 2
|
5月前
|
设计模式 Java
Java设计模式:外观模式之优雅门面(九)
Java设计模式:外观模式之优雅门面(九)

热门文章

最新文章

  • 1
    C++一分钟之-设计模式:工厂模式与抽象工厂
    43
  • 2
    《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
    50
  • 3
    C++一分钟之-C++中的设计模式:单例模式
    58
  • 4
    《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
    38
  • 5
    《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
    63
  • 6
    Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
    58
  • 7
    Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
    42
  • 8
    Java面试题:如何使用设计模式优化多线程环境下的资源管理?Java内存模型与并发工具类的协同工作,描述ForkJoinPool的工作机制,并解释其在并行计算中的优势。如何根据任务特性调整线程池参数
    50
  • 9
    Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
    110
  • 10
    Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
    78
  • 下一篇
    无影云桌面