Skip to Content
Week 03Day 3 - 接口与抽象类

Day 3 - 接口与抽象类

建议用时:180-210 分钟

你将学会什么

  • 接口是什么,为什么说它是能力约定
  • 类实现接口是什么意思
  • 为什么服务应该依赖接口,而不是直接依赖具体类
  • 抽象类是什么,为什么不能直接 new
  • 接口和抽象类的区别
  • 如何用抽象类提供公共流程,用子类补差异
  • 如何在一个小例子里替换不同实现

接口和抽象类不是为了写得复杂。它们的目标是让调用者依赖“能力”,而不是绑死某个具体实现。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“继承与组合”,今天继续学习“接口与抽象类”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“接口与抽象类”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分先解决“这是什么、为什么要这样写”。每看到一个短例子就亲手敲一遍并运行,不要先背完整程序。

今天先抓住什么

接口解决的是:

调用者只关心你能做什么,不关心你具体怎么做

例如订单服务只需要“能发送通知”的能力:

INotifier Send(message)

至于是邮件、短信、控制台输出,都可以换。

名词先讲清楚

名词直接解释例子
接口能力约定,只规定要做什么INotifier
实现接口类按接口要求写具体代码EmailNotifier : INotifier
抽象类不能直接创建对象的半成品父类abstract class BaseNotifier
抽象方法只声明,不写具体代码protected abstract string GetPrefix()
依赖接口字段或参数类型写接口INotifier notifier
多态同一个接口变量指向不同实现new EmailNotifier()new SmsNotifier()

接口是什么

接口只规定能力,不规定实现。

INotifier notifier = new ConsoleNotifier(); notifier.Send("保存成功"); interface INotifier { void Send(string message); } class ConsoleNotifier : INotifier { public void Send(string message) { Console.WriteLine(message); } }

INotifier 要求实现类必须有 Send 方法。ConsoleNotifier 负责具体怎么发送。

为什么要依赖接口

如果 OrderService 直接依赖 EmailNotifier

OrderService -> EmailNotifier

以后要换短信通知,就要改 OrderService

如果依赖接口:

OrderService -> INotifier EmailNotifier -> INotifier SmsNotifier -> INotifier

OrderService 不关心具体是谁,只要能 Send 就行。

两个实现可以互换

void SendMessage(INotifier notifier) { notifier.Send("保存成功"); } SendMessage(new ConsoleNotifier()); SendMessage(new SilentNotifier()); interface INotifier { void Send(string message); } class ConsoleNotifier : INotifier { public void Send(string message) { Console.WriteLine(message); } } class SilentNotifier : INotifier { public void Send(string message) { } }

SendMessage 只依赖接口,所以两个实现都能传进去。

抽象类是什么

抽象类是“有一部分写好了,但还不完整”的父类。

var notifier = new EmailNotifier(); notifier.Send("保存成功"); abstract class BaseNotifier { public void Send(string message) { Console.WriteLine($"[{GetPrefix()}] {message}"); } protected abstract string GetPrefix(); } class EmailNotifier : BaseNotifier { protected override string GetPrefix() { return "Email"; } }

BaseNotifier 提供统一的 Send 流程,但不知道前缀是什么,所以把 GetPrefix 留给子类。

接口和抽象类怎么选

需求选什么
只规定能力,不关心实现接口
多个类有共同流程或共同字段抽象类
一个类需要实现多个能力接口
需要默认实现和公共保护方法抽象类

简单规则:

先用接口表达能力。 如果多个实现有共同代码,再抽抽象类。

接口和抽象类常用写法速查

需求写法说明
定义能力interface IProductRepository只说能做什么
实现接口class JsonRepository : IProductRepository给出具体做法
依赖接口ProductService(IProductRepository repo)方便替换实现
抽象方法public abstract void Save()子类必须实现
公共方法普通方法写在抽象类里子类复用流程
受保护成员protected给子类用,不给外部用

接口模板:

interface IProductRepository { void Save(Product product); Product? FindByName(string name); } class MemoryProductRepository : IProductRepository { private readonly List<Product> products = new(); public void Save(Product product) { products.Add(product); } public Product? FindByName(string name) { return products.FirstOrDefault(product => product.Name == name); } }

依赖接口模板:

class ProductService { private readonly IProductRepository repository; public ProductService(IProductRepository repository) { this.repository = repository; } }

常见错误

错误修法
接口里塞太多方法拆成更小的接口
抽象类当普通类直接 new只能 new 子类
服务直接依赖具体类改成依赖接口
实现接口后漏方法按接口补全方法
为了复杂而抽象确实有替换需求再抽

今天写代码时按这个顺序

  1. 先写具体类,让功能跑通。
  2. 观察调用者真正需要什么能力。
  3. 把能力抽成接口。
  4. 让具体类实现接口。
  5. 调用者改成依赖接口。
  6. 如果多个实现有共同流程,再加抽象类。

第二部分:把知识组合成完整例子

前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个订单服务:订单保存后发送通知,但通知方式可以替换。

最终你要写出这个程序

INotifier notifier = new EmailNotifier(); var service = new OrderService(notifier); service.Save("A001"); interface INotifier { void Send(string message); } abstract class BaseNotifier : INotifier { public void Send(string message) { Console.WriteLine($"[{GetPrefix()}] {message}"); } protected abstract string GetPrefix(); } class EmailNotifier : BaseNotifier { protected override string GetPrefix() { return "Email"; } } class SmsNotifier : BaseNotifier { protected override string GetPrefix() { return "SMS"; } } class OrderService { private readonly INotifier notifier; public OrderService(INotifier notifier) { this.notifier = notifier; } public void Save(string orderNo) { Console.WriteLine($"订单 {orderNo} 已保存"); notifier.Send($"订单 {orderNo} 保存成功"); } }

输出:

订单 A001 已保存 [Email] 订单 A001 保存成功

如果把第一行改成:

INotifier notifier = new SmsNotifier();

输出前缀会变成 [SMS],但 OrderService 不需要改。

第三部分:跟着敲代码

从这里开始动手。每个例子都可以直接放进 Program.cs 运行。

例子 1:最小接口

INotifier notifier = new ConsoleNotifier(); notifier.Send("保存成功"); interface INotifier { void Send(string message); } class ConsoleNotifier : INotifier { public void Send(string message) { Console.WriteLine(message); } }

例子 2:两个实现替换

void SendMessage(INotifier notifier) { notifier.Send("保存成功"); } SendMessage(new ConsoleNotifier()); SendMessage(new SilentNotifier()); interface INotifier { void Send(string message); } class ConsoleNotifier : INotifier { public void Send(string message) { Console.WriteLine(message); } } class SilentNotifier : INotifier { public void Send(string message) { Console.WriteLine("静默通知:不真正发送"); } }

例子 3:服务依赖接口

var service = new OrderService(new ConsoleNotifier()); service.Save("A001"); interface INotifier { void Send(string message); } class ConsoleNotifier : INotifier { public void Send(string message) { Console.WriteLine(message); } } class OrderService { private readonly INotifier notifier; public OrderService(INotifier notifier) { this.notifier = notifier; } public void Save(string orderNo) { Console.WriteLine($"订单 {orderNo} 已保存"); notifier.Send($"订单 {orderNo} 保存成功"); } }

例子 4:抽象类提供公共流程

BaseNotifier notifier = new EmailNotifier(); notifier.Send("保存成功"); abstract class BaseNotifier { public void Send(string message) { Console.WriteLine($"[{GetPrefix()}] {message}"); } protected abstract string GetPrefix(); } class EmailNotifier : BaseNotifier { protected override string GetPrefix() { return "Email"; } }

例子 5:接口 + 抽象类组合使用

INotifier email = new EmailNotifier(); INotifier sms = new SmsNotifier(); email.Send("订单已保存"); sms.Send("订单已保存"); interface INotifier { void Send(string message); } abstract class BaseNotifier : INotifier { public void Send(string message) { Console.WriteLine($"[{GetPrefix()}] {message}"); } protected abstract string GetPrefix(); } class EmailNotifier : BaseNotifier { protected override string GetPrefix() { return "Email"; } } class SmsNotifier : BaseNotifier { protected override string GetPrefix() { return "SMS"; } }

小白重复敲写训练

先只写一个接口和一个实现,再增加第二个实现。

训练 1:第一个接口

IPrinter printer = new ConsolePrinter(); printer.Print("hello"); interface IPrinter { void Print(string text); } class ConsolePrinter : IPrinter { public void Print(string text) { Console.WriteLine(text); } }

改动任务:增加 UpperPrinter,把文本转成大写后输出。

训练 2:接口作为方法参数

void Run(INotifier notifier) { notifier.Send("任务完成"); } Run(new ConsoleNotifier()); interface INotifier { void Send(string message); } class ConsoleNotifier : INotifier { public void Send(string message) => Console.WriteLine(message); }

第二遍写一个 SilentNotifier,方法体保持为空。

训练 3:最小抽象类

Shape shape = new Rectangle { Width = 3, Height = 4 }; Console.WriteLine(shape.Area()); abstract class Shape { public abstract double Area(); } class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } public override double Area() => Width * Height; }

第三遍增加一个圆形实现。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:接口与抽象类。今天至少要掌握的 3 个点是:

1. 接口是什么,为什么说它是能力约定 2. 类实现接口是什么意思 3. 为什么服务应该依赖接口,而不是直接依赖具体类

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:接口是什么,为什么说它是能力约定。

上位机专项练习

接口规定通信能力,抽象类共享设备公共逻辑,方便替换串口、TCP 或模拟连接。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:定义设备连接接口

interface IDeviceConnection { bool Connect(); string Read(); } class FakeConnection : IDeviceConnection { public bool Connect() => true; public string Read() => "25.6"; } class Program { static void Main() { IDeviceConnection connection = new FakeConnection(); Console.WriteLine(connection.Connect()); Console.WriteLine(connection.Read()); } }

运行结果或界面效果:

True 25.6

改动任务: 增加 Disconnect 方法。

专项例子 2:替换不同读取实现

interface IReader { double ReadValue(); } class SerialReader : IReader { public double ReadValue() => 20.1; } class TcpReader : IReader { public double ReadValue() => 30.2; } class Program { static void Print(IReader reader) { Console.WriteLine(reader.ReadValue()); } static void Main() { Print(new SerialReader()); Print(new TcpReader()); } }

运行结果或界面效果:

20.1 30.2

改动任务: 增加返回 99.9 的模拟实现。

专项例子 3:抽象类保存公共名称

abstract class DeviceBase { public string Name { get; } protected DeviceBase(string name) { Name = name; } public abstract double ReadValue(); } class SimulatorDevice : DeviceBase { public SimulatorDevice(string name) : base(name) { } public override double ReadValue() => 42.0; } class Program { static void Main() { DeviceBase device = new SimulatorDevice("模拟设备"); Console.WriteLine($"{device.Name}: {device.ReadValue()}"); } }

运行结果或界面效果:

模拟设备: 42

改动任务: 把模拟值改成 55.5。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。

作业要求

写一个订单保存和通知程序,要求:

  1. 定义 INotifier 接口。
  2. EmailNotifierSmsNotifier 都实现通知能力。
  3. 抽象类 BaseNotifier 提供统一输出格式。
  4. OrderService 依赖 INotifier
  5. 切换通知方式时,不修改 OrderService

完整代码

INotifier notifier = new EmailNotifier(); var service = new OrderService(notifier); service.Save("A001"); notifier = new SmsNotifier(); service = new OrderService(notifier); service.Save("A002"); interface INotifier { void Send(string message); } abstract class BaseNotifier : INotifier { public void Send(string message) { Console.WriteLine($"[{GetPrefix()}] {message}"); } protected abstract string GetPrefix(); } class EmailNotifier : BaseNotifier { protected override string GetPrefix() { return "Email"; } } class SmsNotifier : BaseNotifier { protected override string GetPrefix() { return "SMS"; } } class OrderService { private readonly INotifier notifier; public OrderService(INotifier notifier) { this.notifier = notifier; } public void Save(string orderNo) { Console.WriteLine($"订单 {orderNo} 已保存"); notifier.Send($"订单 {orderNo} 保存成功"); } }

正确运行结果

订单 A001 已保存 [Email] 订单 A001 保存成功 订单 A002 已保存 [SMS] 订单 A002 保存成功

你要能说清楚这 6 件事

  • 接口规定能力,不关心具体实现。
  • 类实现接口后,必须提供接口要求的方法。
  • 服务依赖接口后,实现可以替换。
  • 抽象类不能直接创建对象。
  • 抽象类适合提供公共流程。
  • 子类通过 override 补具体差异。