难怪在任何编程语言中最被过度使用的概念之一是接口。毕竟,他们从远古时代就与我们同在。如今,将它们添加到每个班级的做法已经变得如此普遍,以至于几乎没有人质疑它。通常,引入接口的原因通常会导致错误的用法。
在本文中,我们将扩展可以将接口引入代码库的不同原因。您将看到在哪些情况下使用接口是可取的。但最重要的是,您会找到接口背后的实际原因。
事不宜迟,让我们开始吧
初级开发人员还没有学到的东西,高级开发人员已经忘记了 😁
我一直在问许多开发人员,他们为什么要在代码中引入接口,而作为回应,他们只会嘲笑我。
“你在开玩笑吗?它是一个接口。当然,我们需要一个!
当然🤓.您可能不会立即说出原因,但会不自觉地觉得界面很方便,应该添加。我敢肯定,如果你能使大脑紧张,你会自己找到几个原因。但我不希望你在辛苦了一天之后这样做,所以让我们遵循我的理由。
在代码中引入接口的最基本原因是抽象。
接口提供了一种从对象的实现细节中抽象出对象行为的方法。
interface IShape
{
double CalculateArea();
}
class Circle : IShape { . . . }
class Rectangle : IShape { . . . }
一旦你有了抽象,你就有了继承。继承意味着多态性。多态性导致 OOP。有了OOP,我们就有了模式。这就是乐趣开始😎的地方。
接口强制执行它们必须满足的类的协定。这利用了合同设计,而不是约定设计。
例如,在 ASP 中,我们可以按约定和契约定义中间件类。
使用基于约定的方法,您需要:
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;
public MyCustomMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext, ITextOutput output)
{
output.Write(DateTime.Now);
await _next(httpContext);
}
}
使用基于合约的方法,需要继承并实现接口:IMiddleware
public class MyCustomMiddleware : IMiddleware
{
private readonly ITextOutput _output;
public MyCustomMiddleware(ITextOutput output)
{
_output = output;
}
public async Task InvokeAsync(HttpContext httpContext, RequestDelegate next)
{
_output.Write(DateTime.Now);
await next(httpContext);
}
}
就是这么简单。没有带有魔术签名的方法,不需要记住或谷歌搜索。我想你可以告诉自己哪种方法不那么容易出错。
如果某个类需要替换或更新,只要它遵循相同的接口,依赖于它的其他代码部分就不会受到影响。
例如,下面我们可以轻松更改实现或回滚前一个实现。如果没有接口,将更难实现:
interface IUserRepository { . . . }
// used in POC
class InMemoryUserRepository : IUserRepository { . . . }
// used in production
class SqlUserRepository : IUserRepository { . . . }
// used when presenting to a customer 🙃
class MockUserRepository : IUserRepository { . . . }
接口在代码中引入了一个可扩展性点。
假设我们有一个存储库。拥有接口允许我们在不修改代码的情况下扩展代码。例如,我们可以用某种缓存来装饰它。
interface IUserRepository { . . . }
// responsible for user persistance
class SqlUserRepository : IUserRepository { . . . }
// responsible for a cache
// cache logic is encapsulated in a single place
class UserRepositoryCacheDecorator : IUserRepository
{
public IReadOnlyCollection<User> GetAll()
{
return _cache.GetOrAdd(key: "users", value: () =>
{
return _userRepository.GetAll();
});
}
public void DeleteInactive()
{
_cache.Invalidate(key: "users");
return _userRepository.DeleteInactive();
}
. . .
}
这也称为开闭原则。在不修改现有代码的情况下添加新代码时。
您现在可能已经意识到了这一点(因为我自己😅刚刚编造了它),但在 C# 中,有四种方法可以定义泛型:
我们只对最后两个感兴趣。
以同样的方式,泛型允许我们为不同类型的代码编写可重用的代码:
interface IRepository<TKey>
{
void DeleteById(TKey id);
. . .
}
类似的接口也可以实现:
interface IRepository
{
void DeleteById(IEntityKey id);
. . .
}
还是不方便?然后试着告诉我这两者之间的区别:
void WriteToFile<T>(IEnumerable<T> collection, string fileName)
where T : IShape
{
. . .
}
void WriteToFile(IEnumerable<IShape> collection, string fileName)
{
. . .
}
你可以有一个空的界面。这就是所谓的标记。它的目的只是将一个类“标记”为具有某种能力或特征。
interface IEntity { }
class User : IEntity
{
. . .
}
与属性类似,它们可用于在运行时对对象类型进行分类或标识,这些行为可以在以后通过反射进行扩展
接口允许我们以_执行_和_依赖流_指向不同方向的方式反转依赖关系。
这样,您的高级模块依赖于抽象(接口),而具体实现也依赖于这些抽象。这使您可以轻松地替换实现,而无需修改高级模块。
模块化是指将大型系统分解为更小、更易于管理的部分或模块的做法。
在界面中,您可以定义预期行为,而无需深入研究实现。
interface ICanFly { . . . }
interface ICanRide { . . . }
interface ICanWalk { . . . }
class Plane : ICanFly, ICanRide { . . . }
class Duck : ICanFly, ICanWalk { . . . }
// implementation details do not matter
// we only need to investigate ICanFly behaviour
ICanFly flyable = new ...()
分解有助于隔离代码库的不同部分,使其更易于理解、维护和更新。
通过设计带有接口的系统,您可以创建一个灵活的结构,以便更轻松地扩展和修改...🥱
好吧,好吧😒,我希望你没有放弃并关闭页面。
我可以用我在互联网上找到的其他花哨的词来继续列出。但是,我知道您点击了链接,而不是阅读另一篇带有明显内容的无聊文章。同样在这里。我也不想列出每篇文章都一样😏的,所以让我重新开始。
不管写了什么,没有多少开发人员能解释为什么左边比右边好:
尽管列出的内容在技术上是正确的,但这些仅适用于 OOP。大多数时候,我们在代码中的类只是贫血的服务或 POCO 类,它们更接近函数集和 DTO,而不是实际的领域类。
实践表明,开发人员经常为数据库或任何其他抽象引入接口,即使这些接口只有一个实现:
interface IUserRepository { . . . }
class UserRepository : IUserRepository { . . . }
当然,您正在抽象外部依赖关系。但是,与此同时,没有人能解释为什么没有 、 、 等的接口。Newtonsoft.JsonMediatRAutoMapper
具有单个实现的接口忽略了抽象的整个要点,并且不提供松耦合,只提供实现这些接口的具体类。接口经常泄露实现细节也就不足为奇了。简单地说,因为没有第二次实现,这将阻止我们这样做。
那么接口的意义何在呢?🤔
单元测试。
是的。无论听起来多么简单或愚蠢,接口的全部意义在于单元测试。没有这些,你就无法嘲笑。
var mock = new Mock<IUserService>();
mock
.Setup(x => x.GetAll())
.Returns(new List<UserDto>());
var userService = mock.Object;
如果可以在没有接口的情况下进行模拟,您会看到更少的接口。
我要说的是,你不需要为每个服务定义接口。但是,您可能仍然应该这样做,以使您的代码单元可立即测试。
当然,一些先进的技术,如依赖关系的反转,如果没有这些,根本不可能实现。但是,我只是希望你不要再对自己撒谎,说你在代码中有接口的实际原因。