:::info 接口和抽象类既是理论难点,又是代码难点。
接口和抽象类用得好,写出来的代码才好测试。 :::

引言

软件也是工业的分支,设计严谨的软件必须经得起测试。
软件能不能测试、测试出问题后好不好修复、软件整体运行状态好不好监控,都依赖于对接口和抽象类的使用。
接口和抽象类是现代面向对象的基石,也是高阶面向对象程序设计的起点。

学习设计模式的前提:

  1. 透彻理解并熟练使用接口和抽象类
  2. 深入理解 SOLID 设计原则,并在日常工作中自觉得使用它们

    SOLID

  • SRP: Single Responsibility Principle
  • OCP: Open Closed Principle
  • LSP: Liskov Substitution Principle
  • ISP: InterfaceSegregation Principle
  • DIP: Dependency Inversion Principle

指代了面向对象编程和面向对象设计的五个基本原则。

首字母 指代 概念
S 单一功能原则 对象应该仅具有一种单一功能。
O 开闭原则 软件体应该是对于扩展开放的,但是对于修改封闭的。
L 里氏替换原则 程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换。
参考 契约式设计
I 接口隔离原则 多个特定客户端接口要好于一个宽泛用途的接口。
D 依赖反转原则 一个方法应该遵从“依赖于抽象而不是一个实例”。依赖注入是该原则的一种实现方式。

关于 C# 设计原则的更多知识,推荐《Agile Principles, Patterns, and Practices in C#》。
Agile Principles, Patterns, and Practices in C#.pdf

为做基类而生的“抽象类”与“开闭原则”

抽象类和开闭原则有密切的联系。

设计原则的重要性和它在敏捷开发中扮演的重要角色:
这些规则就如同交通规则,是为了高效协作而诞生的

  1. 硬性规定:例如变量名合法,语法合法
  2. 软性规则

    抽象类

    一个类里面一旦有了 abstract成员,类就变成了抽象类,就必须标 abstract

抽象类内部至少有一个函数成员未完全实现

  1. abstract class Student
  2. {
  3. abstract public void Study();
  4. }

abstract 成员即暂未实现的成员,因为它必须在子类中被实现,所以抽象类不能是 private的
因为抽象类内部还有未实现的函数成员,计算机不知道怎么调用这类成员,于是编译器干脆不允许你实例化抽象类。

一个类不允许实例化,它就只剩两个用处:

  1. 作为基类,在派生类里面实现基类中的 abstract成员
  2. 声明基类(抽象类)类型变量去引用子类(已实现基类中的abstract成员)类型的实例,这又称为“多态”。

抽象方法的实现,看起来和override重写 virtual 方法有些类似,所以抽象方法在某些编程语言(如 C++)中又被称为“纯虚方法”。

virtual(虚方法)还是有方法体的,只不过是等着被子类重写abstract(纯虚方法)却连方法体都没有。

我们之前学的非抽象类又称为 Concrete Class。

开闭原则

如果不是为了修复 bug 和添加新功能,别总去修改类的代码,特别是类当中函数成员的代码。
我们应该封装那些不变的、稳定的、固定的和确定的成员,而把那些不确定的,有可能改变的成员声明为抽象成员,并且留给子类去实现。

开放修复 bug 和添加新功能,关闭对类的更改。

示例

示例演示如何添加交通工具类,通过版本的迭代来讲解开闭原则、抽象类和接口。

初始版本

从 Car 直接 copy 代码到 Truck:

  1. class Car
  2. {
  3. public void Run()
  4. {
  5. Console.WriteLine("Car is running...");
  6. }
  7. public void Stop()
  8. {
  9. Console.WriteLine("Stopped!");
  10. }
  11. }
  12. class Truck
  13. {
  14. public void Run()
  15. {
  16. Console.WriteLine("Truck is running...");
  17. }
  18. public void Stop()
  19. {
  20. Console.WriteLine("Stopped!");
  21. }
  22. }

这就已经违反了设计原则:不能 copy paste

提取父类版

将相同的方法提取出来放在父类里面:

  1. class Vehicle
  2. {
  3. public void Stop()
  4. {
  5. Console.WriteLine("Stopped!");
  6. }
  7. }
  8. class Car : Vehicle
  9. {
  10. public void Run()
  11. {
  12. Console.WriteLine("Car is running...");
  13. }
  14. }
  15. class Truck : Vehicle
  16. {
  17. public void Run()
  18. {
  19. Console.WriteLine("Truck is running...");
  20. }
  21. }

但这样会有一个问题就是 Vehicle 类型变量无法调用 Run 方法,有两种解决方法:
1.Vehicle 里面添加一个带参数的 Run 方法:

  1. class Vehicle
  2. {
  3. public void Stop()
  4. {
  5. Console.WriteLine("Stopped!");
  6. }
  7. public void Fill()
  8. {
  9. Console.WriteLine("Pay and fill...");
  10. }
  11. public void Run(string type)
  12. {
  13. if (type == "car")
  14. {
  15. Console.WriteLine("Car is running...");
  16. }
  17. else if (type == "truck")
  18. {
  19. Console.WriteLine("Truck is running...");
  20. }
  21. else if (type == "RaceCar")
  22. {
  23. Console.WriteLine("RaceCar is running...");
  24. }
  25. }
  26. }
  27. class Car
  28. {
  29. public void Run()
  30. {
  31. Console.WriteLine("Car is running!");
  32. }
  33. }
  34. class Trunk
  35. {
  36. public void Run()
  37. {
  38. Console.WriteLine("Trunk is running!");
  39. }
  40. }
  41. class RaceCar
  42. {
  43. public void Run()
  44. {
  45. Console.WriteLine("RaceCar is running!");
  46. }
  47. }

上述代码违反“开闭原则”,既未修缮 bug 又未添新功能就多了个 Run 方法。
而且一旦以后再添加别的交通工具类,又需打开(Open) Vehicle 类,修改 Run 方法。
2.虚方法:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. Vehicle v = new Car();
  6. v.Run();
  7. // Car is running...
  8. }
  9. }
  10. class Vehicle
  11. {
  12. public void Stop()
  13. {
  14. Console.WriteLine("Stopped!");
  15. }
  16. public virtual void Run()
  17. {
  18. Console.WriteLine("Vehicle is running...");
  19. }
  20. }
  21. class Car : Vehicle
  22. {
  23. public override void Run()
  24. {
  25. Console.WriteLine("Car is running...");
  26. }
  27. }
  28. class Truck : Vehicle
  29. {
  30. public override void Run()
  31. {
  32. Console.WriteLine("Truck is running...");
  33. }
  34. }

虚方法解决了 Vehicle 类型变量调用子类 Run 方法的问题,也遗留下来一个问题:Vehicle 的 Run 方法的行为本身就很模糊,且在实际应用中也根本不会被调到。
而且从测试的角度来看,测试一段你永远用不到的代码,也是不合理的。

抽象类版

要不就干脆 Run 方法里面什么都不写,进而直接把 Run 的方法体干掉,Run 就变成了一个抽象方法。于是 Vehicle 也变成了抽象类。

  1. // abstract class ***** 示例
  2. abstract class Vehicle
  3. {
  4. public void Stop()
  5. {
  6. Console.WriteLine("Stopped!");
  7. }
  8. // public abstract void *****(); 示例
  9. public abstract void Run();
  10. }

当 Vehicle 变成抽象类后,再添加新的继承于 Vehicle 的类就很简单了,也无需修改 Vehicle 的代码。

  1. abstract class Vehicle
  2. {
  3. public void Stop()
  4. {
  5. Console.WriteLine("Stopped!");
  6. }
  7. public abstract void Run();
  8. }
  9. class Car : Vehicle
  10. {
  11. public override void Run()
  12. {
  13. Console.WriteLine("Car is running...");
  14. }
  15. }
  16. class Truck : Vehicle
  17. {
  18. public override void Run()
  19. {
  20. Console.WriteLine("Truck is running...");
  21. }
  22. }
  23. class RaceCar : Vehicle
  24. {
  25. public override void Run()
  26. {
  27. Console.WriteLine("Race car is running...");
  28. }
  29. }

不光要掌握最后虚方法的用法,还有理解之前过程中的问题,进而识别并改善工作中的代码。

纯抽象类版(接口)

有没有一种可能,一个抽象类里面的所有方法都是抽象方法?
VehicleBase 是纯虚类,它将成员的实现向下推,推到 Vehicle。Vehicle 实现了 Stop 和 Fill 后将 Run 的实现继续向下推。

  1. // 特别抽象
  2. abstract class VehicleBase
  3. {
  4. public abstract void Stop();
  5. public abstract void Fill();
  6. public abstract void Run();
  7. }
  8. // 抽象
  9. abstract class Vehicle:VehicleBase
  10. {
  11. public override void Stop()
  12. {
  13. Console.WriteLine("Stopped!");
  14. }
  15. public override void Fill()
  16. {
  17. Console.WriteLine("Pay and fill...");
  18. }
  19. }
  20. // 具体
  21. class Car : Vehicle
  22. {
  23. public override void Run()
  24. {
  25. Console.WriteLine("Car is running...");
  26. }
  27. }

在 C++ 中能看到这种纯虚类的写法,但在 C# 和 Java 中,纯虚类其实就是接口。

  1. 因为interface要求其内部所有成员都是 public 的,所以就把 public 去掉了
  2. 接口本身就包含了“是纯抽象类”的含义(所有成员一定是抽象的),所以abstract也去掉了
  3. 因为 abstract 关键字去掉了,所以实现过程中的 override关键字也去掉了 ```csharp interface VehicleBase { /public/ void Stop();

    void Fill();

    void Run(); }

abstract class Vehicle : VehicleBase { public void Stop() { Console.WriteLine(“Stopped!”); }

  1. public void Fill()
  2. {
  3. Console.WriteLine("Pay and fill...");
  4. }
  5. // Run 暂未实现,所以依然是 abstract 的
  6. public abstract void Run();

}

class Car : Vehicle { public override void Run() { Console.WriteLine(“Car is running…”); } }

  1. 纯虚类演变成了接口
  2. > 现在的代码架构就有点像平时工作中用的了。
  3. 又因为接口在 C# 中的命名约定以` I `开头,加一名词(示例:IWork):
  4. ```csharp
  5. interface IVehicle
  6. {
  7. void Stop();
  8. void Fill();
  9. void Run();
  10. }

汇总上述所有方法的简单架构。

  1. using System;
  2. namespace Example27
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. Vehicle v = new Car();
  9. v.Run();
  10. }
  11. }
  12. interface IVehicle
  13. {
  14. void Run();
  15. void Stop();
  16. void Fill();
  17. }
  18. abstract class Vehicle:IVehicle
  19. {
  20. public void Stop()
  21. {
  22. Console.WriteLine("Stopped!");
  23. }
  24. public void Fill()
  25. {
  26. Console.WriteLine("Pay and Fill...");
  27. }
  28. abstract public void Run();
  29. }
  30. class Car : Vehicle
  31. {
  32. public override void Run()
  33. {
  34. Console.WriteLine("Car is running...");
  35. }
  36. }
  37. class Truck : Vehicle
  38. {
  39. public override void Run()
  40. {
  41. Console.WriteLine("Truck is running...");
  42. }
  43. }
  44. class RaceCar : Vehicle
  45. {
  46. public override void Run()
  47. {
  48. Console.WriteLine("RaceCar is running...");
  49. }
  50. }
  51. }

总结:接口和抽象类

抽象类

  • 接口和抽象类都是“软件工程产物”
  • 具体类 -> 抽象类 -> 接口:越来越抽象,内部实现的东西越来越少
  • 抽象类是未完全实现逻辑的“类”(可以有字段和非 public 成员,它们代表了“具体逻辑”)
  • 抽象类为复用而生:专门作为基类来使用。也具有解耦功能
  • 封装确定的,开放不确定的(开闭原则),推迟到合适的子类中去实观

    接口

  • 接口是完全未实现逻辑的“类”(“纯虚类”;只有函数成员;成员全部 public,且为隐式public)

    从 C# 8 开始,使用默认接口方法,您可以拥有一个接口定义,以及该定义中某些或所有方法的默认实现。

  • 接口为解耦而生:“高内聚,低耦合”,方便单元测试

  • 接口是一个“协约”。早已为工业生产所熟知(有分工必有协作,有协作必有协约)
  • 它们都不能实例化。只能用来声明变量、引用具体类(concrete class)的实例

对于一个方法来说,方法体就是它的实现;对于数据成员,如字段,它就是对类存储数据的实现。