如何实现优雅解耦 三层架构

本文主要基于java的Spring Boot 框架,探索在SpringBoot下不同模块间互相引用导致的依赖循环问题。

一、三层架构概述

对于三层架构,有几种不同的表示,但大致可以归类为以下三种。

  1. 表示层Presentation Layer

    • 视图View :提供UI界面与用户进行交互

    • 控制器Controller:接收用户的请求并协调调度其他层的处理

  2. 业务逻辑层Business Logic Layer

    • 服务层Service:协调数据访问层,处理业务逻辑

  3. 数据访问层Data Access Layer

    • 数据访问对象DAO 或者 Repository:负责与数据库进行交互

二、三层架构的循环依赖问题

这里的模块以数据表作为划分依据,例如:user表就为user模块,product表就为product模块

作者在原先的业务开发中,Controller层中几乎只充当一个路由的作用,然后直接调用Service层的某一方法,做一个甩手掌柜。而DAO层一般只提供与数据库的交互,所以会在服务层Service封装很多方法(例如:封装了可以走缓存的方法,对于用户存不存在的判别等等)。这样就导致了所有的业务处理都积压在了Service层上,现在有Aservice,Bservice,Amapper,Bmapper。如果A要调用B模块的相关内容,Aservice中就会使用Bservice作为依赖注入。然而,如果有一天Bservice又要使用A模块的内容,就得只能退而求其次,使用Amapper作为依赖注入。来避免一个循环依赖的问题。

作者深受其扰,在一个service中参杂着不同的mapper和不同的service,实在是太恶心了。于是,决定探索在SpringBoot的三层架构中优雅解决互相依赖的问题。

三、不同解耦方式的探索

我们先约定一个场景和相关的类。

场景:我们要为设计一个电商后台管理系统,来管理平台的店家,商品分类,商品等。下文也将基于此场景

类:

@Data
public class Shop { //电商平台不同的店铺
    private Integer shopId;
    private String shopCode;
    private String shopName;
    //.... 省略其他信息
}
​
@Data
@Accessors(chain = true)
public class User { //电商用户类
    private Integer userId;
    private String username;
    private String pwd;
    private String nickName;
    private Integer shopId; //每个电商用户隶属于不同的店铺
    //...省略其他参数
}
​
@Data
public class ProductCategory { //商品分类
    private Integer productCatId;
    private Integer shopId; //每个店铺有自己的不同的商品分类
    private String productCatName;
    //...省略其他信息
}
​
@Data
public class Product {
    private Integer productId;
    private Integer productCatId; //每个商品隶属于不同的商品分类
    private String productName;
    private BigDecimal price;
​
    //...省略其他信息
}

1、使用控制器协调服务层

我们不再只让Controller只发挥它的路由,让它参与数据的过滤,处理,协调各个service。service只调用mapper,mapper充当最底层的功能。

这里给出新增用户的场景示例。

public interface UserMapper extends BaseMapper<User> {
}
public interface ShopMapper extends BaseMapper<Shop> {
}
​
@Service
@RequiredArgsConstructor
public class ShopServiceImpl implements ShopService {
​
    private final ShopMapper shopMapper;
​
    @Override
    public boolean checkExist(Integer shopId) {
        return shopMapper.selectById(shopId) != null;
    }
​
    @Override
    @Cacheable(value = "shopName", key = "#shopId")
    public String getShopName(Integer shopId) {
        //..........其他逻辑,这里简单使用缓存注解代替
        return shopMapper.selectById(shopId).getShopName();
    }
}
​
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
​
    private final UserMapper userMapper;
​
    @Override
    public void addUser(User user) {
        userMapper.insert(user);
    }
​
    @Override
    @Cacheable(value = "user", key = "#id")
    public User getUser(Integer id) {
        //省略其他逻辑
        return userMapper.selectById(id);
    }
}
​

我们定义如上代码的相关mapper和service,各个service专注于自己的事,只使用mapper当作底层架构。

然后,我们的Controller也同理,将service当作底层架构,只调用不同service提供的服务。

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
​
    private final UserService userService;
​
    private final ShopService shopService;
​
    @PostMapping
    public R<Void> addUser(@RequestBody User user) { //为了举例方便,直接使用DO接收
        if (!shopService.checkExist(user.getShopId())) {
            //.........其他逻辑
            throw new RuntimeException("店铺不存在");
        }
        userService.addUser(user);
        return R.ok();
    }
​
    @GetMapping("/{userId}")
    public R<UserVo> getUser(@PathVariable Integer userId) {
        User user = userService.getUser(userId);
        if (user == null) {
            //...........其他逻辑
            return R.fail();
        }
        UserVo userVo = new UserVo(user);
        //调用 shopService中添加了缓存的get方法进行渲染
        userVo.setShopName(shopService.getShopName(user.getShopId()));
        return R.ok(userVo);
    }
​
}
​
@Data
public class UserVo extends User {
​
    private String shopName;
​
    public UserVo(User user) {
        this.setUserId(user.getUserId());
        this.setUsername(user.getUsername());
        this.setPwd(user.getPwd());
        this.setNickName(user.getNickName());
        this.setShopId(user.getShopId());
    }
}

这样,我们就实现了一个优雅的解耦,让service层变得不再臃肿,提高了代码的复用率。

不过,我们一定要确保上层架构做足了数据处理

2、在服务层抽象出公共服务层

如果,我们不希望Controller做如此多的业务内容。可以尝试从service中抽象出公共service和业务service,业务service不再调用mapper,而只调用公共service,使用公共service提供的方法。然后controller在协调调用业务service。

我们采用如上图的架构,将原先ProductService细分为ProductCommonService和ProductService(NEW)

将ProductCatService细分为ProductCatCommonService提供基础服务。

然后使用ProductService(NEW)联调 ProductCommonService 和 ProductCatCommonService。

在service层进行了更细致的划分,具体的类关系如下

@Service
@RequiredArgsConstructor
public class ProductCatCommonServiceImpl implements ProductCatCommonService {
​
    private final ProductCategoryMapper productCategoryMapper;
​
    @Override
    public boolean checkExist(Integer productCatId) {
        return productCategoryMapper.selectById(productCatId) != null;
    }
}
​
@Service
@RequiredArgsConstructor
public class ProductCommonServiceImpl implements ProductCommonService {
​
    private final ProductMapper productMapper;
​
    @Override
    public void save(Product product) {
        productMapper.insert(product);
        //.....其他逻辑
    }
}
​
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
​
    private final ProductCommonService productCommonService;
    private final ProductCatCommonService productCatCommonService;
​
    @Override
    public void addProduct(Product product) {
        if (!productCatCommonService.checkExist(product.getProductCatId())) {
            //....其他逻辑
            throw new RuntimeException("商品分类不存在");
        }
        //....其他校验逻辑
        productCommonService.save(product);
    }
}

不过优劣也很明显:减轻了Controller的工作,细化了代码职责,强化了底层的构筑。但是导致了类的膨胀,抽出公共service的做法相当于又多了一个协调层来代替原先Controller应该做的工作,加大了代码量。

3、使用注解进行数据渲染

一般情况下,我们不能直接将数据库的实体类对象直接返回给前端。我们将其封装为VO类,而VO类一般需要对数据的关联项进行渲染。

举个例子,我们的Product中具有 product_cat_id 这一属性,我们需要将其渲染为具体的 cat_name,然后我们还可能需要拿到 product的所属店铺。

也就是封装成如下的类。

@Data
public class ProductVo {
​
    private Integer productId;
​
    private String productName;
​
    private Integer productCatId;
​
    private String productCatName;
​
    private Integer shopId;
​
    private String shopName;
​
    private BigDecimal price;
}
​
@Data
public class Product {
​
    private Integer productId;
​
    private Integer productCatId;
​
    private String productName;
​
    private BigDecimal price;
​
    //省略其他信息
}

一般写法下,我们将在一个方法中,显示的协调调用各个service或者mapper中提供好的服务,来查询出相关信息进行一个封装。

一个提供基础服务的方法可能被多处进行使用,为了解决这种重复的劳动,作者开发了一款使用简单功能但功能强大的框架,做个使用实例。

相关文档链接:

Easy-Translation

目前我们的getDetailById方法没有做任何的动作,而调用相关接口的返回对象也没有对非Product的属性做任何的填充。

    @Override
    public ProductVo getDetailById(Integer id) {
        Product product = productCommonService.getById(id);
        return new ProductVo(product);
    }

在以往的代码开发中,我们需要引入ProductCatService和ShopService,才能对ProductVo的catName,shopId,shopName进行渲染。

我们在其他服务中已经写好了相关方法。

    @Override
    public ProductCategory getById(Integer id) {
        return productCategoryMapper.selectById(id);
    }
    
    @Override
    @Cacheable(value = "shopName", key = "#shopId")
    public String getShopName(Integer shopId) {
        //..........其他逻辑,这里简单使用缓存注解代替
        return shopMapper.selectById(shopId).getShopName();
    }

我们现在要做的就是,让他们成为翻译器,然后在想要执行翻译的字段上使用该翻译器,最后挑选一个喜欢的地方执行翻译即可。

说的复杂,但是3个注解就足以实现。

    @Override
    @Cacheable(value = "productCatName", key = "#id")
    @Translator("id_to_product_category")
    public ProductCategory getById(Integer id) {
        return productCategoryMapper.selectById(id);
    }
​
    @Override
    @Cacheable(value = "shopName", key = "#shopId")
    @Translator("id_to_shop_name")
    public String getShopName(Integer shopId) {
        //..........其他逻辑,这里简单使用缓存注解代替
        return shopMapper.selectById(shopId).getShopName();
    }

在这两个非常常用的方法,添加 @Translator注解,将其注册为翻译器。

然后,在我们的Vo类上,使用@Mapping注解对productVo的相关字段进行翻译。

@Data
public class ProductVo {
​
    private Integer productId;
​
    private String productName;
​
    private Integer productCatId;
​
    @Mapping(translator = "id_to_product_category",mapper = "productCatId", receive = "productCatName",sort = 0)
    private String productCatName;
​
    @Mapping(translator = "id_to_product_category",mapper = "productCatId", receive = "shopId",sort = 0)
    private Integer shopId;
​
    @Mapping(translator = "id_to_shop_name",mapper = "shopId", sort = 1)
    private String shopName;
​
    private BigDecimal price;
​
    public ProductVo(Product product) {
        this.productId = product.getProductId();
        this.productName = product.getProductName();
        this.productCatId = product.getProductCatId();
        this.price = product.getPrice();
    }
}
​

@Mapping中的mapper属性是为数组,可以获取该对象的其他属性然后传递给翻译器,receive属性是接收翻译器翻译结果的某一属性。

可以注意到,shopId和productCatName调用了同一翻译器,且mapper指定一致,说明我们可以只调用一次方法,就可以获取这两个属性。而翻译器确实也只会执行一次在这种情况下,所以开发者无需担心带来的IO问题。

我们可以使用sort控制翻译器的执行顺序,来做一个简单的编排。 当然,还有许多更加强大的功能,如异步翻译,回调翻译,条件补充翻译等作者不一一举例。

下一步,启用翻译

    @Override
    @TranslationExecute
    public ProductVo getDetailById(Integer id) {
        Product product = productCommonService.getById(id);
        return new ProductVo(product);
    }

增强我们的方法,让翻译器执行

当然,我们也可以在Controller层进行处理,起到的作用是一样的。

    @GetMapping("/{id}")
    @TranslationExecute(field = "data")
    public R<ProductVo> getProduct(@PathVariable Integer id){
        return R.ok(productService.getDetailById(id));
    }

各位开发者要是觉得还可以,希望大家可以去gitee或github上给个star,支持一下作者。

Easy-Translation

结语

以上就是作者对三层架构下,依赖混乱的处理方法和思考,这三中策略可以互相依存。希望对大家能有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值