接口与抽象类
接口由抽象类进化而来。
抽象类的方法要求不是private,接口类的成员方方法要求一定必须是public,默认都不让写。
接口的本质:服务的消费者或服务的调用者,与服务的提供者之间的契约。契约:对合同的双方都是可见的。所以用public。
而抽象类的protected成员只是留给子类所看到的。
契约:使自由合作成为可能:即约束服务方,又约束使用方。
接口引入历程:
对一组整数求和求平均值。
这组数据可能放在一个int类型的数组中,也可能放在一个ArrayList集合(非泛型)中。如果不使用接口的情况下,需要使用四个函数来实现。因为C#语言是强类型的语言。用来操作整型数组的函数不能用来处理ArrayList实例。
static void Main(string[] args)
{
int[] nums1 = new int[] {1, 2, 3, 4, 5};
ArrayList nums2 = new ArrayList {1, 2, 3, 4, 5};
Console.WriteLine(Sum(nums1));
Console.WriteLine(Avg(nums1));
}
static int Sum(int[] nums)
{
int sum = 0;
foreach (var num in nums)
{
sum += num;
}
return sum;
}
static double Avg(int[] nums)
{
int sum = 0;
double count = 0; // 这样一个整一double,就可以return一个double。
foreach (var n in nums)
{
sum += n;
count++;
}
return sum / count;
}
此时的函数如果传入nums2就会报错,因为函数输入的参数是整型数组。将两个函数CopyPaste。修改一下参数即可。同时因为ArrayList里面的元素类型为Object,所以需要强制类型转换。此时的函数Avg和Sum就有了重载。
static void Main(string[] args)
{
int[] nums1 = new int[] {1, 2, 3, 4, 5};
ArrayList nums2 = new ArrayList {1, 2, 3, 4, 5};
Console.WriteLine(Sum(nums1));
Console.WriteLine(Avg(nums1));
Console.WriteLine(Sum(nums2));
Console.WriteLine(Avg(nums2));
}
static int Sum(int[] nums)
{
int sum = 0;
foreach (var num in nums)
{
sum += num;
}
return sum;
}
static double Avg(int[] nums)
{
int sum = 0;
double count = 0;
foreach (var n in nums)
{
sum += n;
count++;
}
return sum / count;
}
static int Sum(ArrayList nums)
{
int sum = 0;
foreach (var num in nums)
{
sum += (int)num;
}
return sum;
}
static double Avg(ArrayList nums)
{
int sum = 0;
double count = 0;
foreach (var n in nums)
{
sum += (int)n;
count++;
}
return sum / count;
}
此时如果引入接口如何实现呢?
首先接口是一种协议:契约。契约的双方:供方是nums1和nums2。需求方是Avg和Sum函数。先分析需求:两个函数的需求均是:传入的数组能够被foreach迭代就好了。因为数组和ArrayList都是继承了IEnumerable,所以都是可以迭代的。
供方和需方都要求IEnumerable,所以要设法使联系起来,将参数类型设置为IEnumerable类型。里氏转换法:让一个函数需要父类类型的参数时,我们可以传一个子类类型的参数来代替。因为is-a这个概念。
static void Main(string[] args)
{
int[] nums1 = new int[] {1, 2, 3, 4, 5};
ArrayList nums2 = new ArrayList {1, 2, 3, 4, 5};
Console.WriteLine(Sum(nums1));
Console.WriteLine(Avg(nums1));
Console.WriteLine(Sum(nums2));
Console.WriteLine(Avg(nums2));
}
static int Sum(IEnumerable nums)
{
int sum = 0;
foreach (var num in nums)
{
sum += (int)num;
}
return sum;
}
static double Avg(IEnumerable nums)
{
int sum = 0;
double count = 0;
foreach (var n in nums)
{
sum += (int)n;
count++;
}
return sum / count;
}
依赖与耦合:
类是对现实世界的抽象模拟,现实世界有合作关系,体现在代码中就是依赖关系。依赖越直接耦合就越紧。
class Program
{
static void Main(string[] args)
{
var engine = new Engine();
var car = new Car(engine);
car.Run(3);
Console.WriteLine(car.Speed);
}
}
class Engine
{
// private无法从外部设置发动机的转速;
public int RPM { get; private set; }
public void Work(int gas)
{
this.RPM = 1000 * 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);
this.Speed = _engine.RPM / 100;
}
}
Car这个类严重依赖于Engine。这就是紧耦合。如果Engine有修改,相应的Car也要跟着修改。这样缠绕在一起。Debug难度会增大。
引入接口降低耦合度。接口就是为了松耦合而生。松耦合最大的好处是:功能的提供方,变得可以被替换,从而降低紧耦合的中紧耦合功能方不能替换所带来的高风险和高成本。
解耦的表现:依赖反转
当我们分析问题时可以将大问题拆分,自顶向下分解;当我们解决问题时,是自下向上解决,将小问题逐一解决,那么大问题也就解决了。
依赖反转原则:
其实是给我们一种思路。
第一幅图:小车司机开小车,卡车司机开卡车,赛车司机开赛车。三条链路,司机依赖车。但是卡车司机也有私家小车,赛车手偶尔也要开小车或者卡车。
首先我们考虑Driver这个类别可以抽象为一类。然后各种车也可以抽象为Vehicle。因为存在一人开多车这样的情况,首先抽象Vehicle为接口比较合理。这就有了第二幅图。
第二幅图:此时的依赖关系变成了,车依赖于接口,人还是依赖于车。由原来的人仅仅依赖车,多了一条车依赖于IVehicle接口。并且在Driver中多了一个_vehicle字段依赖于IVehicle,供Drive方法调用,以满足Drvier可以开多种车。
第三幅图:将多种Driver抽象一个父类DriverBase,来满足多种司机可以开多种车。这种交叉混淆形式。此时的依赖,由从上到下,变成了从下到上。像一座桥,把两类事物充分联系在一起。
第四幅图:最后演变为:设计模式。
接口有两种实现:自底向上(重构)、自顶向下(设计)。自顶向下的需要技术非常熟练才可以。否则就是不断重构就好了。