image.png

接口

abstract 中的抽象方法只规定了不能是 private 的,而接口中的“抽象方法”只能是 public 的。
这样的成员访问级别就决定了接口的本质:接口是服务消费者和服务提供者之间的契约
既然是契约,那就必须是透明的,对双方都是可见的。
除了 public,abstract 的抽象方法还可以是 protected 和 internal,它们都不是给功能调用者准备的,各自有特定的可见目标。

接口即契约(contract)
契约使自由合作成为可能,所谓自由合作就是一份合同摆在这里,它即约束服务的使用者也约束服务的提供者。如果该契约的使用者和提供者有多个,它们之间还能自由组合。

接口契约示例

求和和求平均值的操作…

未使用接口时:

不使用接口时,int[]和ArrayList不一致,需要再写一个方法。

  1. class Program{
  2. static void Main(string[] args){
  3. int[] nums1 = new int[] { 1, 2, 3, 4, 5 };
  4. ArrayList nums2 = new ArrayList { 1, 2, 3, 4, 5 };
  5. Console.WriteLine(Sum(nums1));
  6. Console.WriteLine(Avg(nums1));
  7. Console.WriteLine(Sum(nums2));
  8. Console.WriteLine(Avg(nums2));
  9. }
  10. static int Sum(int[] nums){
  11. int sum = 0;
  12. foreach (var n in nums){sum += n;}
  13. return sum;
  14. }
  15. static double Avg(int[] nums){
  16. int sum = 0;
  17. double count = 0;
  18. foreach (var n in nums){sum += n; count++;}
  19. return sum / count;
  20. }
  21. static int Sum(ArrayList nums){
  22. int sum = 0;
  23. foreach (var n in nums){sum += (int)n;}
  24. return sum;
  25. }
  26. static double Avg(ArrayList nums){
  27. int sum = 0;
  28. double count = 0;
  29. foreach (var n in nums){sum += (int)n; count++;}
  30. return sum / count;
  31. }
  32. }

供方是 nums1 和 nums2,需方是 Sum 和 Avg 这两函数。
需方需要传进来的参数可以迭代就行,别的不关心也用不到。
整型数组的基类是 Array,Array 和 ArrayList 都实现了 IEnumerable。

  1. static int Sum(IEnumerable nums){
  2. int sum = 0;
  3. foreach (var n in nums){
  4. sum += (int)n;
  5. }
  6. return sum;
  7. }
  8. static double Avg(IEnumerable nums){
  9. int sum = 0;
  10. double count = 0;
  11. foreach (var n in nums){
  12. sum += (int)n;
  13. count++;
  14. }
  15. return sum / count;
  16. }

依赖与耦合

现实世界中有分工、合作,面向对象是对现实世界的抽象,它也有分工、合作。
类与类、对象与对象间的分工、合作。
在面向对象中,合作有个专业术语“依赖”,依赖的同时就出现了耦合。依赖越直接,耦合就越紧

Car 与 Engine 紧耦合的示例:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var engine = new Engine();
  6. var car = new Car(engine);
  7. car.Run(3);
  8. Console.WriteLine(car.Speed);
  9. }
  10. }
  11. class Engine
  12. {
  13. public int RPM { get; private set; }
  14. public void Work(int gas)
  15. {
  16. this.RPM = 1000 * gas;
  17. }
  18. }
  19. class Car
  20. {
  21. // Car 里面有个 Engine 类型的字段,它两就是紧耦合了
  22. // Car 依赖于 Engine
  23. private Engine _engine;
  24. public int Speed { get; private set; }
  25. public Car(Engine engine)
  26. {
  27. _engine = engine;
  28. }
  29. public void Run(int gas)
  30. {
  31. _engine.Work(gas);
  32. this.Speed = _engine.RPM / 100;
  33. }
  34. }

紧耦合的问题:

  1. 基础类一旦出问题,上层类写得再好也没辙
  2. 程序调试时很难定位问题源头
  3. 基础类修改时,会影响写上层类的其他程序员的工作

所以程序开发中要尽量避免紧耦合,解决方法就是接口。

接口:

  1. 约束调用者只能调用接口中包含的方法
  2. 让调用者放心去调,不必关心方法怎么实现的、谁提供的

    接口解耦示例

    以老式手机举例,对用户来说他只关心手机可以接(打)电话和收(发)短信。
    对于手机厂商,接口约束了他只要造的是手机,就必须可靠实现上面的四个功能。

    用户如果丢了个手机,他只要再买个手机,不必关心是那个牌子的,肯定也包含这四个功能,上手就可以用。用术语来说就是“人和手机是解耦的”。 ```csharp class Program{ static void Main(string[] args){

    1. //var user = new PhoneUser(new NokiaPhone());
    2. var user = new PhoneUser(new EricssonPhone());
    3. user.UsePhone();
    4. Console.ReadKey();

    } }

class PhoneUser{ private IPhone _phone; public PhoneUser(IPhone phone){ _phone = phone; } public void UsePhone() { _phone.Dail(); _phone.PickUp(); _phone.Receive(); _phone.Send(); } }

interface IPhone{ //接口方法 void Dail(); void PickUp(); void Send(); void Receive(); }

class NokiaPhone : IPhone{ //接口方法具体实现1 public void Dail(){ Console.WriteLine(“Nokia calling …”); } public void PickUp(){ Console.WriteLine(“Hello! This is Tim!”); } public void Send(){ Console.WriteLine(“Nokia message ring …”); } public void Receive(){ Console.WriteLine(“Hello!”); } }

class EricssonPhone : IPhone{//接口方法具体实现2 public void Dail(){ Console.WriteLine(“Ericsson calling …”); } public void PickUp(){ Console.WriteLine(“Hello! This is Tim!”); } public void Send(){ Console.WriteLine(“Ericsson ring …”); } public void Receive(){ Console.WriteLine(“Good evening!”); } }

  1. 没有用接口时,如果一个类坏了,你需要 Open 它再去修改,修改时可能产生难以预料的副作用。引入接口后,耦合度大幅降低,换手机只需要换个类名,就可以了。
  2. 等学了反射后,连这里的一行代码都不需要改,只要在配置文件中修改一个名字即可。
  3. 在代码中只要有可以替换的地方,就一定有接口的存在;接口就是为了解耦(松耦合)而生。
  4. 松耦合最大的好处就是让功能的提供方变得可替换,从而降低紧耦合时“功能的提供方不可替换”带来的高风险和高成本。
  5. - 高风险:功能提供方一旦出问题,依赖于它的功能都挂
  6. - 高成本:如果功能提供方的程序员崩了,会导致功能使用方的整个团队工作受阻
  7. <a name="8cca4242"></a>
  8. # 依赖反转原则
  9. **● 解耦在代码中的表现就是依赖反转。**<br />**● 单元测试就是依赖反转在开发中的直接应用和直接受益者。**<br />![](https://cdn.nlark.com/yuque/0/2018/png/101969/1540105716604-2078d0ee-b4c2-4fb7-afc0-833bedff54df.png#crop=0&crop=0&crop=1&crop=1&from=url&id=QuIrX&margin=%5Bobject%20Object%5D&originHeight=286&originWidth=1160&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />人类解决问题的典型思维:自顶向下,逐步求精。
  10. 在面向对象里像这样来解决问题时,这些问题就变成了不同的类,且类和类之间紧耦合,它们也形成了这样的金字塔。
  11. 依赖反转给了我们一种新思路,用来**平衡**自顶向下的思维方式。
  12. > 平衡:不要一味推崇依赖反转,很多时候自顶向下就很好用,就该用。
  13. ![](https://cdn.nlark.com/yuque/0/2018/png/101969/1540105788912-4e958251-03e5-428a-bb7e-98e6ee495e13.png#crop=0&crop=0&crop=1&crop=1&from=url&id=NkncK&margin=%5Bobject%20Object%5D&originHeight=819&originWidth=1445&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  14. <a name="93b824b5"></a>
  15. # 单元测试
  16. 用例子来展示接口、解耦和依赖反转原则是怎么被单元测试应用的。<br />紧耦合:
  17. ```csharp
  18. class Program{
  19. static void Main(string[] args){
  20. var fan = new DeskFan(new PowerSupply());
  21. Console.WriteLine(fan.Work());
  22. }
  23. }
  24. // 背景:电扇有个电源,电源输出电流越大电扇转得越快
  25. // 电源输出有报警上限
  26. class PowerSupply{
  27. public int GetPower(){
  28. //return 100;
  29. return 210;
  30. }
  31. }
  32. class DeskFan{
  33. private PowerSupply _powerSupply;
  34. public DeskFan(PowerSupply powerSupply){
  35. _powerSupply = powerSupply;
  36. }
  37. public string Work(){
  38. int power = _powerSupply.GetPower();
  39. if (power <= 0){return "Won't work.";}
  40. else if (power < 100){return "Slow";}
  41. else if (power < 200){return "Work fine";}
  42. else{return "Warning";}
  43. }
  44. }

现在的问题是:我要测试电扇是否能按预期工作,我必须去修改 PowerSupply 里面的代码,这违反了开闭原则。
而且可能有除了电扇外的别的电器也连到了这个电源上面(在其他位置也引用了 PowerSupply),为了测试电扇工作就去改电源,很可能会造成别的问题。

接口的产生:自底向上(重构)和自顶向下(设计)。
只有对业务足够熟悉才能做到自顶向下,更多时候是一边写一边重构,现在我们就用接口去对电源和风扇进行解耦。

  1. class Program{
  2. static void Main(string[] args){
  3. var fan = new DeskFan(new PowerSupply());
  4. Console.WriteLine(fan.Work());
  5. }
  6. }
  7. public interface IPowerSupply{ //接口类型
  8. int GetPower();
  9. }
  10. public class PowerSupply : IPowerSupply{
  11. public int GetPower(){
  12. return 110;
  13. }
  14. }
  15. public class DeskFan{
  16. private IPowerSupply _powerSupply;
  17. public DeskFan(IPowerSupply powerSupply){
  18. _powerSupply = powerSupply;
  19. }
  20. public string Work()
  21. {
  22. int power = _powerSupply.GetPower();
  23. if (power <= 0){return "Won't work.";}
  24. else if (power < 100){return "Slow";}
  25. else if (power < 200){return "Work fine";}
  26. else{return "Warning";}
  27. }
  28. }

有接口后,我们就可以专门创建一个用于测试的电源类。

  1. 为了单元测试,将相关的类和接口都显式声明为 public
  2. 示例本身是个 .NET Core Console App,其相应的测试项目最好用 xUnit(官方之选)
  3. 测试项目命名:被测试项目名.Tests,例如 InterfaceExample.Tests
  4. 测试项目要引用被测试项目
  5. 测试项目里面的类和被测试项目的类一一对应,例如 DeskFanTests.cs ```csharp using Xunit;

namespace InterfaceExample.Tests { public class DeskFanTest { [Fact] public void PowerLowerThanZero_OK() { var fan = new DeskFan(new PowerSupplyLowerThanZero()); var expected = “Won’t work.”; var actual = fan.Work(); Assert.Equal(expected, actual); }

  1. [Fact]
  2. public void PowerHigherThan200_Warning()
  3. {
  4. var fan = new DeskFan(new PowerSupplyHigherThan200());
  5. // 注:此处为了演示,实际程序那边先故意改成了 Exploded!
  6. var expected = "Warning";
  7. var actual = fan.Work();
  8. Assert.Equal(expected, actual);
  9. }
  10. }
  11. class PowerSupplyLowerThanZero : IPowerSupply
  12. {
  13. public int GetPower()
  14. {
  15. return 0;
  16. }
  17. }
  18. class PowerSupplyHigherThan200 : IPowerSupply
  19. {
  20. public int GetPower()
  21. {
  22. return 220;
  23. }
  24. }

}

  1. 每当有新的代码提交后,就将 TestCase 全部跑一遍,如果原来通过了的,这次却没有通过(称为**回退**),就开始 Debug
  2. 平时工作中写测试 case 和写代码的重要性是一样的,没有测试 case 监控的代码的正确性、可靠度都不能保证。<br />程序想要能被测试,就需要引入接口、松耦合、依赖反转。
  3. <a name="Mock"></a>
  4. ## Mock
  5. 右击项目,点击![image.png](https://cdn.nlark.com/yuque/0/2022/png/26312855/1657461305139-e0c0c236-4c48-4f7b-8879-e187d1c759bf.png#clientId=ue571fb6b-a087-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=17&id=ue35d82fe&margin=%5Bobject%20Object%5D&name=image.png&originHeight=17&originWidth=157&originalType=binary&ratio=1&rotation=0&showTitle=false&size=5630&status=done&style=none&taskId=uecdb0ddd-5d17-49bf-8c6a-12d8b337e47&title=&width=157),搜Moq安装即可。
  6. ```csharp
  7. using Xunit;
  8. using Moq;
  9. namespace InterfaceExample.Tests
  10. {
  11. public class DeskFanTest
  12. {
  13. [Fact]
  14. public void PowerLowerThanZero_OK()
  15. {
  16. var mock = new Mock<IPowerSupply>();
  17. // 设置该 mock 对应的 GetPower 方法返回 0
  18. mock.Setup(ps => ps.GetPower()).Returns(() => 0);
  19. var fan = new DeskFan(mock.Object);
  20. var expected = "Won't work.";
  21. var actual = fan.Work();
  22. Assert.Equal(expected, actual);
  23. }
  24. [Fact]
  25. public void PowerHigherThan200_Warning()
  26. {
  27. var mock = new Mock<IPowerSupply>();
  28. mock.Setup(ps => ps.GetPower()).Returns(() => 220);
  29. var fan = new DeskFan(mock.Object);
  30. var expected = "Warning";
  31. var actual = fan.Work();
  32. Assert.Equal(expected, actual);
  33. }
  34. }
  35. }

总结

(20)接口,依赖反转,单元测试 - 图3
各种方法间的关系

  • 接口方法用虚线圆表示接口方法是纯虚方法
    • 接口方法的修饰符一定是 public
  • abstract 比纯虚方法稍微实现了点,但并未完全实现,其实现还要下推出去
    • 从 abstract 到 virtual 和实现方法必须加 override
  • virtual、override 和实现方法是有了逻辑有了方法体的方法
    • 从 virtual 到 override 方法也必须加 override