系列文章|源码
https://github.com/tyronczt/design-mode-learn
定义-是什么
依赖倒置原则(Dependency inversion principle,简称 DIP),其含义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节, 细节应该依赖于抽象
- 要针对接口编程,不要针对实现编程
传统的自顶向下设计
传统设计方式采用自顶向下的原则,逐级依赖,中层模块和高层模块的耦合度很高,如果需要修改其中的一个模块,则可能会导致其它很多模块也需要修改,牵一发动全身,不易于维护。 不使用依赖反转的系统构架,控制流和依赖关系流的依赖箭头是一个方向的,由高层指向底层,也就是高层依赖底层。
依赖倒置原则
举个通俗一点的例子:
唐朝的皇帝要了解一下今年的科举情况,他肯定不会直接去找礼部尚书问情况,而是让当班的公公去传唤;礼部尚书也会通过管家或者身边人去找侍郎或郎中了解科举情况。
例子中的公公、管家就是接口/抽象层的角色,按照传统思维应该是皇帝自顶向下直接问各个侍郎或郎中去问科举情况,依赖倒置,侍郎、郎中去尚书那儿汇报情况再去皇帝那儿汇报。
思考-为什么
核心思想:面向接口编程
优点
- 减少类之间的耦合性
- 提高系统稳定性
- 提高代码可读性和维护性
- 可降低修改程序所造成的风险
应用-怎么用
如果说实现开闭原则的关键事抽象化,是面向对象设计的目标的话,依赖倒置原则就是这个面向对象设计的主要机制。
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则:
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
案例:用户抽奖
需求:有部分投注用户,设计随机、权重方式进行抽奖
代码仓库:https://github.com/tyronczt/design-mode-learn/tree/main/design-mode-learn-4-01
public class BetUser {
// 用户姓名
private String userName;
// 用户权重
private int userWeight;
public BetUser() {
}
public BetUser(String userName, int userWeight) {
this.userName = userName;
this.userWeight = userWeight;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getUserWeight() {
return userWeight;
}
public void setUserWeight(int userWeight) {
this.userWeight = userWeight;
}
}
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;
}
}
public class ApiTest1 {
private Logger logger = LoggerFactory.getLogger(ApiTest1.class);
@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));
}
}
// 结果输出
16:51:18.748 [main] INFO c.t.design.mode.learn.test.ApiTest1 - 随机抽奖,中奖用户名单:[{"userWeight":89,"userName":"笨笨"},{"userWeight":10,"userName":"丑蛋"},{"userWeight":65,"userName":"花花"}]
16:51:18.751 [main] INFO c.t.design.mode.learn.test.ApiTest1 - 权重抽奖,中奖用户名单:[{"userWeight":89,"userName":"笨笨"},{"userWeight":72,"userName":"小白"},{"userWeight":65,"userName":"花花"}]
上述编码为传统编码思路,需要随机抽奖即新建随机抽奖方法,同理实现权重抽奖,自顶上向下,弊端是如果再增加抽奖模式,还要在原有方法中修改,并在应用层(Test)方法中进行修改,这破坏了依赖倒置原则,下面使用依赖倒置原则来进行修改:
public class DrawControl {
private IDraw draw;
public List<BetUser> doDraw(IDraw draw, List<BetUser> betUserList, int count) {
return draw.prize(betUserList, count);
}
}
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 ApiTest2 {
private Logger logger = LoggerFactory.getLogger(ApiTest2.class);
@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));
}
}
17:32:20.769 [main] INFO c.t.design.mode.learn.test.ApiTest2 - 随机抽奖,中奖用户名单:[{"userWeight":10,"userName":"丑蛋"},{"userWeight":43,"userName":"豆豆"},{"userWeight":65,"userName":"花花"}]
17:32:20.773 [main] INFO c.t.design.mode.learn.test.ApiTest2 - 权重抽奖,中奖用户名单:[{"userWeight":89,"userName":"笨笨"},{"userWeight":72,"userName":"小白"},{"userWeight":65,"userName":"花花"}]
通过改造后的案例:
- 需要具体抽奖方式由 Test 决定(应用层决定,高层模块)
- 再新增其他抽奖方式,只需再扩展抽奖方式,而抽奖控制层不需要变动
- 在底层模块扩展
- 是拓展抽奖方式,而不是去修改已有的抽奖方式,也符合 开闭原则
- 面向接口编程,而不是面对抽奖器
- Test 与 抽奖器是解耦的
- 抽奖器与具体的抽奖实现是解耦的
- 抽奖器与抽奖接口 IDraw 是有依赖的
- 所谓高内聚,低耦合,就是尽量减少耦合
依赖倒置原则表现了一种事实:
- 相对于细节的多变性,抽象的东西要稳定得多
- 以抽象搭建起来的架构,比以细节搭建起来的要稳定得多
那么抽象的目的也就是:制定好规范和契约,如 IDraw 就是一种规范契约
参考
设计模式六大原则(三)----依赖倒置原则 - 掘金
依赖倒置原则:高层代码和底层代码,到底谁该依赖谁? - 掘金
【依赖倒置原则 - 六大设计原则 - 设计模式】 - 掘金
《重学Java设计模式》第二章:六大设计原则【依赖倒置原则】_哔哩哔哩_bilibili
依赖倒置原则 | MRCODE-BOOK