依赖倒置原则(Dependency Inversion Principle, DIP)是面向对象设计原则之一,其核心内容是:
1. 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
2. 抽象不应该依赖于细节,细节应该依赖于抽象。
从我的角度理解,这两句话可以概括为:
在编写业务服务时,我们不应该直接依赖具体的实现类,而应该依赖由这些类共性的行为抽象出来的接口或抽象类。这样的设计能够提高程序的可扩展性和可维护性,同时降低模块之间的耦合度。
这里的“抽象”通常指的是接口或抽象类,它们定义了一组契约,规定了应该做什么,但不规定具体如何实现。而“细节”则是具体的实现类,它们负责实现抽象定义的契约,提供具体的功能。
因此,在进行系统设计时,我们应当优先定义模块之间的抽象契约,然后基于这些契约来编写具体的实现类。这样,高层模块和低层模块都依赖于相同的抽象,从而使得系统更加灵活,易于扩展和修改。
具体的示例中,我们要实现一个动物园的一个功能,动物园中有很多的动物如大象,老虎,猫,我们在这里就可以将这些动物的共有行为和属性抽象出来,定义一个接口为IAnimal,然后让具体的动物类(如大象、老虎、猫)实现这个接口。这样,高层模块(如动物园、饲养员)就可以只依赖于IAnimal接口,而不依赖于具体的动物类。下面我们定义一个接口为IAnimal
/// <summary>
/// 抽象的动物类,定义动物的基本行为
/// </summary>
public interface IAnimal
{
//定义基本的动物属性:姓名,年龄,物种
string Name { get; }
int Age { get; }
string Species { get; }
//定义动物基本行为:吃,睡,叫,玩
void Eat(string food);
void Sleep(int hours);
string MakeSound();
//额外的计算属性如:判断是否饿了,当前饱腹度
bool IsHungry { get; }
double Satiety { get; }
void Play()
{
Console.WriteLine($"{Species}{Name} 正在玩耍");
}
}
在这个接口里我们定义动物的基本行为:吃睡叫玩,基本属性:名字年龄以及物种,在定义的行为中其中玩我们给予了一个默认的行为就是玩。这样实现IAnimal接口的动物就算没有实现玩的方法也可以有一个默认玩的行为。这样的好处是提供了通用行为,减少重复代码。接口中计算属性封装了状态判断逻辑
接下来就是具体的动物来实现这些抽象契约也就是IAnimal 接口
首先是大象 Elephant 实现 IAnimal 接口
/// <summary>
/// 大象类,实现动物接口
/// </summary>
public class Elephant : IAnimal
{
public string Name { get; }
public int Age { get; }
public string Species => "大象";
//特有属性:象鼻子长度
public double LongNose { get; }
public Elephant(string name, int age, double longNose)
{
Name = name;
Age = age;
LongNose = longNose;
}
public bool IsHungry => Satiety < 30;
public double Satiety { get; private set; } = 10;
public void Eat(string food)
{
Console.WriteLine($"{Species}{Name} 用 {LongNose} 米长的鼻子卷起 {food},慢慢咀嚼");
Satiety = Math.Min(100, Satiety + 20);
}
public string MakeSound()
{
Satiety -= 5;
return $"{Species}{Name}叫了一声";
}
public void Sleep(int hours)
{
Console.WriteLine($"{Species}{Name} 站着睡了 {hours} 小时");
Satiety = Satiety -= 1 * hours;
}
// 大象特有方法
public void SprayWater()
{
Console.WriteLine($"{Species}{Name} 用鼻子喷水降温");
Satiety -= 8;
}
}
在大象的实现类里我们实现接口所定义的属性,行为,并扩展大象特有的属性象鼻子长度(LongNose)以及特有的行为喷水(SprayWater),在每个行为中将饱腹度(Satiety)进行变化并根据饱腹度的值判断是否饥饿(IsHungry)
同样的,我们再将老虎 Tiger 实现 IAnimal 接口
/// <summary>
/// 老虎类,实现动物接口
/// </summary>
public class Tiger : IAnimal
{
public string Name { get; }
public int Age { get; }
public string Species => "老虎";
//特有属性:斑纹数量
public double StripesCount { get; }
public Tiger(string name, int age, double stripesCount)
{
Name = name;
Age = age;
StripesCount = stripesCount;
}
public bool IsHungry => Satiety < 40;
public double Satiety { get; private set; } = 10;
public void Eat(string food)
{
Console.WriteLine($"{Species}{Name} 露出尖牙,凶猛撕咬 {food}");
Satiety = Math.Min(100, Satiety + 30);
}
public string MakeSound()
{
Satiety -= 5;
return $"{Species}{Name}叫了一声";
}
public void Sleep(int hours)
{
Console.WriteLine($"具有{StripesCount}斑纹的{Species}{Name} 趴在草地上睡了 {hours} 小时");
Satiety = Satiety -= 1 * hours;
}
// 老虎特有方法
public void MarkTerritory()
{
Console.WriteLine($"{Species}{Name} 在领地边缘留下标记");
Satiety -= 8;
}
}
同样的在老虎类中我们也实现了共同的动物行为并实现老虎特有的行为标记领地(MarkTerritory),并定义了特有的属性斑纹条数(StripesCount)
大象和老虎都实现了抽象出来的接口IAnimal,并实现了他们自己具体的行为以及他们自己特有的属性,如老虎睡觉的时候会展示自己的斑纹条数,大象吃东西的时候会用鼻子卷起食物。
这就是“抽象不应该依赖于细节,细节应该依赖于抽象”,在这里抽象的IAnimal不依赖任何类,他自己去定义动物的基本行为与属性,而细节就是具体的大象类和老虎类,他们共同依赖接口IAnimal,实现共有得动物行为并可以实现他们自己的特有属性以及行为。
接着我们定义高层模块动物园(Zoo)
public class Zoo
{
private readonly List<IAnimal> _animals = new List<IAnimal>();
public void AddAnimal(IAnimal animal)
{
_animals.Add(animal);
Console.WriteLine($"动物园新成员: {animal.Name} ({animal.Species})");
}
public void DailyRoutine()
{
Console.WriteLine("动物园日常开始");
// 晨间喂食
Console.WriteLine("\n上午8:00 晨间喂食时间:");
foreach (var animal in _animals)
{
var food = GetFoodForAnimal(animal);
Console.WriteLine($"\n喂食 {animal.Name}:");
animal.Eat(food);
if (animal.IsHungry)
{
Console.WriteLine($"{animal.Name} 看起来还很饿,需要加餐!");
animal.Eat("额外零食");
}
}
// 动物表演
Console.WriteLine("\n上午10:00 动物表演时间:");
foreach (var animal in _animals)
{
Console.WriteLine($"\n{animal.Name} 的表演:");
Console.WriteLine($"叫声: {animal.MakeSound()}");
// 动物特有行为
PerformSpecialBehavior(animal);
}
// 午休
Console.WriteLine("\n[下午1:00] 午休时间:");
foreach (var animal in _animals)
{
animal.Sleep(2);
}
// 下午活动
Console.WriteLine("\n[下午3:00] 自由活动时间:");
foreach (var animal in _animals)
{
animal.Play();
}
Console.WriteLine("动物园日常结束\n");
}
private string GetFoodForAnimal(IAnimal animal)
{
return animal switch
{
Elephant => "香蕉、甘蔗、草料",
Tiger => "新鲜牛肉、鸡肉",
_ => "通用饲料"
};
}
private void PerformSpecialBehavior(IAnimal animal)
{
switch (animal)
{
case Elephant elephant:
if (DateTime.Now.Hour >= 12 && DateTime.Now.Hour <= 15)
{
elephant.SprayWater();
}
break;
case Tiger tiger:
tiger.MarkTerritory();
if (tiger.Satiety > 50)
{
tiger.MarkTerritory();
}
break;
}
}
public void ShowAnimalStatus()
{
Console.WriteLine("\n动物状态报告");
foreach (var animal in _animals)
{
string hungerStatus = animal.IsHungry ? "饥饿" : "饱了";
Console.WriteLine($"{animal.Name} ({animal.Species}, {animal.Age}岁) - 能量: {animal.Satiety:F1}, 状态: {hungerStatus}");
}
}
}
在高层模块中我们依赖的也是共性的动物IAnimal,只关注抽象接口而不关注具体实现。如在添加动物(AddAnimal)方法中我们不关注具体添加了什么动物,而是只需要添加动物,其他的一些行为也是一样的,像动物园的日常(DailyRoutine),动物的报告状态(ShowAnimalStatus)都不关注具体的动物,只是去使用动物共有的行为。
而特殊行为(PerformSpecialBehavior)这个方法目前我这里是去判断了动物的种类从而执行了不同的行为,其实这里还可以去定义一个特殊动物的接口继承基础动物接口,然后将这些具有特殊行为的动物如大象和老虎实现特殊动物接口,然后在这个特殊行为展示的方法中来使用 animal is ISpecialBehaviorAnimal 判断当前的动物有没有实现特殊行为从而去执行特殊行为,这样不管动物园来了多少特殊行为或者没有特殊行为的动物,我们只需要让他们实现相应的接口即可,而无需修改动物园的代码。
接下来执行主程序:
private static void Main(string[] args)
{
// 创建动物园
var zoo = new Zoo();
// 创建各种动物
var elephant = new Elephant("壮壮", 15, 2.3);
var tiger = new Tiger("威威", 8, 12);
// 添加动物到动物园
zoo.AddAnimal(elephant);
zoo.AddAnimal(tiger);
Console.WriteLine("\n第一天动物园运营\n");
// 执行日常
zoo.DailyRoutine();
// 显示动物状态
zoo.ShowAnimalStatus();
Console.WriteLine("\n接口多态演示");
DemonstratePolymorphism(new List<IAnimal> { elephant, tiger });
Console.WriteLine("\n按任意键退出...");
Console.ReadKey();
}
private static void DemonstratePolymorphism(IEnumerable<IAnimal> animals)
{
Console.WriteLine("所有动物依次展示基本行为:");
foreach (var animal in animals)
{
Console.WriteLine($"\n--- {animal.Name} ({animal.Species}) ---");
Console.WriteLine($"叫声: {animal.MakeSound()}");
// 使用接口的默认实现
animal.Play();
}
}
}
打印出来如图:


从主程序中我们可以看到,我们只要将具体的大象,老虎等动物引入进入,动物园就能自动运转起来了。
我们可以得出依赖倒置的几个步骤:
1. 找到共性:大象老虎都有动物的共性
2. 定义抽象:创建IAnimal接口,定义所有动物的共同契约
3. 实现细节:每个具体的动物类以自己的方式实现接口
4. 依赖抽象:高层模块(Zoo)和低层模块(Elephant,Tiger)只依赖IAnimal接口
在这个例子中 高层模块(Zoo),低层模块(Elephant ,Elephant)并没有直接依赖,而是两者都依赖于IAnimal 这个接口, 也就是”高层模块不应该依赖于低层模块,两者都应该依赖于抽象”
从这里我们可以得出一个结论:依赖倒置原则的核心就是面向接口编程而非实现编程。这样的设计可以使得程序的可扩展性大大提高,我们不需要修改原有的代码,只需要引入新的动物实现我们共有的抽象接口即可,这符合了开闭原则(对扩展开放,对修改关闭)。
同时也降低程序的耦合度,如果高层模块直接依赖低层模块的时候,这时候耦合度太高,低层模块的任何变动都可能波及高层模块,导致维护成本上升。而通过依赖倒置,我们让抽象(接口)保持稳定,因为接口定义了不变的契约;而实现的细节则可以灵活多变。这样,低层模块的变动不会影响高层模块,从而使得系统更加健壮和易于维护。