你知道C#接口背后的实际原理吗?

作者:微信公众号:【架构师老卢】
5-4 8:42
41

概述:难怪在任何编程语言中最被过度使用的概念之一是接口。毕竟,他们从远古时代就与我们同在。如今,将它们添加到每个班级的做法已经变得如此普遍,以至于几乎没有人质疑它。通常,引入接口的原因通常会导致错误的用法。在本文中,我们将扩展可以将接口引入代码库的不同原因。您将看到在哪些情况下使用接口是可取的。但最重要的是,您会找到接口背后的实际原因。事不宜迟,让我们开始吧期望值初级开发人员还没有学到的东西,高级开发人员已经忘记了 😁我一直在问许多开发人员,他们为什么要在代码中引入接口,而作为回应,他们只会嘲笑我。“你在开玩笑吗?它是一个接口。当然,我们需要一个!当然🤓.您可能不会立即说出原因,但会不自觉地觉得

难怪在任何编程语言中最被过度使用的概念之一是接口。毕竟,他们从远古时代就与我们同在。如今,将它们添加到每个班级的做法已经变得如此普遍,以至于几乎没有人质疑它。通常,引入接口的原因通常会导致错误的用法。

在本文中,我们将扩展可以将接口引入代码库的不同原因。您将看到在哪些情况下使用接口是可取的。但最重要的是,您会找到接口背后的实际原因。

事不宜迟,让我们开始吧

期望值

初级开发人员还没有学到的东西,高级开发人员已经忘记了 😁

我一直在问许多开发人员,他们为什么要在代码中引入接口,而作为回应,他们只会嘲笑我。

“你在开玩笑吗?它是一个接口。当然,我们需要一个!

当然🤓.您可能不会立即说出原因,但会不自觉地觉得界面很方便,应该添加。我敢肯定,如果你能使大脑紧张,你会自己找到几个原因。但我不希望你在辛苦了一天之后这样做,所以让我们遵循我的理由。

抽象化

在代码中引入接口的最基本原因是抽象。

接口提供了一种从对象的实现细节中抽象出对象行为的方法。

interface IShape  
{  
    double CalculateArea();  
}  
  
class Circle : IShape { . . . }  
class Rectangle : IShape { . . . }

一旦你有了抽象,你就有了继承。继承意味着多态性。多态性导致 OOP。有了OOP,我们就有了模式。这就是乐趣开始😎的地方。

Contracts

接口强制执行它们必须满足的类的协定。这利用了合同设计,而不是约定设计。

例如,在 ASP 中,我们可以按约定和契约定义中间件类。

使用基于约定的方法,您需要:

  • 参数类型为RequestDelegate
  • 名为 or 的公共方法。此方法必须:
    - 返回 -
    接受类型的第一个参数InvokeInvokeAsyncTaskHttpContext
  • 可以将其他依赖项添加到方法中Invoke
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# 中,有四种方法可以定义泛型:

  • strings
  • objects
  • interfaces
  • generics

我们只对最后两个感兴趣。

以同样的方式,泛型允许我们为不同类型的代码编写可重用的代码:

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 ...() 

分解有助于隔离代码库的不同部分,使其更易于理解、维护和更新。

Scaling

通过设计带有接口的系统,您可以创建一个灵活的结构,以便更轻松地扩展和修改...🥱

好吧,好吧😒,我希望你没有放弃并关闭页面。

我可以用我在互联网上找到的其他花哨的词来继续列出。但是,我知道您点击了链接,而不是阅读另一篇带有明显内容的无聊文章。同样在这里。我也不想列出每篇文章都一样😏的,所以让我重新开始。

Reality

不管写了什么,没有多少开发人员能解释为什么左边比右边好:

尽管列出的内容在技术上是正确的,但这些仅适用于 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;

如果可以在没有接口的情况下进行模拟,您会看到更少的接口。

我要说的是,你不需要为每个服务定义接口。但是,您可能仍然应该这样做,以使您的代码单元可立即测试。

当然,一些先进的技术,如依赖关系的反转,如果没有这些,根本不可能实现。但是,我只是希望你不要再对自己撒谎,说你在代码中有接口的实际原因。

相关留言评论
昵称:
邮箱:
阅读排行