六边形架构给我带来了什么

本贴最后更新于 553 天前,其中的信息可能已经时异事殊

前瞻

我们公司的项目是使用 Java 语言 Springboot 框架开发的,由于项目诞生较早没经过设计所以一直是以最简单的 Controller,Service,Repository,Entity 的架构跑到了如今,恰逢美国对中国的技术封锁日益加剧,国家推出了中国信创产业发展白皮书,所以一些技术需要进行国产化,国内也涌出众多国产化的产品,如: TongWeb, OpenGauss,kingBase,Oceanbase 等。这篇文章不对这些国产化产品进行评价。只是由于信创的要求,大量客户要求我们厂家也需要支持国产化的组件。而一个优秀的软件架构可以省下非常多的工作量。

当前的形势

没错,对于老代码的屎山,高耦合就是唯一的特点。底层使用的 postgresql 数据库。以前我也发过一篇文章

信创兼容 kingBase 数据库,kingBase 是基于 postgres9.6 开发的所以没什么难度,但是这次我们要兼容 Oceanbase,这个数据库是基于 Mysql 开发的,由于 mysql 和 postgres 在语法层面的不同和不同的支持类型,同时兼容几乎是不可能的。所以我们需要改变我们目前的软件的设计架构,也就是六边形架构。

什么是六边形架构

六边形架构(Hexagonal Architecture),又称为端口和适配器架构(Ports and Adapters Architecture),是一种软件架构风格,旨在实现松耦合、可测试和可扩展的应用程序。它强调将业务逻辑与外部依赖(例如数据库、UI、外部服务等)解耦,以便更容易进行替换、测试和演化。

六边形架构的核心思想是将应用程序划分为内部核心(Core)和外部适配器(Adapters)两个主要部分:

  1. 内部核心:内部核心是应用程序的主要业务逻辑和领域模型的集中部分。它包含业务实体、值对象、领域服务和业务规则等,以实现特定的业务需求。内部核心是独立于外部环境的,不依赖于具体的技术实现或外部系统。
  2. 外部适配器:外部适配器是将内部核心与外部依赖(例如数据库、UI、外部服务等)连接起来的部分。它负责将外部数据转换为内部核心所需的格式,并将内部核心的输出适配为外部依赖所需的格式。外部适配器还负责处理与外部环境的交互,例如数据访问、UI 呈现、外部服务调用等。

当然万变不离其宗,假如我们能够真正理解和做到 高内聚低耦合,那么我相信不管什么架构都可以对突然的需求和业务进行有条不紊的应对。

新的项目结构

我不是一个理论学家,方法论讲完了,就开始来一个 demo 吧

src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── hazelcastspb
    │   │               ├── HazelcastSpbApplication.java
    │   │               ├── app
    │   │               │   └── rest
    │   │               │       └── InterfaceController.java
    │   │               ├── domain
    │   │               │   ├── Interface.java
    │   │               │   ├── repository
    │   │               │   │   └── InterfaceRepository.java
    │   │               │   └── service
    │   │               │       ├── DomainInterfaceService.java
    │   │               │       └── InterfaceService.java
    │   │               └── infra
    │   │                   ├── config
    │   │                   │   ├── BeanConfiguration.java
    │   │                   │   └── HazelcastClientConfig.java
    │   │                   └── repository
    │   │                       ├── config
    │   │                       │   └── EntityConfiguration.java
    │   │                       ├── mysql
    │   │                       │   ├── InterfaceEntity.java
    │   │                       │   ├── MySQLInterfaceRepository.java
    │   │                       │   └── SpringDataInterfaceRepository.java
    │   │                       └── postgresql
    │   │                           ├── InterfaceEntity.java
    │   │                           ├── PSQLInterfaceRepository.java
    │   │                           └── SpringDataInterfaceRepository.java

可以看到项目分成了应用层领域层基础架构层,基础架构层负责底层的数据库交互操作,领域层则基于基础架构层提供的数据进行业务逻辑计算,这个 demo 比较简单,所以这里就不赘述 DDD 部分的内容。

数据源切换

我使用的是 Spring data JPA, 数据源的切换我们需要解决如下问题

为什么会有两个 InterfaceEntity?

因为我的 Interface 对象的某些字段在不同数据库中的类型不同。当然如果都一样可以使用一个·Entity ·。

package com.example.hazelcastspb.domain;
//略


@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@MappedSuperclass
public class Interface implements Serializable {
    @Id
    @GeneratedValue(generator = "jpa-uuid")
    @Column(name = "id")
    protected UUID id;
    @Column(name = "urn")
    protected String urn;
    @Column(name = "device")
    protected String device;
    @Column(name = "name")
    protected String name;
    @Column(name = "alias")
    protected String alias;
    @Column(name = "description")
    protected String description;
    @Column(name = "mtu")
    protected int mtu;
    @Column(name = "mac_address")
    protected String macAddress;
    @Column(name = "vlan_id")
    protected String vlanId;
    @Column(name = "vsys")
    protected String vsys;
    @Column(name = "vrf")
    protected String vrf;
    @Column(name = "zone")
    protected String zone;
    @Column(name = "text")
    protected String text;
    @Transient
    private Set<String> ips;
    @Transient
    private Map<String, Object> extra;
    @Transient
    protected Boolean enabled;

}

在 domain 包下的 Interface 类,绝大部分的字段 Postgres 和 Mysql 都是一致的,除了 ips,extra,enabled,所以我将这三个字段加上了 @Transient 注解。

mysql 包下的 InterfaceEntity

package com.example.hazelcastspb.infra.repository.mysql;
//略

/**
 * @author fangcong
 * @version 0.0.1
 * @since Created by work on 2023-06-16 11:28
 **/
@Entity(name = "mysql_interface")
@Table(name = "interface")
@TypeDef(name = "json", typeClass = JsonStringType.class)
public class InterfaceEntity extends Interface {

    @Column(name = "enabled", columnDefinition = "TINYINT(1)")
    private Boolean enabled;

    @Type(type = "json")
    @Column(name = "ips", columnDefinition = "json")
    private Set<String> ips;
    @Type(type = "json")
    @Column(name = "extra", columnDefinition = "json")
    private Map<String, Object> extra;
}

postgresql 包下的 InterfaceEntity

package com.example.hazelcastspb.infra.repository.postgresql;
//略

/**
 * @author fangcong
 * @version 0.0.1
 * @since Created by work on 2023-06-16 11:28
 **/
@Entity(name = "pg_interface")
@Table(name = "interface")
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
public class InterfaceEntity extends Interface {

    @Column(name = "enabled")
    private Boolean enabled;
    @Type(type = "jsonb")
    @Column(name = "ips", columnDefinition = "jsonb")
    private Set<String> ips;
    @Type(type = "jsonb")
    @Column(name = "extra", columnDefinition = "jsonb")
    private Map<String, Object> extra;
}

可以注意到,布尔值和json类型 在两种数据库中的展现形式是不一样的。你们更喜欢哪种呢?我是 postgres 的粉丝。

两个 InterfaceEntity 怎么动态加载?

如果你懂 JPA 的话,就知道实体也就是加上了 @Entity 的类的加载是不能像 Spring 的 bean 一样通过 @Conditional 这样的方式来实现动态加载的,你必须指定好当前数据源要加载的实体的包。所以我们可以通过当前数据源加载的驱动类来判断。关于实体工厂类这部分在另一篇文章里有提到 Springboot 配置多数据源
直接贴代码:

package com.example.hazelcastspb.infra.repository.config;
//略

/**
 * @author fangcong
 * @version 0.0.1
 * @since Created by work on 2023-06-16 15:01
 **/
@Configuration
public class EntityConfiguration implements ApplicationContextAware {

    public static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext context) {
        applicationContext = context;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @Bean(name = "entityManager")
    public EntityManager entityManager(EntityManagerFactoryBuilder builder,DataSource DataSource) {
        return Objects.requireNonNull(entityManagerFactoryBean(builder, DataSource).getObject()).createEntityManager();
    }

    @Bean("entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder, DataSource dataSource) {
        var ds = (HikariDataSource) dataSource;
        final String driverClassName = ds.getDriverClassName();
        if (driverClassName.equals("org.postgresql.Driver")) {
            return builder.dataSource(dataSource)
                    .properties(properties())
                    .packages("com.example.hazelcastspb.infra.repository.postgresql")
                    .build();
        } else {
            return builder.dataSource(dataSource)
                    .properties(properties())
                    .packages("com.example.hazelcastspb.infra.repository.mysql")
                    .build();
        }

    }

    private Map<String, String> properties() {
        Map<String, String> jpaProperties = new HashMap<>(16);
        jpaProperties.put("hibernate.hbm2ddl.auto", "update");
        jpaProperties.put("hibernate.show_sql", "true");
        return jpaProperties;
    }

}

动态加载 Bean

通过 spring@ConditionalOnClass 来进行 bean 的动态加载。

package com.example.hazelcastspb.infra.repository.mysql;
//略

/**
 * @author fangcong
 * @version 0.0.1
 * @since Created by work on 2023-06-16 11:51
 **/
@Repository
@ConditionalOnClass(name = {"com.mysql.cj.jdbc.Driver"})
public interface SpringDataInterfaceRepository extends JpaRepository<InterfaceEntity, UUID> {

    List<InterfaceEntity> findByDevice(String device);

    InterfaceEntity findByName(String name);

}
package com.example.hazelcastspb.infra.repository.postgresql;
//略

/**
 * @author fangcong
 * @version 0.0.1
 * @since Created by work on 2023-06-16 11:51
 **/
@Repository
@ConditionalOnClass(name = {"org.postgresql.Driver"})
public interface SpringDataInterfaceRepository extends JpaRepository<InterfaceEntity, UUID> {

    List<InterfaceEntity> findByDevice(String device);

    InterfaceEntity findByName(String name);
}

最终返回一个 InterfaceServicebean 提供给领域层使用

package com.example.hazelcastspb.infra.config;

//略

@Configuration
public class BeanConfiguration implements ApplicationContextAware {

    public static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext context) {
        applicationContext = context;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @Bean
    @Primary
    @ConditionalOnClass(name = {"com.mysql.cj.jdbc.Driver"})
    InterfaceRepository initMysqlRepository(com.example.hazelcastspb.infra.repository.mysql.SpringDataInterfaceRepository repository) {
        return new MySQLInterfaceRepository(repository);
    }

    @Bean
    @ConditionalOnClass(name = {"org.postgresql.Driver"})
    InterfaceRepository initPgRepository(SpringDataInterfaceRepository repository) {
        return new PSQLInterfaceRepository(repository);
    }

    @Bean
    InterfaceService interfaceService(InterfaceRepository repository) {
        return new DomainInterfaceService(repository);
    }
}

结尾

这样的话,通过分层和面向领域的接口开发降低了对于数据库的耦合度。当然这个例子还可以进一步优化。

  1. 我们使用的是 JPA,对于大部分的增删改查都有 Hibernate 帮我们做,每支持一种数据库就多加一个 SpringDataInterfaceRepository 是不是有点傻?
  2. 简单的实体完全可以通用一个,该怎么修改?

大家可以通过评论区或者私信跟我进行讨论。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    142 引用 • 442 回帖 • 1 关注
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    943 引用 • 1460 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...