1.什么是依赖关系
我们先来看这张图,Driver,Trucker,Racer三个类中均包含了他们所驾驶的专门车辆的类Car、Truck、RaceCar。驾驶员类这三个类的Drive方法是与车辆类密切相关的,也就是说脱离了这三个车辆类,驾驶员类无法正常使用,这就是我们常说的依赖,我们的驾驶员类依赖于车辆类 (请注意根据类的实际情况确认类与类的实际依赖关系)。
我们来通过一段代码加深对依赖的理解,首先创建一个引擎类
class Engine
{
public int RPM { get; private set; } //引擎转速
public void Work(int gas)
{
this.RPM = 1000 * gas;//引擎转速等于1000 x gas
}
}
接下来我们创建一个汽车类,汽车需要引擎才能跑起来,所以汽车类包含引擎类。
class Car
{
private Engine _engine;
public Car(Engine engine)
{
_engine = engine;
}
public int Speed { get; private set; }
public void Run (int gas)
{
_engine.Work(gas);//调用引擎中的Work方法,将gas的量作为参数传入
this.Speed = _engine.RPM / 100;//汽车的速度与引擎的转速相关
Console.WriteLine(this.Speed);//打印一下我们汽车的速度
}
}
我们来看一下结果
static void Main(string[] args)
{
Engine engine = new Engine();
Car car=new Car(engine);
car.Run(10);
Console.ReadLine();
}
当给予的gas值为10时,汽车的速度为100,通过这个例子我们可以得出一个结论,当前的Car的关键方法Run方法与Engine类的Work方法是密切相关的,Car类与Engine类是紧耦合的,如果我们像下面一样修改代码,汽车就无法正常工作。这也就说明了我们的Car类是依赖于Engine类的。
class Engine
{
public int RPM { get; private set; }
public void Work(int gas)
{
this.RPM = 0 * gas;//汽车就无法正常工作了
}
}
2.依赖反转
我们先来设想这样一种情况,一个司机他同时是卡车司机与小汽车司机,如果按照我们上面依赖关系的例子,在他开小汽车的时候这个司机的类型必须是小汽车司机,在他开卡车的时候他的类型必须是卡车司机,这样司机与汽车类型的耦合度就太高了(我们可以这样设想,如果这个司机会开十几种车,那这个司机的类型岂不是要设置对应的十几种,使用起来也并不方便)。
我们可以使用依赖反转来解决我们刚刚提到的不便,我们使用一个接口IVehicle来解耦,Car类与Truck类同时继承(实现)Ivehicle接口,这样我们刚才提到的可以驾驶多种车辆的司机就可以不用在根据车辆的类型来改变自己的身份了。(注意这里不要钻牛角尖,我们需要根据实际情况来决定我们是否要使得依赖关系反转,在下面例子的情况下,司机就是会开所有车辆载具的,所以具体是什么车辆和司机没有关系,因为他都会开。而在现实生活中,这种情况并不是普遍的,司机只会开一种或多种而不是所有类型的车辆载具。)
由于我们的司机不需要在考虑具体的车辆是什么了,只要是车辆司机都能开,那我们就可以利用多态的性质来去写我们的代码。
class Driver
{ //我们的vehicle字段是Ivehicle接口类型的字段
private Ivehicle vehicle;//我们直接通过vehicle字段去引用我们的车辆实例
public Driver(Ivehicle _vehicle)
{
this.vehicle = _vehicle;
}
public void Drive()
{
vehicle.Run();
}
}
interface Ivehicle
{
void Run();
}
class Car:Ivehicle //Car实现了Ivehicle接口 多态的性质,我们可以用Ivehicle类型的字段去
{ //引用car类型的实例
public void Run()
{
Console.WriteLine("Car is Running");
}
}
class Truck:Ivehicle
{
public void Run()
{
Console.WriteLine("Truck is Running");
}
}
那依赖反转到底反转在什么地方了呢?从图里看,我们的car与Truck依赖于Ivehicle,而我们的Driver也依赖于Ivehicle。
我们来看这样一张图,我们人类的一般思维,对于一个大的问题会把它拆分为小的问题,逐步去解决小的问题,从而使得大的问题也能被解决,这种思维是自顶向下逐步求精的。我们去看我们依赖关系的示例图时,我们被依赖的类总是放在依赖于它下方的位置。这就与图片中的大问题的解决是通过(依赖)小问题的解决而解决的思维方式是一致的。而我们刚刚展示的依赖反转的例子中,Car与Truck是放在了它们所依赖的IVehicle接口下方,我们的依赖方向(箭头方向)变成了由下到上的方向。这种方向上的反转,就是我们依赖反转这个名字的由来,它区别我们自顶向下的思维方式,它是自下向上的一种思维方式。
在这里,或许有些人会对IVehicle这个接口是否可以用一个Vehicle抽象类来替代感到困惑
那我们来说一说为什么这里不用抽象类而去使用一个接口呢(实际上是使用抽象类远比接口麻烦,要写的代码更多= - =),我们需要根据实际的情况来讨论,以我们这个例子的实际情况来看,IVehicle中只需要拥有一个可以被子类重写的虚方法即可,而不需要去拥有字段(接口中不能拥有字段,抽象类中可以拥有字段 PS:两者均可拥有属性),我们没有必要去使用一个抽象类,可以直接去使用一个比抽象类更抽象的接口去完成我们的需求(只需要有一个能被子类重写的虚方法),你使用抽象类需要改动的代码也远比使用接口多,而得到的结果在这个例子中都是一样的,我们来看看如果使用抽象类代码是什么样子的。
public class Program
{
static void Main(string[] args)
{
Car mycar=new Car();
Driver me=new Driver(mycar);
me.Drive();
Console.ReadLine();
}
class Driver
{
private Ivehicle vehicle;
public Driver(Ivehicle _vehicle)
{
this.vehicle = _vehicle;
}
public void Drive()
{
vehicle.Run();
}
}
abstract class Ivehicle //一般接口命名才在名称前+I,所以这里一般直接命名vehicle
{ //在这个例子中我就不修改名称了,大家知道即可
public abstract void Run();//在这里我们比接口要多写public 和 abstract
}
class Car:Ivehicle
{
public override void Run()//在这里我们比使用接口要多写override
{
Console.WriteLine("Car is Running");
}
}
class Truck:Ivehicle
{
public override void Run()//在这里我们比使用接口要多写override
{
Console.WriteLine("Truck is Running");
}
}
所以,在能满足需求的情况下,使用接口更好(接口能多继承,抽象类不能多继承,接口写的代码比抽象类少,能用接口为什么要用抽象类对不对)。
我们来看下面这种情况,又多了一种Ai司机,Ai司机与我们的普通司机都属于司机,而司机必然拥有IVehicle类型的字段,我们知道接口中是不能拥有字段的,所以我们在这里只能使用一个司机的抽象类,让我们的Driver与AiDriver都继承我们的DriverBase。
这是我对B站Up主TimothyLiu(刘铁猛老师!!!)对于依赖与依赖反转的学习总结与思考体会,如有错漏,恳请指出,谢谢!