编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

依赖倒置原则详解(对依赖倒置的表述错误的是)

wxchong 2024-10-27 15:50:41 开源技术 96 ℃ 0 评论

传统的分层架构无人不知、无人不晓。时至今日,仍然能见很多系统采用简单的分层设计,Controller负责封装UI层接口;Service层负责封装业务逻辑,Dao层负责数据库、缓存的访问等。

有分层有必然有依赖,上层组件依赖下层的组件来实现其功能和职责。这种层级间的依赖会产生什么问题呢?

我们来看一个例子,比如我们有一个订单的服务类,如下:

public class OrderService {

    @Resource
    private OrderMapper orderMapper;

    @Resource
    private RedisClient redisClient;

    @Resource
    private ESClient esClient;

    public void createNewOrder(Order order) {
        if (!isValidOrder(order)) {
            throw new IllegalArgumentException("订单数据不合法...");
        }

        // 存数据库
        orderMapper.add(order);

        // 存Reids
        redisClient.set(String.format("Order:%s", order.getId()), order.toJson());

        // 存ES
        esClient.index("orderIndex", order.toJson());
    }

    public Order queryByKey(long orderId) {
        String orderJson = redisClient.get(String.format("Order:%s", orderId));
        if (null != orderJson) {
            return Order.fromJson(orderJson);
        }

        return null;
    }

    public List<Order> searchOrders(OrderSearchCondition condition) {
        // ...
    }
}


这段代码有什么问题吗?新增的合法订单数据存到数据库中,通过Redis缓存来加快ID的查询性能,数据冗余到ES实现复杂场景的搜索。从功能上看它可能是正确的(这里我们先不谈多个中间件间的数据一致性),但从设计上看是有问题的:OrderService是业务层的组件,在这里它受到了底层组件(OrderMapper,redisClient和esClient)的影响。

试想一下如果ES Client的API发生了变更,我们将不得不修改OrderService类来适应;或者Redis缓存后仍然无法满足性能需求,我们想要通过JVM本地缓存的方法来进一步提升性能,也将不能不修改OrderService类。这是我们不希望见到的。

OrderService是业务抽象,它应该关心的是业务的逻辑(订单的合法性校验和持久化;根据ID可以获得一个订单对;根据条件可以检索到订单数据),而不是具体的实现过程,或者说不应该受到低层组件的影响,导致这个局面的根因就是依赖,高层组件依赖了低层组件

依赖倒置原则的定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。简而言之,依赖倒置原则要求我们通过抽象来将高层和低层模块连接起来,而不是直接依赖于具体的实现。

回到我们前面的例子,应该怎么做才算符合依赖倒置原则呢?

  • 我们需要把Business Layer看作抽象,它是核心业务逻辑的体现,应该与具体的技术解偶开来;
  • 之前是Business Layer依赖Persistence Layer,我们得换个思路反过来,让Persistence Layer实现Business Layer,即Persistence Layer依赖了Business Layer。这也是依赖倒置名称的由来。
  • Presentation Layer依赖Business Layer,保持不变即可。重点是所有的业务规则、业务逻辑都应该由Business Layer来完成,Presentation Layer只能负责像协议转换、出入参的封装和组装等业务无关的事务。

还原到代码层面具体怎么做呢,还是以OrderService为例,首先我们需要分析OrderService的业务,抽象一个符合业务规则的接口OrderDao:

public interface OrderDao {
    void createOrder(Order order);

    Order getByKey(long orderId);
    
    List<Order> searchByCondition(OrderSearchCondition condition);
}

这里要注意的是:1、OrderDao是属于抽象层的,它长什么样、提供怎么样的方法完全由抽象层的业务来决定;2、抽象层不关心谁来实现,也不关心如何实现。如何实现是技术层面要考虑的事,是否通过缓存提升性能、是否冗余到ES提升检索性能,抽象层并不care。这么做最大的好处是提升业务抽象层的业务纯度,使得业务复杂度和技术实现接近1:1,实现研发能效的最大化。

然后还需要对OrderService做一些小小的修改:

public class OrderService {

    @Resource
    private OrderDao orderDao;

    public void createNewOrder(Order order) {
        if (!isValidOrder(order)) {
            throw new IllegalArgumentException("订单数据不合法...");
        }

        orderDao.createOrder(order);
    }

    public Order queryByKey(long orderId) {
        return orderDao.getByKey(orderId);
    }

    public List<Order> searchOrders(OrderSearchCondition condition) {
        return orderDao.searchByCondition(condition);
    }
}


最后,我们需要在Persistence Layer实现OrderDao:

OrderDaoImpl实现OrderDao,它通过OrderMapper、RedisClient和ESClient按需要操控数据库、Redis缓存和ElasticSearch,重要的是这些实现细节对Business Layer层的OrderService来说完全透明了。如果ES Client的API发生了变更,或者我们想要通过JVM本地缓存的方法来进一步提升性能,只需要修改Persistence Layer的OrderDaoImpl就好了。

思维模式的转变:数据驱动 vs 领域驱动

数据驱动

传统分层架构流行是因为过去我们更多是采用数据驱动的方式在设计系统,拿到需求第一步往往想的是数据库如何设计,有哪些表,表之间的关系是什么,结合表的设计完成数据访问层的代码;再结合页面和交互设计和完成UI层的代码,最后才去拼拼凑凑的写完业务层。是一种自顶向下和自底向上相结合的方式,设计上偏重技术,代码与业务脱节,研发对业务的理解也容易出现偏差,因为关注点在数据和处理逻辑上。当系统出差各种问题时,往往难以改动。

领域驱动

  • 从业务本身出发,以业务为本质,提供问题空间和解决方案。
  • 采用由内向外的方式进行设计和开发,设计、代码与业务逻辑高度一致。
  • 团队内部更容易培养领域专家,技术可以与业务一起谈论问题和提供解决方案。
  • 数据库等基础设施,在领域内只是若干实现方式的一个选择,并不重要。


如果觉得这篇文章对您有帮助,别忘了点?关注!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表