Day 3 - 接口与抽象类
建议用时:180-210 分钟
你将学会什么
- 接口是什么,为什么说它是能力约定
- 类实现接口是什么意思
- 为什么服务应该依赖接口,而不是直接依赖具体类
- 抽象类是什么,为什么不能直接
new - 接口和抽象类的区别
- 如何用抽象类提供公共流程,用子类补差异
- 如何在一个小例子里替换不同实现
接口和抽象类不是为了写得复杂。它们的目标是让调用者依赖“能力”,而不是绑死某个具体实现。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“继承与组合”,今天继续学习“接口与抽象类”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 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 -> INotifierOrderService 不关心具体是谁,只要能 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 子类 |
| 服务直接依赖具体类 | 改成依赖接口 |
| 实现接口后漏方法 | 按接口补全方法 |
| 为了复杂而抽象 | 确实有替换需求再抽 |
今天写代码时按这个顺序
- 先写具体类,让功能跑通。
- 观察调用者真正需要什么能力。
- 把能力抽成接口。
- 让具体类实现接口。
- 调用者改成依赖接口。
- 如果多个实现有共同流程,再加抽象类。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个订单服务:订单保存后发送通知,但通知方式可以替换。
最终你要写出这个程序
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。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。
作业要求
写一个订单保存和通知程序,要求:
- 定义
INotifier接口。 EmailNotifier和SmsNotifier都实现通知能力。- 抽象类
BaseNotifier提供统一输出格式。 OrderService依赖INotifier。- 切换通知方式时,不修改
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补具体差异。