@[toc]
概述
设计代码架构时,高层模块不应该依赖于底层模块,二者都应该依赖于抽象。 抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒置原则是实现开闭原则的重要途径之一, 它降低了类之间的耦合,提高了系统的稳定性和可维护性。
Case
抽奖系统服务
Bad Impl
先用最直接的方式,即按照不同的抽奖逻辑定义出不同的接口,让外部服务调用
【抽奖用户类】
public class BetUser {
private String userName; // 用户姓名
private int userWeight; // 用户权重
public BetUser() {
}
public BetUser(String userName, int userWeight) {
this.userName = userName;
this.userWeight = userWeight;
}
// set get
}
普通对象,包括了姓名和权重 。
接下来实现两种不同的抽奖逻辑,在一个类中用两个接口实现
public class DrawControl {
// 随机抽取指定数量的用户,作为中奖用户
public List<BetUser> doDrawRandom(List<BetUser> list, int count) {
// 集合数量很小直接返回
if (list.size() <= count) return list;
// 乱序集合
Collections.shuffle(list);
// 取出指定数量的中奖用户
List<BetUser> prizeList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
prizeList.add(list.get(i));
}
return prizeList;
}
// 权重排名获取指定数量的用户,作为中奖用户
public List<BetUser> doDrawWeight(List<BetUser> list, int count) {
// 按照权重排序
list.sort((o1, o2) -> {
int e = o2.getUserWeight() - o1.getUserWeight();
if (0 == e) return 0;
return e > 0 ? 1 : -1;
});
// 取出指定数量的中奖用户
List<BetUser> prizeList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
prizeList.add(list.get(i));
}
return prizeList;
}
}
包含了两个接口,一个随机抽奖,另外一个按照权重排序
【单元测试】
@Test
public void test_DrawControl(){
List<BetUser> betUserList = new ArrayList<>();
betUserList.add(new BetUser("花花", 65));
betUserList.add(new BetUser("豆豆", 43));
betUserList.add(new BetUser("小白", 72));
betUserList.add(new BetUser("笨笨", 89));
betUserList.add(new BetUser("丑蛋", 10));
DrawControl drawControl = new DrawControl();
List<BetUser> prizeRandomUserList = drawControl.doDrawRandom(betUserList, 3);
logger.info("随机抽奖,中奖用户名单:{}", JSON.toJSON(prizeRandomUserList));
List<BetUser> prizeWeightUserList = drawControl.doDrawWeight(betUserList, 3);
logger.info("权重抽奖,中奖用户名单:{}", JSON.toJSON(prizeWeightUserList));
}
从测试结果上没啥问题。
但如果考虑扩展性的话,就不太友好了。 每次扩展都需要新增接口,同时对于调用方来说需要新增调用的接口代码。
其次,说着接口数量的增加,代码行数就会爆炸,难以维护。
Better Impl
为了良好的扩展性,可以采用依赖倒置、面向抽象编程的方式实现
首先定义抽奖接口,任何一个实现方都可实现自己的抽奖逻辑。
【抽奖接口】
public interface IDraw {
// 获取中奖用户接口
List<BetUser> prize(List<BetUser> list, int count);
}
【随机抽象实现】
public class DrawRandom implements IDraw {
@Override
public List<BetUser> prize(List<BetUser> list, int count) {
// 集合数量很小直接返回
if (list.size() <= count) return list;
// 乱序集合
Collections.shuffle(list);
// 取出指定数量的中奖用户
List<BetUser> prizeList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
prizeList.add(list.get(i));
}
return prizeList;
}
}
【权重抽奖实现】
public class DrawWeightRank implements IDraw {
@Override
public List<BetUser> prize(List<BetUser> list, int count) {
// 按照权重排序
list.sort((o1, o2) -> {
int e = o2.getUserWeight() - o1.getUserWeight();
if (0 == e) return 0;
return e > 0 ? 1 : -1;
});
// 取出指定数量的中奖用户
List<BetUser> prizeList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
prizeList.add(list.get(i));
}
return prizeList;
}
}
---------
【创建抽奖服务】
public class DrawControl {
private IDraw draw;
public List<BetUser> doDraw(IDraw draw, List<BetUser> betUserList, int count) {
return draw.prize(betUserList, count);
}
}
这个类中体现了依赖倒置的重要性,可以把任何一种抽奖逻辑传递给这个类。 这样的好处是可以不断的扩展,但是不需要在外部新增调用接口,降低了一套代码的维护成本,提高了可扩展性和可维护性。
这里的重点是把实现逻辑的接口作为参数参数
【单元测试】
@Test
public void test_DrawControl() {
List<BetUser> betUserList = new ArrayList<>();
betUserList.add(new BetUser("花花", 65));
betUserList.add(new BetUser("豆豆", 43));
betUserList.add(new BetUser("小白", 72));
betUserList.add(new BetUser("笨笨", 89));
betUserList.add(new BetUser("丑蛋", 10));
DrawControl drawControl = new DrawControl();
List<BetUser> prizeRandomUserList = drawControl.doDraw(new DrawRandom(), betUserList, 3);
logger.info("随机抽奖,中奖用户名单:{}", JSON.toJSON(prizeRandomUserList));
List<BetUser> prizeWeightUserList = drawControl.doDraw(new DrawWeightRank(), betUserList, 3);
logger.info("权重抽奖,中奖用户名单:{}", JSON.toJSON(prizeWeightUserList));
}
可以看到,入参新增了 new DrawRandom()
、new DrawWeightRank()
。 在这两个抽奖的功能逻辑作为入参后,扩展起来非常方便。
以这种抽象接口为基准搭建起来的框架会更加的稳定,算程已经建设好,外部只需要实现自己的算子即可,最终把算子交给算程处理。