来源:mikechen的互联网架构
依赖倒置是确保架构设计灵活性、扩展性的重要原则。
把依赖方向搞反,是经常出现的典型错误。依赖关系没处理好,就会导致一个小改动,影响一大片。
结构化的编程思路,是自上而下进行功能分解,再按照分解结果进行组合的。这个思路很自然就延续到了很多人的编程习惯中。
本文,我们将通过手工图解、源码示例,来全面解析依赖倒置原则。
大家好,我是 mikechen。
依赖倒置是良好架构设计的基石,社/校招面试常问,必知必会非常重要。为方便大家系统学习,我已将本文归纳到《阿里架构师进阶专题合集》。需要的同学,拉到文末自取。
依赖倒置原则(DIP),英文全称 Dependency Inversion Principle。
顾名思义,就是将传统的依赖关系颠倒过来,让高层模块和底层模块都依赖于抽象接口。
依赖倒置之所以被提出,是源自它所提倡的软件设计原则。
依赖倒置原则的两个核心思想:
1) 高层模块不应依赖于底层模块,高层模块、底层模块都应依赖于抽象。
2) 抽象不应该依赖于细节,细节应该依赖于抽象。
这意味着,
在软件设计中,应该使用抽象类、接口或抽象方法等抽象层,来定义模块之间的通信接口。而具体实现,应该依赖于这些抽象。
一图释义:
抽象的关键在于:
它提供了一种通用的、高层次的方式,来定义模块之间的接口和交互,而不关心具体的实现。
高层模块将依赖于这个抽象,而不是直接依赖于底层模块的具体细节。
在依赖倒置原则中,抽象可以通过抽象类、接口或抽象方法来实现。
依赖倒置改变了传统的依赖关系。
依赖倒置从具体细节转移到了抽象上,极大提高了系统的灵活性、可维护性和可扩展性。
在传统的依赖关系中,高层模块直接依赖于底层模式的实现细节。
高层模块需要了解并依赖于底层模块的内部细节,包括其数据结构、算法和逻辑。
由于高层模块与底层模式是一种紧耦合的关系,当底层模块结构发生变化时,高层就需要随之改变。
一个小的变更,可能导致大范围的代码修改和测试。
传统依赖的架构设计很不合理,系统结构太刚性,难以维护及扩展。
传统依赖 VS 依赖倒置:
再来对比下传统依赖、依赖倒置的实现。
假设:
现在你需要实现一个面包店,你第一件想到的事情是什么?
我想到的是一个面包店,里面有很多具体的面包。
例如:法棍面包、全麦面包、白面包、牛角面包、荞麦面包、法式面包、甜甜圈面包、裸麦面包。
传统依赖关系:
面包店就是上层模块,面包是下层模块。如图:
面包店(Bakery)直接依赖于具体的面包类型(FrenchBaguette 和 WholeWheatBread),这是传统的依赖关系。
当增加底层模块(面包类型)时,对高层模块(面包店)有何影响呢。
class Bakery {
private FrenchBaguette frenchBaguette;
private WholeWheatBread wholeWheatBread;
public Bakery() {
frenchBaguette = new FrenchBaguette();
wholeWheatBread = new WholeWheatBread();
// 初始化具体的面包类型
}
public void serveBread() {
frenchBaguette.serve();
wholeWheatBread.serve();
// 提供其他具体面包类型
}
}
class FrenchBaguette {
public void serve() {
System.out.println("法棍面包供应中");
}
}
class WholeWheatBread {
public void serve() {
System.out.println("全麦面包供应中");
}
}
// 假设现在需要添加一种新的面包类型,例如牛角面包
class Croissant {
public void serve() {
System.out.println("牛角面包供应中");
}
}
public class Main {
public static void main(String[] args) {
Bakery bakery = new Bakery();
bakery.serveBread();
// 现在需要添加新的面包类型(牛角面包)
Croissant croissant = new Croissant();
croissant.serve();
// 这里需要手动修改Bakery类,将新的面包类型整合进去
}
}
如果要添加新的面包类型,例如 Croissant
,就需要修改 Bakery
类的代码,将新的面包类型整合进去。
显然,这种方式不够灵活,增加了维护成本和代码的脆弱性。
依赖倒置:
我们不希望让面包店理会这些具体类,于是,重新设计面包店:
创建一个抽象的面包接口,让面包店依赖于这个抽象接口,不再依赖于具体的面包种类。
既然法棍面包、牛角面包、白面包等都是面包,就让它们共享一个面包接口,抽象化一个面包类。
如图所示:
面包店是高层模块,面包类是抽象接口,而各种具体的面包是底层模块。
现在,我们需要增加新的面包类型(底层模块)。
// 面包接口,作为高层模块和底层模块之间的抽象
interface Bread {
void serve();
}
// 具体面包类实现面包接口,作为底层模块
class FrenchBaguette implements Bread {
public void serve() {
System.out.println("法棍面包供应中");
}
}
class WholeWheatBread implements Bread {
public void serve() {
System.out.println("全麦面包供应中");
}
}
// 假设需要添加一种新的面包类型,例如牛角面包
class Croissant implements Bread {
public void serve() {
System.out.println("牛角面包供应中");
}
}
// 高层模块,面包店
class Bakery {
private Bread bread;
public Bakery(Bread bread) {
this.bread = bread;
}
public void serveBread() {
bread.serve();
}
}
public class Main {
public static void main(String[] args) {
// 创建面包店并提供不同类型的面包
Bread frenchBaguette = new FrenchBaguette();
Bread wholeWheatBread = new WholeWheatBread();
Bakery bakery1 = new Bakery(frenchBaguette);
bakery1.serveBread();
Bakery bakery2 = new Bakery(wholeWheatBread);
bakery2.serveBread();
// 新增的面包类型(牛角面包)不会影响面包店
Bread croissant = new Croissant();
Bakery bakery3 = new Bakery(croissant);
bakery3.serveBread();
}
}
面包店(Bakery
)依赖于抽象的 Bread
接口,而不依赖于具体的面包类型。
当需要新增一种面包类型(例如Croissant
)时,不会对高层模块(Bakery
)产生影响,因为高层模块依赖于抽象接口。
对于具体的实现类我们不管,只要接口的行为不发生变化,增加新的面包类后,上层服务不用做任何的修改。
这样的设计降低了层与层之间的耦合,能很好地适应需求的变化,从而提高了系统的可扩展性和可维护性。
依赖倒置原则通过抽象(接口或抽象类),让各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。
依赖倒置原则的核心思想是:高层模块不应依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
遵循依赖倒置原则,通过降低层与层之间的耦合,可以很好地适应需求的变化,提高了系统的可扩展性和可维护性。
建议收藏备用,划走就再也找不到了。
依赖倒置是良好架构设计的基石,社/校招面试也常问,必知必会非常重要。