接口解决依赖倒置与多态扩展问题,通过最小契约约定“能做什么”而非“怎么做”,支持不同实现类被同一逻辑无缝替换、单元测试模拟及依赖注入,但应避免滥用导致接口爆炸或暴露内部细节。

接口解决的是“依赖倒置”和“多态扩展”问题
不是为了写起来更“规范”,而是当多个类需要被同一套逻辑调用,又不能共享实现时,接口提供了最小契约——只约定“能做什么”,不约束“怎么做”。比如 IRepository 让 SqlRepository 和 InMemoryRepository 都能被 OrderService 无缝替换,而不用改服务层代码。
没有接口时,你只能靠基类或具体类型硬编码
一旦用 class SqlRepository : RepositoryBase,后续加 MongoRepository 就得让它也继承 RepositoryBase,但两者底层差异大,强行共用父类会迅速膨胀出一堆空实现或 throw new NotSupportedException()。接口则允许完全无关的类实现同一组方法:SqlRepository、MongoRepository、FakeRepository 可以各自独立,只共享 GetAsync、SaveAsync 等签名。
接口让单元测试和依赖注入真正可行
Moq 或 NSubstitute 只能模拟接口(或抽象类),不能 mock 具体类。如果你的构造函数参数是 SqlRepository,就无法在测试中替换成假实现;但改成 IRepository 后,测试可直接传入 mock 对象。同样,IServiceCollection.AddScoped 这类注册才成立——DI 容器靠接口类型解析实现,不是靠类名。
接口不是越多越好,滥用会导致“接口爆炸”
一个类暴露 5 个接口(IReadable、IWritable、ISearchable…)看似灵活,实际让调用方不断 as 或 is 判断,违背了单一职责。更常见的问题是:把本该是内部细节的方法(如 ResetConnection()、FlushCache())放进公共接口,导致所有实现都必须处理它,哪怕根本不适用。接口应只包含调用方真正需要的、稳定的行为契约。
public interface ICalculator
{
double Add(double a, double b);
double Multiply(double a, double b);
}
// 调用方只依赖这个契约,不关心是内存计算还是调远程 API
public class Service
{
private readonly ICalculator _calc;
public Service(ICalculator calc) => _calc = calc;
public double TotalPrice(double price, double taxRate) =>
_calc.Add(price, _calc.Multiply(price, taxRate));
}
接口真正的复杂点不在定义,而在识别“谁是调用方、谁是实现方、契约边界在哪”。很多团队过早抽象,结果接口刚写完就被三个不同业务需求撕成两半——这时候删掉接口,比硬撑着维护更诚实。








