当前位置: 首页 > article >正文

契约思维驱动开发:OpenAPI的最佳实践

文章目录

  • 引言
  • 软件设计中的规范
    • 命名规范
    • 异常处理规范
      • 服务提供者的异常处理
      • 服务调用者的异常处理
    • 架构规范
  • 软件设计中的标准
    • Java规范
    • API设计标准
  • 依赖契约的扩展机制
    • 基于接口的扩展
      • 1. Spring ApplicationListener 扩展示例
        • 接口定义 (框架提供)
        • 用户自定义实现 (扩展点)
        • 框架调用逻辑 (Spring内部实现)
      • 2. Logback AppenderBase 扩展示例
        • 抽象类定义 (框架提供)
        • 用户自定义 Appender (扩展点)
        • 配置文件 (约定契约)
      • 扩展机制核心特点
    • 基于配置数据的扩展
      • 示例 1:数据库连接池配置(通过属性文件约定)
        • 数据契约定义
        • 框架解析逻辑
        • 用户使用方式
      • 示例 2:动态策略加载(通过类名约定)
        • 数据契约定义
        • 框架解析逻辑
        • 用户扩展方式
      • 示例 3:SLF4J 日志绑定(通过类名和静态方法约定)
        • 数据契约定义
        • 用户扩展实现
        • 框架调用逻辑
      • 关键设计对比
      • 核心特点

在这里插入图片描述


引言

开发过程中大概率会碰到这样的场景: 代码评估要两天,而修改代码只需要几分钟。这种局面主要是由系统的混乱无序造成的,因为缺少规范和约束,代码的复杂度随意滋长,导致维护困难。面对这样的代码,往往要花费很长时间去厘清代码结构和业务逻辑,而真正需要修改的点也许只是“一行代码”而已。

为了保证软件编程风格的一致性,减少随心所欲带来的复杂度,我们有必要使用契约思维制定一定程度上的编程规范,去约束团队的行为。规范的价值,就在于它能保证代码的一致性,而一致性在很大程度上可以降低认知成本和复杂度


软件设计中的规范

软件需要规范,然而规范并不是越多越好。每个团队都应该根据自己的实际情况,制定能够帮助团队降低系统复杂度、避免混乱的规范。

一般至少会在团队中落实命名规范、异常处理规范、架构规范。 这3个规范可以有效地帮助团队治理代码复杂度。

命名规范

每个开发团队都应该有自己的命名规范,确保命名的一致性。对于核心的领域概念,应该有一个核心领域词汇表,确保这些领域词汇在代码中的表达是一致的.

在一个商品项目中一个和库存相关的逻辑,在短短的一段代码中,就有3种不同的对库存的描述方式,分别是Stock、Inventory、Sellable Amout。这意味着对同一个领域概念,需要理解3遍,这极大地增加了我的记忆负担和认知成本

如果团队有一个命名规范——核心领域词汇表,加上团队成员的共同遵守和维护,其实上述问题是可以避免的

在项目之初就整理出核心的领域概念,然后用中英文的形式,把这些概念放到设计文档中。要求英文的原因是,需要保持领域概念从文档到代码的一致性。 这种方式一方面规范了项目代码的命名,另一方面也让其他的技术人员更深入地理解了该业务。


异常处理规范

在异常处理方面,我们可以分别从服务提供者和服务调用者两个视角去看

服务提供者的异常处理

很多系统都没有异常规范,主要体现在没有统一的异常处理,以及对异常码没有规定。这种规范的缺失会导致系统中到处充斥着try-catch-throw的代码片段。这种代码缺少美感,会影响我们阅读代码的体验,割裂思考的连续性。

  • 对于异常,建议最好采用failfast(快速失败)的策略,即一旦发生异常,则终止当前的处理流程,抛出异常即可,然后在最外层进行统一的异常处理。不要在内部进行过多的try-catch,也不要把异常包装成返回值,这些都会额外增加关于异常的处理成本。

  • 对于业务系统而言,基本上只需要定义两类异常(Exception),一个是BizException(业务异常),另一个是SysException(系统异常),而且这两个异常都应该是Unchecked Exception

  • 不建议使用Checked Exception,是因为它破坏了开闭原则。如果你在一个方法中抛出了Checked Exception,而catch语句在3个层级之上,那么你需要在catch语句和抛出异常处理之间的每个方法签名中声明该异常。这意味着对软件中较低层级的修改都将波及较高层级,修改好的模块必须重新构建、发布,即便它们自身所关注的任何东西都没被改动过。这也是C#、Python和Ruby都不支持Checked Exception的原因,因为其依赖成本要高于显式声明带来的收益。

  • 最后,针对业务异常和系统异常要做统一的异常处理,类似于AOP,在应用处理请求的切面上进行异常处理收敛

因为我们只有一个异常类型BizException,所以针对不同的异常,需要通过不同的异常码(ErrorCode)或者不同的异常消息(ErrorMessage)来做区分。ErrorCode也需要有规范做约束,但要不要使用ErrorCode,使用什么样的ErrorCode,要视具体场景而定。

对于平台、底层系统或软件产品,可以采用编号式的编码规范,这种编码的好处是编码风格固定,给人一种正式感;缺点是必须要配合文档,才能理解错误码代表的意思.

 ORA-00001:违反唯一约束条件。
• ORA-00017:请求会话以设置跟踪事件。
• ORA-00018:超出最大会话数。
• ORA-00019:超出最大会话许可数。
• ORA-00023:会话引用进程私用内存;无法分离会话。
• ORA-00024:单一进程模式下不允许从多个进程注册。

然而对于大部分的业务系统而言,完全没有必要采用这种正式的ErrorCode编码风格。因为你不是一个通用系统,使用者只是你的上游系统,因此简单高效处理就好,可以选择不要ErrorCode,仅用ErrorMessage来传达错误信息;或者使用一个基于字符串的、自明的ErrorCode编码,譬如业务系统中有需要表示为“客户姓名不能为空”的ErrorCode,将其定义为CustomerNameIsNull即可。


服务调用者的异常处理

在分布式环境下,一个功能往往需要多个服务的协作才能完成。在使用远程服务的过程中,难免会出现各种异常,比如网络异常、服务自身的异常等。对于那些对可用性要求非常高的场景,我们有必要制定一个服务重试、服务降级的策略,以便当其中一个服务不可用的时候,我们仍然能够对外提供服务。

对于服务重试(Retry),我们需要查看异常种类,如果是BizException,则说明是服务调用者自己的问题(缺少参数、参数不正确,或者不满足业务规则),这时不需要重试;如果是SysException或者其他的系统Error,则可以进行重试。

如果重试仍然不能解决问题,可以考虑服务降级。


架构规范

COLA , Clean Object-oriented and Layered Architecture “整洁的面向对象分层架构”


软件设计中的标准

Java规范

对于从事Java后端开发的人员来说,一定对JCP(Java Community Process)不陌生,JCP是一个开放的国际组织,维护的规范包括J2ME、J2SE、J2EE、XML、OSS、JAIN等。组织成员可以提交Java规范请求(Java Specification Requests, JSR),JSR是由JCP成员向委员会提交的Java发展议案,经过一系列流程后,如果通过,最终会体现在未来版本的Java中。

比如: JSR-315是一份详细的规范说明,其中对Servlet的概念、相关Interface(接口),以及如何实现Servlet都给出了明确的定义和规定,但是并没有限制要如何实现
在这里插入图片描述

得益于这样的标准规范设计,不管是Tomcat、Jetty、JBoss,所有基于Servlet的应用程序都可以无缝地运行在任何一个Servlet容器中。只有这样,SpringMVC才是一个通用的MVC框架,它的实现不依赖于具体的Web容器


API设计标准

好的系统架构离不开好的API设计,一个设计不够完善的API注定会导致系统的后续发展和维护非常困难。良好的API设计要至少遵循3个标准,分别是可理解性、封装性和可扩展性

  • 可理解性: 代码首先是写给人读的,其次才是被机器运行的 。 API是系统的门面,是使用者了解系统的窗口。一个清晰可理解的API可以显著降低认知成本,提升团队之间的协作效率

  • 封装性: API应该是对实现细节的封装,也就是说,好的API设计和好的类设计是类似的,即不应该过多地暴露实现细节,只对外暴露可见的公共(public)部分即可。 封装性做得不好的原因大致有两个,一个是技术实现细节的暴露,另一个是内部业务语义的外泄。

  • 可扩展性: API作为一个重要契约,一旦发布出去,想要修改就会变得比较困难。被外部使用越多的API,其变动越困难。因此,我们在设计API的时候,最好预留一定的扩展性,以备不时之需


依赖契约的扩展机制

软件中的扩展机制基本也是依赖契约完成的。扩展的实现方式一般有两种,

  • 一种是基于接口的扩展,这是依赖接口的契约;
  • 另一种是基于配置数据的扩展,这是依赖数据的契约。

基于接口的扩展

基于接口的扩展主要利用面向对象的多态机制,需要先在组件中定义一个接口(或抽象方法)和处理该接口的模板,然后由用户实现自己的定制。
在这里插入图片描述

这种扩展方式在框架设计中被广泛使用。例如Spring中的ApplicationListener,用户可以用Listener实现容器初始化之后的特殊处理。再比如logback中的AppenderBase,用户可以通过继承AppenderBase实现定制的Appender诉求(向消息队列发送日志)


Spring ApplicationListenerLogback AppenderBase 代码示例,展示基于接口的扩展机制

1. Spring ApplicationListener 扩展示例

接口定义 (框架提供)

Spring 框架中定义的监听器接口:

// Spring框架定义的接口,用户需要实现它
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E event);
}
用户自定义实现 (扩展点)

用户实现 ApplicationListener 监听容器刷新事件:

// 用户自定义监听器,监听容器初始化完成事件
@Component
public class CustomApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 容器初始化完成后执行的定制逻辑
        System.out.println("Spring容器初始化完成!执行自定义初始化操作...");
        // 例如:加载缓存、启动定时任务等
    }
}
框架调用逻辑 (Spring内部实现)

Spring 容器在初始化完成后,会自动发布 ContextRefreshedEvent 事件,并调用所有注册的监听器:

// 伪代码:Spring框架的事件广播逻辑
public void publishEvent(ApplicationEvent event) {
    for (ApplicationListener<?> listener : listeners) {
        invokeListener(listener, event); // 调用监听器的 onApplicationEvent 方法
    }
}

2. Logback AppenderBase 扩展示例

抽象类定义 (框架提供)

Logback 框架中定义的 AppenderBase 抽象类:

// Logback框架提供的抽象类,用户需要继承它
public abstract class AppenderBase<E> implements Appender<E> {
    // 基础方法(框架已实现)
    public void start() { /* 初始化资源 */ }
    public void stop() { /* 释放资源 */ }
    
    // 抽象方法需要用户实现
    protected abstract void append(E event);
}
用户自定义 Appender (扩展点)

用户继承 AppenderBase 实现将日志发送到消息队列:

// 用户自定义Appender,将日志发送到消息队列
public class MQAppender extends AppenderBase<ILoggingEvent> {
    private String queueName; // 可扩展配置参数
    
    @Override
    protected void append(ILoggingEvent event) {
        // 将日志事件发送到消息队列
        String logMessage = event.getFormattedMessage();
        sendToMQ(queueName, logMessage); // 伪代码:实际需调用MQ客户端API
    }

    // 支持配置注入
    public void setQueueName(String queueName) {
        this.queueName = queueName;
    }
}
配置文件 (约定契约)

logback.xml 中配置自定义 Appender:

<configuration>
    <appender name="MQ_APPENDER" class="com.example.MQAppender">
        <queueName>log_queue</queueName>
    </appender>

    <root level="INFO">
        <appender-ref ref="MQ_APPENDER" />
    </root>
</configuration>

扩展机制核心特点

特点说明
契约定义框架定义接口/抽象类,明确扩展点的方法签名和调用规则
实现隔离扩展实现与框架逻辑解耦,通过多态动态绑定
配置化可通过配置文件(如XML、注解)声明扩展实现,无需修改框架代码
开放闭合原则框架对扩展开放(允许新增Appender/Listener),对修改闭合(无需改动框架核心逻辑)

这种模式通过 接口契约 + 配置化 实现了灵活的扩展能力,是框架设计中常用的扩展点设计方法。


基于配置数据的扩展

基于配置数据的扩展,首先要约定一个数据格式,然后利用用户提供的数据组装成实例对象,用户提供的数据是对象中的属性(有时也可能是类,比如SLF4J中的StaticLoggerBinder

在这里插入图片描述
我们一般在应用中使用键值对(key-value pair)的方式来配置需要修改的属性。这里契约发挥的作用是,配置文件的格式及数据值必须遵循设计的约定,否则配置不能按照预期发挥作用。

比如要使用logback,就必须按照logback的约定去设置logback.xml。如果破坏了这个契约,那么logback就不能正确地从配置文件中解析用户的配置。

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
    
    <!-- 定义变量 -->
    <property name="LOG_PATH" value="logs" />
    <property name="APP_NAME" value="myapp" />
    <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
    
    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 仅当在开发环境时启用(示例条件) -->
        <!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>DEBUG</level>
        </filter> -->
    </appender>

    <!-- 文件滚动记录 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APP_NAME}.log</file>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 每日滚动 + 文件大小限制 -->
            <fileNamePattern>${LOG_PATH}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory> <!-- 保留30天 -->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <!-- 错误日志单独记录 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/error.log</file>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/error-%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>90</maxHistory>
        </rollingPolicy>
    </appender>

    <!-- 日志级别配置 -->
    <logger name="com.example" level="DEBUG" /> <!-- 指定包日志级别 -->
    <logger name="org.springframework" level="WARN" />

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
        <appender-ref ref="ERROR_FILE" />
    </root>

</configuration>

当然,扩展点自身是一种接口扩展,而获取扩展点的实现方式是数据扩展。


以下是 基于配置数据的扩展 的代码示例,通过约定数据格式(如键值对、类名等)实现扩展能力:


示例 1:数据库连接池配置(通过属性文件约定)

数据契约定义

框架约定配置文件格式(如 database.properties),要求必须包含以下键值对:

# 必须包含以下 key,且值类型合法
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=123456
jdbc.pool.maxSize=20
框架解析逻辑

框架读取配置文件并封装为配置对象:

public class DatabaseConfig {
    private String url;
    private String username;
    private String password;
    private int maxPoolSize;

    // 通过反射或手动解析 properties 填充属性
    public void load(Properties props) {
        this.url = props.getProperty("jdbc.url");
        this.username = props.getProperty("jdbc.username");
        this.password = props.getProperty("jdbc.password");
        this.maxPoolSize = Integer.parseInt(props.getProperty("jdbc.pool.maxSize"));
    }

    // 根据配置创建数据源
    public DataSource createDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(url);
        config.setUsername(username);
        config.setPassword(password);
        config.setMaximumPoolSize(maxPoolSize);
        return new HikariDataSource(config);
    }
}
用户使用方式

用户只需修改配置文件即可扩展连接池行为,无需修改代码

# 用户修改配置文件调整行为
jdbc.pool.maxSize=50  # 扩展连接池容量

示例 2:动态策略加载(通过类名约定)

数据契约定义

框架约定在配置文件中指定策略实现类的全限定名(如 strategy-config.xml):

<strategies>
    <payment-strategy class="com.example.AlipayStrategy"/> <!-- 必须实现 PaymentStrategy 接口 -->
</strategies>
框架解析逻辑

框架读取配置并动态加载类:

public interface PaymentStrategy {
    void pay(double amount);
}

public class StrategyFactory {
    public static PaymentStrategy createStrategy(String className) {
        try {
            Class<?> clazz = Class.forName(className);
            return (PaymentStrategy) clazz.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException("Failed to load strategy: " + className, e);
        }
    }
}

// 使用示例
PaymentStrategy strategy = StrategyFactory.createStrategy("com.example.AlipayStrategy");
strategy.pay(100.0);
用户扩展方式

用户新增策略实现类并修改配置文件:

// 用户新增微信支付策略
public class WechatPayStrategy implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("微信支付: " + amount);
    }
}

修改配置文件即可切换策略:

<strategies>
    <payment-strategy class="com.example.WechatPayStrategy"/> <!-- 切换实现类 -->
</strategies>

示例 3:SLF4J 日志绑定(通过类名和静态方法约定)

数据契约定义

SLF4J 要求用户必须在类路径中提供 org.slf4j.impl.StaticLoggerBinder 类,且该类必须实现以下方法:

public class StaticLoggerBinder {
    public static StaticLoggerBinder getSingleton() { /* ... */ }
    public ILoggerFactory getLoggerFactory() { /* ... */ }
}
用户扩展实现

日志框架(如 Logback)遵守契约提供实现:

// Logback 提供的 StaticLoggerBinder(位于 logback-classic 库中)
package org.slf4j.impl;

public class StaticLoggerBinder {
    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
    
    public static StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }

    public ILoggerFactory getLoggerFactory() {
        return new LoggerContext(); // Logback 的上下文实现
    }
}
框架调用逻辑

SLF4J 通过静态绑定加载用户提供的实现:

// SLF4J 的 LoggerFactory 类(框架侧)
public final class LoggerFactory {
    private static ILoggerFactory getILoggerFactory() {
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
    }
}

关键设计对比

设计要素基于接口的扩展基于配置的扩展
扩展方式实现接口/继承抽象类修改配置文件
契约形式接口方法签名配置格式(键名、类名、方法名)
运行时变化需重新编译动态生效(如热更新)
典型场景框架插件、事件监听参数调整、策略切换、实现绑定

核心特点

  1. 数据即扩展:通过修改外部化配置(而非代码)改变系统行为
  2. 约定大于配置:必须严格遵循键名、类名、方法名等约定
  3. 动态性:许多框架支持配置热加载(如 Spring Cloud Config)

通过这种模式,用户只需遵守数据格式契约,即可灵活扩展系统能力,典型应用包括数据库连接池参数调整、日志实现切换、支付策略动态配置等。
在这里插入图片描述


http://www.kler.cn/a/557219.html

相关文章:

  • MATLAB进阶之路:数据导入与处理
  • 【c语言】函数_作业详解
  • 一文读懂大模型文件后缀名,解锁 AI 世界的密码
  • 探索Android动态埋点的新视界:UprobeStats深度解析
  • 解决“error: Tried to call obs_frontend_start_virtualcam with no callbacks!”
  • 计算机视觉算法实战——智能零售货架监测(主页有源码)
  • 83_CentOS7通过yum无法安装软件问题解决方案
  • 基于springboot的攀枝花市鲜花销售系统
  • 【论文阅读】identifying backdoor data with optimized scaled prediction consistency
  • 蓝桥杯真题 - 缴纳过路费 - 题解
  • 氧化锆(化学式ZrO₂)在多个工业领域发挥重要作用京煌科技
  • 机器学习 - 投票感知器
  • VUE四:Vue-cli
  • Android 中 如何监控 某个磁盘有哪些进程或线程在持续的读写
  • 【WebGL】fbo双pass案例
  • SpringAI系列 - ToolCalling篇(二) - 如何设置应用侧工具参数ToolContext(有坑)
  • hot100-滑动窗口
  • ctfshow——phps源码泄露
  • Tio-Boot 集成 Spring Boot 实现即时通讯功能全解析
  • 深度学习图像预处理可视化:拆解Compose操作的全过程