1. 引言
所以我们已经发现接口其实就是很强的契约。那么接口级编程到底是啥?其实就是一种设计哲学,意思是:“依赖抽象,不要依赖具体实现。”
再来个比喻。你想煮咖啡,你买了台咖啡机。你其实不在乎它是具体哪款“超级无敌机V3000”还是“巨能Barista 5000”,对吧?你只关心这玩意能“做咖啡”——也就是实现了ICoffeeMaker这个“契约”。你只要按下“做咖啡”按钮,它就能搞定。至于它是用咖啡豆、胶囊还是别的啥,对你来说根本不用管。
在代码里,这就意味着你的方法里不用死盯着Document或者Image这种具体类型,而是要用IPrintable。你的代码就能和任何实现了这个接口的对象打交道,根本不用知道它具体是啥类型。
接口契约——就是保证每个实现接口的类都必须实现接口里所有成员(方法、属性、事件)。对程序来说,任何实现了接口的对象都支持一套“能力”,你完全可以放心用,根本不用管它内部怎么实现的。
比如你和同事约好总是在大厅的红沙发见面。他怎么去——坐电梯、走楼梯还是爬墙——你都不用管。关键是:他一定会到。接口就是代码里的“红沙发”。
例子
public interface IPrintable
{
void Print();
}
契约:谁实现了IPrintable,谁就得能把自己打印到屏幕上。怎么打印——那是细节。
2. 契约作为系统各部分的交互点
实际怎么用
一个系统可能有几十个类,都是不同的人、不同时间、不同目的写的。但只要大家都遵守同一个契约(也就是实现同一个接口),你就能统一地用它们。
真实应用例子:想象一下客户数据库。有Customer类、Employee类、Contractor类。每个类存信息的方式都不一样,但只要它们都实现了比如IContactInfo接口,这个接口要求有GetEmail()和GetPhoneNumber()方法,那外部代码根本不用管对象是什么类型——只要能拿到email和电话就行。
public interface IContactInfo
{
string GetEmail();
string GetPhoneNumber();
}
public class Customer : IContactInfo
{
public string Email { get; set; }
public string Phone { get; set; }
public string GetEmail() => Email;
public string GetPhoneNumber() => Phone;
}
// Employee和Contractor同理...
现在如果你要打印所有公司有联系的人的数据(不管是谁),你只要遍历IContactInfo列表,调用方法——就全搞定了。
3. “接口级编程”
接口级编程——就是写代码时只依赖接口(契约),不依赖具体类。只要类实现了接口,怎么实现都行,只要满足接口要求就OK。
为啥这么重要?
- 可扩展性:加新类型很容易,老代码不用动。
- 可测试性:测试时可以随便用mock(假对象)替换。
- 灵活性:实现可以天天变,接口一直不变。
- 架构清晰:模块之间耦合度低,复用性高。
例子——熟悉的程序:“银行账户”
之前的例子里我们有个抽象类BankAccount,有个抽象方法Withdraw(),具体类型(SavingsAccount、CheckingAccount)实现细节。
现在我们加个接口——比如用来输出余额信息:
public interface IBalanceReporter
{
void ReportBalance();
}
public abstract class BankAccount : IBalanceReporter
{
public double Balance { get; set; }
public abstract void Withdraw(double amount);
public void ReportBalance()
{
Console.WriteLine($"当前余额: {Balance} 欧元");
}
}
现在所有用IBalanceReporter的代码都能调用ReportBalance(),不用管具体账户类型。
4. 通过契约实现通用处理和多态
例子:通用处理器
只要用接口,你就能写出和对象类型无关的通用方法:
static void PrintAllBalances(IBalanceReporter[] accounts)
{
foreach (var reporter in accounts)
{
reporter.ReportBalance();
}
}
这个列表里可以放任何实现了IBalanceReporter的对象:SavingsAccount、CheckingAccount,甚至MockAccountForTesting。没有黑魔法,全部都能用。
图解:接口契约带来的好处
+-------------------+ 实现 +-------------------+
| BankAccount | <---------------- | IBalanceReporter |
| (SavingsAccount) | |-------------------|
| (CheckingAccount)| | + ReportBalance() |
+-------------------+ +-------------------+
| ^
| |
+-----------+-----------------------+
|
任何实现了契约的其他类
5. 契约、业务逻辑和架构设计
接口契约把类应该做什么和怎么做分得很清楚。所以搞架构的人都喜欢先用接口来设计系统逻辑。经常是先有接口(契约),实现可以后面再写。
生活中的例子:支付系统
电子钱包、银行卡、PayPal、加密货币……每种都有自己的细节,但如果抽象出来做个IPaymentProvider接口:
public interface IPaymentProvider
{
void Pay(decimal amount);
bool Refund(decimal amount);
}
和这个接口打交道的代码根本不用管是卡还是账户。对架构和实际开发都很方便:要支持新支付方式,老代码不用动。
6. 把业务逻辑提取到契约里
契约可以把主要的业务规则提到接口层,细节(比如额度检查、返现啥的)留给具体类去实现。
再来个例子
public interface ILogger
{
void LogInfo(string message);
void LogError(string message);
}
public class ConsoleLogger : ILogger
{
public void LogInfo(string message) => Console.WriteLine($"INFO: {message}");
public void LogError(string message) => Console.WriteLine($"ERROR: {message}");
}
public class FileLogger : ILogger
{
public void LogInfo(string message) => /* 写入文件 */;
public void LogError(string message) => /* 写入文件 */;
}
然后客户端代码就像这样(根本不用管用的是什么logger):
void DoWork(ILogger logger)
{
logger.LogInfo("工作开始了。");
// ... 做点啥 ...
logger.LogError("工作出错了。");
}
这就是接口级编程:客户端代码只依赖契约(接口),不依赖具体实现。
7. 错误和常见坑
新手常犯的错:代码死绑具体类,不用接口。这样会有一堆问题:
- 想换东西或测试很难——得改一堆代码。
- 加新类型要到处改。
- 模块之间耦合太紧。
反过来,如果一开始就用接口来设计系统、参数都用接口传递,就能做到低耦合和高灵活。
最重要的建议:多想想模块之间是“契约”在交互,而不是具体实现。
GO TO FULL VERSION