从上节课来看,接口跟纯虚抽象类其实本质上很相似,可以说一模一样。所以接下来的过程中,请不要忘记,接口也是“类”,也可以声明变量,引用实例。
接口从现实意义的角度来看,像是一种“协议”“契约”,是建立在使用者和提供者之间的。对于使用者,它规定了,“我想要什么”“我能要什么”;对于提供者,它规定了“你可以给出什么”。由于接口是契约,故它必须对双方透明公开,对合同双方可见,即接口一定是public的。
本节课讲了接口的三层面/方面作用。下面逐一讲解。
第一个作用,接口发挥“契约”作用,减少重复代码:
using System.Collections;
namespace ConsoleAppPractice
{
class Program
{
static void Main()
{
int[] nums1 = new int[] { 1, 2, 3, 4, 5 };
ArrayList nums2 = new ArrayList() { 1, 2, 3, 4, 5 };
Console.WriteLine(Calculator.Average(nums1));
Console.WriteLine(Calculator.Average(nums2));
}
}
class Calculator
{
public static double Average(IEnumerable vs)
{
double sum = 0;
int count = 0;
foreach (var item in vs)
{
sum += (int)item;
count++;
}
if (count!=0)
{
return sum / count;
}
else
{
return double.NaN;
}
}
}
}
本来要做不同类型的average函数,非常繁琐。但是如果引入了IEnumerable接口的话,就只用一个函数就行。
在这里,接口代表了协议:可以被迭代
ArrayList这个类就实现了这个接口。?
我在此处有些好奇这个IEnumerable内部具体逻辑是啥,怎么知道它能被迭代
这个是ArrayList里面的两个实现函数
?IEnumerable里面的抽象函数
?可以看到这玩意很抽象很基础,包含了一个可迭代数列应该具有的功能。
我猜想具体实现,看上上图,即ArrayList里边的方法。第一个应该是得到一个Enumerator的对象,里面存储着ArrayList该位置的数字。第二个应该是存储指定下标/索引的。【可能用于字典什么的?】然后得到的都是object的对象,所以都应该强制类型转换后再累加。
回到主题。
供(数组)需(方法)双方都遵循着同一个契约,“可以被迭代”
第二个作用,接口可以用来解耦,降低耦合度。但是其实解耦的对象跟接口之间是紧耦合。
下面来看实例。
namespace ConsoleAppPractice
{
class Program
{
static void Main()
{
Engine engine = new Engine();
Car car = new Car(engine);
car.Run(3);
Console.WriteLine(car.Speed);
}
}
class Engine
{
public int RPM { get; set; }
public void Work(int gas)
{
Console.WriteLine("Engine is working!");
RPM = gas * 3000;
}
}
class Car
{
private int speed;
public int Speed
{
get { return speed; }
set { speed = value/1000; }
}
private Engine _engine;
public Car(Engine engine)
{
_engine = engine;
}
public void Run(int gas)
{
_engine.Work(gas);
Speed = _engine.RPM;
}
}
}
在这段代码中,Car与Enigne产生了紧耦合,不利于工程维护。因而,此处宜使用接口。
类似的手机一例:
namespace ConsoleAppPractice
{
class Program
{
static void Main()
{
IPhone phone1 = new Huawei();
IPhone phone2 = new Xiaomi();
PhoneUser user = new PhoneUser(phone2);
user.Action();
}
}
interface IPhone
{
void Call();
void Receive();
}
class Huawei:IPhone
{
public void Call()
{
Console.WriteLine("Huawei is calling.");
}
public void Receive()
{
Console.WriteLine("Huawei recept a message.");
}
}
class Xiaomi : IPhone
{
public void Call()
{
Console.WriteLine("Xiaomi is calling.");
}
public void Receive()
{
Console.WriteLine("Xiaomi recept a message.");
}
}
class PhoneUser
{
private IPhone _phone;
public PhoneUser(IPhone phone)
{
_phone = phone;
}
public void Action()
{
_phone.Call();
_phone.Receive();
}
}
}
通过给使用者提供多个提供者,类似方法重载,来降低依赖程度。
可以说,代码中,如果有替换的地方,就一定有接口。
这里其实用的是SOLID的D,即依赖反转原则:
按照一般的思维方式,解决问题一般是这样:
但是这就造成了金字塔形的紧耦合,不好。因而,我们需要适当运用依赖反转,来降低耦合度。
类似于上述手机和使用者的例子。
?此时表示依赖关系的箭头从自上而下变成了自下而上。从直观角度,这就叫“反转”。这就是依赖反转原则的意思。
第三个作用,接口可以实现单元测试。
namespace ConsoleAppPractice
{
class Program
{
static void Main()
{
IPower powerer = new Powerer();
Fan fan = new Fan(powerer);
Console.WriteLine(fan.WorkCondition());
}
}
interface IPower
{
int GetPower();
}
class Powerer : IPower
{
public int GetPower()
{
return 100;
}
}
class Fan
{
private IPower _powerer;
public Fan(IPower powerer)
{
_powerer = powerer;
}
public string WorkCondition()
{
int power = _powerer.GetPower();
if (power<=0)
{
return "Won't work.";
}
else if (power < 100)
{
return "Work slow.";
}
else if (power < 200)
{
return "Work successfully.";
}
else
{
return "Warning!!";
}
}
}
}
?在这里,如果要测试电量数值为其他时fan能否运作,需要反复修改此处return的数值,显然不符合开闭原则。
此处,就需要额外建一个测试项目。
具体操作过程见刘铁猛《C#语言入门详解》全集_哔哩哔哩_bilibili
下面放上test文件里边的代码。
namespace ConsoleApp1.test
{
public class PowerTest
{
[Fact]
public void PowerLowerThanZero()
{
IPower powerer = new PowerLowerThanZero_power();
Fan fan = new Fan(powerer);
var expected = "Won't work.";
var actual = fan.WorkCondition();
Assert.Equal(actual, expected);
}
[Fact]
public void PowerHigherThan200()
{
IPower powerer = new PowerHigherThan200_power();
Fan fan = new Fan(powerer);
var expected = "Warning!!";
var actual = fan.WorkCondition();
Assert.Equal(actual, expected);
}
}
class PowerLowerThanZero_power : IPower
{
public int GetPower()
{
return 0;
}
}
class PowerHigherThan200_power : IPower
{
public int GetPower()
{
return 210;
}
}
但注意到每次都要新声明一个IPower的派生类,太繁琐了。所以此处引入Moq:
using ConsoleAppPractice;
using Moq;
using System;
using Xunit;
namespace ConsoleApp1.test
{
public class PowerTest
{
[Fact]
public void PowerLowerThanZero()
{
var mock = new Mock<IPower>();
mock.Setup(ps => ps.GetPower()).Returns(() => 0);
Fan fan = new Fan(mock.Object);
var expected = "Won't work.";
var actual = fan.WorkCondition();
Assert.Equal(actual, expected);
}
[Fact]
public void PowerHigherThan200()
{
var mock = new Mock<IPower>();
mock.Setup(ps => ps.GetPower()).Returns(() => 300);
Fan fan = new Fan(mock.Object);
var expected = "Warning!!";
var actual = fan.WorkCondition();
Assert.Equal(actual, expected);
}
}
}
里边那堆东西是Lambda表达式。
四、接口隔离原则
我们都知道,接口是一种对供需方均做出了约束的协议。对于供方,“不会少给”很容易做到,因为接口要求供方必须实现其里面的所有方法,否则不能实例化。但对于需方来说,“不会多要”这一点则是软性的。
什么是不会多要呢?意思是说,需方应该全部用到提供的接口里的东西,接口中不能存在一些完全没被用过的方法,即接口不能太大。如果接口太大,可以将其拆分成多个小接口。
第一种违反接口隔离原则,是传进来的接口太大,有些方法用不到,故应该分为小接口
namespace ConsoleAppPractice
{
class Program
{
static void Main()
{
ITank tank = new HeavyTank();
tank.Run();
}
}
interface IVehicle
{
void Run();
}
class Car : IVehicle
{
public void Run()
{
Console.WriteLine("A car is running!");
}
}
class Truck : IVehicle
{
public void Run()
{
Console.WriteLine("A truck is running!");
}
}
interface ITank
{
void Run();
void Fire();
}
class HeavyTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!!!");
}
public void Run()
{
Console.WriteLine("A heavytank is running......");
}
}
class LightTank : ITank
{
public void Fire()
{
Console.WriteLine("boom!");
}
public void Run()
{
Console.WriteLine("A lighttank is running......");
}
}
}
在这个例子中,我们只想把坦克作为代步工具,因而,ITank这个接口里的Fire方法完全用不到。
而事实上,我们确实可以把坦克这个事物在这个场景的意义下拆分为两方面,一方面是用于行走的轮子,一方面是用于攻击的火炮。因而,我们可以选择把ITank这个接口拆分为两个小接口,让tank全部都接上那两个小接口:
namespace ConsoleAppPractice
{
class Program
{
static void Main()
{
IVehicle tank = new HeavyTank();
tank.Run();
}
}
interface IVehicle
{
void Run();
}
class Car : IVehicle
{
//
}
class Truck : IVehicle
{
//
}
interface IPowder
{
void Fire();
}
class HeavyTank : IPowder,IVehicle
{
public void Fire()
{
Console.WriteLine("Boom!!!");
}
public void Run()
{
Console.WriteLine("A heavytank is running......");
}
}
class LightTank : IPowder, IVehicle
{
//
}
}
但是请注意,这个也不能使用得太过火了,因为它可能会让只有一个方法的小接口越来越多,颗粒度增加。因而,应该把类的大小和接口大小都控制在一个范围内。
第二种违反接口隔离原则的,是一个大接口由两个设计很好的小接口合并,传的时候却传了大接口,这就导致本来会用到的被隔绝在了门外。
比如说本例:
namespace ConsoleAppPractice
{
class Program
{
static void Main()
{
ITank tank = new Car();
}
}
interface IVehicle
{
void Run();
}
//
interface IPowder
{
void Fire();
}
interface ITank : IPowder, IVehicle
{
}
class HeavyTank : ITank
{
//
}
class LightTank : ITank
{
//
}
}
此时编译器会报错,因为Car已经被你挡在外面了。
再比如说,我们之前一直常用到的IEnumerable这个接口,其实它有一个更大的接口,是ICollection。
为了做我们的例子,我们需要创建一个只接上IEnumerable的接口,而没有接上ICollection接口的类。
namespace ConsoleAppPractice
{
class Program
{
static void Main()
{
int[] nums1 = new int[] { 1, 2, 3, 4, 5 };
ArrayList nums2 = new ArrayList() { 1, 2, 3, 4, 5 };
ReadOnlyCollection collection = new ReadOnlyCollection(nums1);
Console.WriteLine(Calculator.Sum(nums1));
Console.WriteLine(Calculator.Sum(nums2));
Console.WriteLine(Calculator.Sum(collection));
}
}
class Calculator
{
public static double Sum(IEnumerable vs)
{
double sum = 0;
foreach (var item in vs)
{
sum += (int)item;
}
return sum;
}
}
class ReadOnlyCollection : IEnumerable
{
private int[] _nums;
public ReadOnlyCollection(int[] nums)
{
_nums = nums;
}
public IEnumerator GetEnumerator()
{
return new Enumerator(this);
}
class Enumerator : IEnumerator//把Enumerator放在类的里面,防止污染名称空间
{
private ReadOnlyCollection _collection;
private int _head;
public Enumerator(ReadOnlyCollection r)
{
_collection = r;
_head = -1;
}
public object Current
{
get
{
object o = _collection._nums[_head];//装箱
return o;
}
}
public bool MoveNext()
{
if (++_head < _collection._nums.Length)
{
return true;
}
else
{
return false;
}
}
public void Reset()
{
_head = -1;
}
}
}
}
如果是这样,非常完美。【注:此处感兴趣可以看看P16讲表达式的时候,老师讲过了foreach的语法糖】
但是如果把Sum需求者的接口改为ICollection,立马报错第十五行,此时你已经把这个IEnumerable拒之门外了。
我们知道,这个功能只需要可迭代就行了。传IEnumerable明显比传ICollection合适。
第三个接口隔离的例子,我们将展现C#语言特有的功能,即显式接口实现。
一个大接口可被拆分成若干个小接口。有没有一个办法,可以让只需要使用这个小接口时,接口的方法才能被看到呢?这个办法就是显式接口实现。
比如说下例:一个杀手是不能被别人发现自己有Kill这个方法的:
namespace ConsoleAppPractice
{
class Program
{
static void Main()
{
var Jeck = new WarmKiller();
}
}
interface IKiller
{
void Kill();
}
interface IGentleman
{
void Love();
}
class WarmKiller : IKiller, IGentleman
{
public void Kill()
{
Console.WriteLine("I will kill the enemy.");
}
public void Love()
{
Console.WriteLine("I love you.");
}
}
}
如果是这样的话,就会被发现有kill方法,很不合理。
这时候就得用到第二项,即显式实现方法。
?此时,除非显式声明变量:
?
?否则,是看不到Kill这个方法的。
?此时想看到love这个方法需要做的。
|