首页 > 技术文章 > 设计原则之依赖倒置原则

likeguang 2022-04-11 16:53 原文

依赖倒置原则

一、简单介绍

依赖倒置原则:高层模块不应该依赖于底层模块,二者都应该来依赖抽象。抽象不应该依赖于细节,而细节应该依赖于抽象

依赖倒置原则是实现开闭原则的重要途径之一,它降低了类之间的耦合,提高了系统的稳定性和可维护性,同时这样的代码一般更加易读,而且便于传承。

二、模拟场景

在互联网中的营销活动中,经常为了拉新和促活,会做一些抽象活动。这些活动会随着业务的不断发展而调整,比如说:随机抽奖、权重抽象等。其中权重是指用户在当前系统中的一个综合排名,比如说活跃度、贡献度等等。

下面模拟出来一个抽象的系统服务,如果是初次搭建这样的系统怎么来进行实现?这个系统是否有良好的扩展性和可维护性,同时在变动和新增业务时测试的复杂度是否高?这些都是在系统服务设计时需要考虑的问题。

三、违反原则做法

用最直接的方式,即按照不同的抽奖逻辑定义出不同的接口,让外部的服务来进行调用。

public class BetUser {

    private String username;
    private int userWeight;

 	// get/setter方法
}

下面在一个类中用两个接口来进行实现:

public class DrawControl {

    /**
     * 随机返回
     * @param betUserList
     * @param count
     * @return
     */
    public List<BetUser> doDrawRandom(List<BetUser> betUserList, int count) {
        // 如果集合数量很小,那么直接返回
        if (betUserList.size()<count) return betUserList;
        Collections.shuffle(betUserList);
        List<BetUser> betUsers = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            betUsers.add(betUserList.get(i));
        }
        return betUsers;
    }

    /**
     * 按照权重来进行排序
     * @param betUserList
     * @param count
     * @return
     */
    public List<BetUser> doDrawWeight(List<BetUser> betUserList, int count) {
        // 如果集合数量很小,那么直接返回
        if (betUserList.size()<count) return betUserList;
        betUserList.sort((b1,b2)->{
            return b2.getUserWeight()- b1.getUserWeight();
        });
        List<BetUser> betUsers = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            betUsers.add(betUserList.get(i));
        }
        return betUsers;
    }

}

进行测试:

public class Test {
    public static void main(String[] args) {
        List<BetUser> betUsers = new ArrayList<>();
        betUsers.add(new BetUser("网盘",8));
        betUsers.add(new BetUser("爱奇艺",2));
        betUsers.add(new BetUser("虎牙",3));
        betUsers.add(new BetUser("爱奇艺",5));
        betUsers.add(new BetUser("斗鱼",9));
        DrawControl drawControl = new DrawControl();
        List<BetUser> betUsers1 = drawControl.doDrawRandom(betUsers, 2);
        List<BetUser> betUsers2 = drawControl.doDrawWeight(betUsers, 2);
        for (BetUser betUser : betUsers1) {
            System.out.println(betUser);
        }
        System.out.println("================================");
        System.out.println("================================");
        System.out.println("================================");
        for (BetUser betUser : betUsers2) {
            System.out.println(betUser);
        }
    }
}

如果代码是一次性的,几乎不变的,那么可以不考虑很多的扩展性和可维护因素。但是如果这些程序具有不确定性或者当业务时候不断的调整和新增,那么这样的实现方式就很不友好。

如果是在不确定性情况下,那么按照上面的实现方式来说,每次扩展都需要新增一个方法或者说是接口,同时对调用方来说需要新增调用接口的代码。其次,对于这个服务类来说,随着接口数量的增加,代码行数会不断的进行暴增,最后难以维护。

四、使用依赖倒置原则改善代码

上面的方式不具备良好的扩展性,那么使用依赖倒置、面向抽象编程的方法来进行实现:

创建接口:

public interface Draw {
    /**
     * 抽象接口类
     * @param betUserList
     * @param count
     * @return
     */
    List<BetUser> prize(List<BetUser> betUserList,int count);
}

创建实现类

随机抽奖实现类:

public class DrawRandom implements Draw {

    public List<BetUser> prize(List<BetUser> betUserList, int count) {
        // 如果集合数量很小,那么直接返回
        if (betUserList.size()<count) return betUserList;
        Collections.shuffle(betUserList);
        List<BetUser> betUsers = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            betUsers.add(betUserList.get(i));
        }
        return betUsers;
    }
}

权重抽象实现类:

public class DrawWeightRank implements Draw {
    @Override
    public List<BetUser> prize(List<BetUser> betUserList, int count) {
        // 如果集合数量很小,那么直接返回
        if (betUserList.size()<count) return betUserList;
        betUserList.sort((b1,b2)->{
            return b2.getUserWeight()- b1.getUserWeight();
        });
        List<BetUser> betUsers = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            betUsers.add(betUserList.get(i));
        }
        return betUsers;
    }
}

创建抽奖服务

public class DrawControl {

    private Draw draw;

    /**
     * 这种巧妙的方式来进行实现!
     * @param draw 注意这里的经典实现方式!
     * @param betUserList
     * @param count
     * @return
     */
    public List<BetUser> doDraw(Draw draw,List<BetUser> betUserList,int count){
        return draw.prize(betUserList,count);
    }
}

这个类中,可以将任何一种抽奖逻辑传递给这个类。这样实现的好处就是不断的扩展,但是不需要在外部新增调用接口,降低了一套代码的维护成本,并提高了可扩展性和可维护性。

对于调用方来说,是毫无感觉的。

五、总结

注意上面的使用亮点:将实现逻辑的接口作为参数,这个在框架中是最为常见的存在。

测试:

/**
 * 对于这种使用方式来说,扩展性强。
 * 如果还需要来进行实现,只需要再来进行扩展即可,然后在创建对象的时候值需要来进行实现即可
 */
public class Test {
    public static void main(String[] args) {
        List<BetUser> betUsers = new ArrayList<>();
        betUsers.add(new BetUser("网盘",8));
        betUsers.add(new BetUser("爱奇艺",2));
        betUsers.add(new BetUser("虎牙",3));
        betUsers.add(new BetUser("爱奇艺",5));
        betUsers.add(new BetUser("斗鱼",9));
        DrawControl drawControl = new DrawControl();
        List<BetUser> betUsers1 = drawControl.doDraw(new DrawRandom(), betUsers, 2);
        for (BetUser betUser : betUsers1) {
            System.out.println(betUser);
        }
        System.out.println("------------------");
        List<BetUser> betUsers2 = drawControl.doDraw(new DrawWeightRank(), betUsers, 2);
        for (BetUser betUser : betUsers2) {
            System.out.println(betUser);
        }
    }
}

以这种抽象接口为基准搭建起来的框架结构会更加稳定,算程已经搭建好了,外部只需要实现自己的算子即可,最终把算子交给算程来处理。

  • 算程:一段算法的执行过程;
  • 算子:具体算法的执行逻辑;

这种方式相对于调用方来说,是没有任何区别的。

推荐阅读