- 单一职责原则(Single Reponsibility Principle,SRP)
- 里氏替换原则(Liskov Substitution Principle,LSP)
- 依赖倒置原则(Dependence Inversion Principle,DIP)
- 接口隔离原则(Interface Segregation Principe,ISP)
- 迪米特法则(Law of Demeter,LOD)
- 开闭原则(Open Closed Principle,OCP)
依赖倒置原则(DIP)
Dependence Inversion Principle,简称:DIP。
高层模块不应该依赖低层模块,两者都应该依赖其抽象,不要依赖细节
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
问题由来:类 classA 直接依赖类 classB ,假如要将类 classA 改为依赖类 classC ,则必须通过修改类 classA 的代码来达成。这种场景下,类 classA 一般是高层模块,负责复杂的业务逻辑;类 classB 和类 classC 是低层模块,负责基本的原子操作;如若修改类 classA ,会给程序带来不必要的风险。
解决方案:将类 classA 修改为依赖接口 Interface,类 classB 和类 classC 各自实现接口 Interface ,类 classA 通过接口 Interface 间接与类 classB 或者类 classC 发生联系,则会大大降低修改类 classA 的几率。
在C#中,抽象就是指接口或者抽象类,两者都不能直接进行实例化;细节就是实现类,就是实现了接口或继承了抽象类而产生的类就是实现类,可以直接被实例化。所谓的高层与低层,每个逻辑实现都是由原始逻辑组成,原始逻辑就属于低层模块,像我们常说的三层架构,业务逻辑层相对数据层,数据层就属于低层模块,业务逻辑层就属于高层模块,是相对来说的。
依赖倒置原则就是程序逻辑在传递参数或关联关系时,尽量引用高层次的抽象,不使用具体的类,即使用接口或抽象类来引用参数,声明变量以及处理方法返回值等。
这样就要求具体的类就尽量不要有多余的方法,否则就调用不到。说简单点,就是“面向接口编程”。
论题:依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性。
一、举个栗子
还是举一个学生使用手机的例子吧,代码如下:
学生类:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
/// <summary>
/// 依赖抽象
/// </summary>
/// <param name="phone"></param>
public void Play(AbstractPhone phone)
{
Console.WriteLine("这里是{0}", this.Name);
phone.Call();
phone.Text();
}
}
手机抽象类:
//手机抽象类
public abstract class AbstractPhone
{
public int Id { get; set; }
public string Branch { get; set; }
public abstract void Call();
public abstract void Text();
}
苹果手机类,继承手机抽象类 AbstractPhone
//苹果手机 继承 AbstractPhone
public class iPhone : AbstractPhone
{
//打电话
public override void Call()
{
Console.WriteLine("User {0} Call", this.GetType().Name);
}
//发短信
public override void Text()
{
Console.WriteLine("User {0} Call", this.GetType().Name);
}
}
荣耀手机类,同样继承手机抽象类 AbstractPhone
//荣耀手机 继承 AbstractPhone
public class Honor : AbstractPhone
{
//打电话
public override void Call()
{
Console.WriteLine("User {0} Call", this.GetType().Name);
}
//发短信
public override void Text()
{
Console.WriteLine("User {0} Call", this.GetType().Name);
}
}
调用程序:
class Program
{
static void Main(string[] args)
{
try
{
Student student = new Student()
{
Id = 66,
Name = "柯南"
};
{
AbstractPhone phone = new iPhone();
student.Play(phone);
}
{
AbstractPhone honor = new Honor();
student.Play(honor);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.Read();
}
}
该例子中,到目前为止,项目没有任何问题。我们常说“危难时刻见真情”,把这句话移植到技术上就成了“变更才显真功夫”,业务绣球变更永无休止,技术前进永无止境,在发生变更时才能发觉我们的设计或程序是否是松耦合。
因此,以上项目中,手机类的声明都以做抽象处理,声明学生实例时依赖于细节,想用iphone还是honor,只需修改new后方的细节即可,它对低层模块的依赖都建立在抽象上了。
然而,倘若一位老师想要打电话发短信,那此处得再写一个Teacher类,而Teacher类的Play()方法和Student代码出现冗余,因此增加了代码的不稳定性。
因此,此处的Student类模块可以参考于手机类的接口一样设计,新增加Person抽象类和Teacher实体类,修改之后,代码如下:
public abstract class Person
{
public int Id { get; set; }
public string Name { get; set; }
public abstract void Play(AbstractPhone phone);
}
public class Student : Person
{
/// <summary>
/// 依赖抽象
/// </summary>
/// <param name="phone"></param>
public override void Play(AbstractPhone phone)
{
Console.WriteLine("这里是{0}{1}", this.Name, this.GetType().Name);
phone.Call();
phone.Text();
}
}
public class Teacher: Person
{
/// <summary>
/// 依赖抽象
/// </summary>
/// <param name="phone"></param>
public override void Play(AbstractPhone phone)
{
Console.WriteLine("这里是{0}{1}", this.Name, this.GetType().Name);
phone.Call();
phone.Text();
}
}
调用程序:
class Program
{
static void Main(string[] args)
{
try
{
Person teacher = new Teacher()
{
Id = 66,
Name = "柯南"
};
{
AbstractPhone phone = new iPhone();
teacher.Play(phone);
}
{
AbstractPhone honor = new Honor();
teacher.Play(honor);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.Read();
}
}
这样就实现了代码的去细节。
同时,负责Student类与负责Teacher类的开发人员,就可以独立开发了,而且项目之间的单元测试也可以独立地运行,而TDD(Test-Driven Development,测试驱动开发)开发模式就是依赖倒置原则的最高级应用。
二、衍生思考
依赖倒置原则的应用可以很多,其中一种的修改即使等式的右方
- 在 AbstractPhone phone = new Honor()的等号右方还可以以其他方式生成,通过 反射+工厂模式,从而实现依赖注入
- 在 .Net Core 中,使用泛型主机 (IHostBuilder)时,就用到了类型注入 Startup 类