暴露异步功能的接口是泄漏抽象吗?


13

我正在阅读《依赖注入原理,实践和模式》一书,并了解了泄漏抽象的概念,该概念在本书中有很好的描述。

这些天来,我正在使用依赖注入重构C#代码库,以便使用异步调用而不是阻塞调用。这样做时,我正在考虑一些接口,这些接口在我的代码库中表示抽象,并且需要重新设计以便可以使用异步调用。

例如,考虑以下接口,它代表应用程序用户的存储库:

public interface IUserRepository 
{
  Task<IEnumerable<User>> GetAllAsync();
}

根据书中的定义,泄漏抽象是设计时考虑到特定实现的抽象,因此某些实现详细说明了抽象本身的“泄漏”。

我的问题如下:我们可以考虑以异步方式设计的接口(例如IUserRepository)作为泄漏抽象的示例吗?

当然,并非所有可能的实现都与异步有关:只有进程外实现(例如SQL实现)才需要同步,但是内存中存储库不需要异步(实际上实现接口的内存中版本可能更多)如果接口公开异步方法很困难,例如,您可能必须在方法实现中返回类似Task.CompletedTask或Task.FromResult(users)之类的东西。

您如何看待?


@Neil我可能明白了。暴露返回Task或Task <T>的方法的接口本身并不是一个泄漏的抽象,只是一个带有涉及任务的签名的协定。返回Task或Task <T>的方法并不意味着具有异步实现(例如,如果我通过使用Task.CompletedTask创建完成的任务,则没有进行异步实现)。反之亦然,C#中的异步实现要求异步方法的返回类型必须为Task或Task <T>类型。换句话说,我界面的唯一“泄漏”方面是名称的异步后缀
Enrico Massone

@Neil实际上有一个命名准则,指出所有异步方法的名称都应以“异步”结尾。但这并不意味着返回Task或Task <T>的方法必须使用Async后缀命名,因为它可以通过不使用异步调用来实现。
Enrico Massone

6
我认为方法的“异步性”由返回a的事实表示Task。给async方法加上async一词的准则是区分其他相同的API调用(C#无法基于返回类型进行分派)。在我们公司,我们将其放在一起。
richzilla

有许多答案和评论解释了为什么该方法的异步特性是抽象的一部分。一个更有趣的问题是,语言或编程API如何将方法的功能与方法的执行方式分离开,以至不再需要Task返回值或异步标记?函数式编程人员似乎已经更好地解决了这一问题。考虑如何用F#和其他语言定义异步方法。
Frank Hileman '19

2
:-)->“功能编程人”哈。异步不比同步泄漏更多,它看起来就是这样,因为默认情况下我们习惯于编写同步代码。如果默认情况下我们都对异步进行编码,那么同步函数似乎会泄漏。
StarTrekRedneck

Answers:


8

当然,可以调用泄漏抽象定律,但这并不是特别有趣,因为它假定所有抽象都是泄漏的。可以为这种猜想辩护和反对,但是如果我们不对抽象的意思和漏水的意思有共同的理解,那将无济于事。因此,我首先尝试描述如何看待这些术语:

抽象

我最喜欢的抽象定义来自Robert C. Martin的APPP

“抽象是本质的放大和无关紧要的消除。”

因此,接口本身并不是抽象。仅当它们将重要内容浮出水面并隐藏其余内容时,它们才是抽象。

漏水的

依赖注入原理,模式和实践一书在依赖注入(DI)的上下文中定义了泄漏抽象一词。在这种情况下,多态和SOLID原则起着重要作用。

依赖关系倒置原则(DIP)中,遵循以下原则,再次引用APPP,该原则是:

“客户拥有抽象接口”

这意味着客户端(调用代码)定义了他们所需的抽象,然后您去实现该抽象。

一个漏水的抽象,在我看来,是违反通过某种方式包括一些功能,客户端不与DIP抽象需要

同步依赖

实现一条业务逻辑的客户端通常将使用DI将自身与某些实现细节(例如,数据库)分离。

考虑一个处理餐厅预订请求的域对象:

public class MaîtreD : IMaîtreD
{
    public MaîtreD(int capacity, IReservationsRepository repository)
    {
        Capacity = capacity;
        Repository = repository;
    }

    public int Capacity { get; }
    public IReservationsRepository Repository { get; }

    public int? TryAccept(Reservation reservation)
    {
        var reservations = Repository.ReadReservations(reservation.Date);
        int reservedSeats = reservations.Sum(r => r.Quantity);

        if (Capacity < reservedSeats + reservation.Quantity)
            return null;

        reservation.IsAccepted = true;
        return Repository.Create(reservation);
    }
}

在这里,IReservationsRepository依赖关系完全由客户端(MaîtreD类)确定:

public interface IReservationsRepository
{
    Reservation[] ReadReservations(DateTimeOffset date);
    int Create(Reservation reservation);
}

这个接口是完全同步的,因为MaîtreD不需要它是异步的。

异步依赖

您可以轻松地将接口更改为异步:

public interface IReservationsRepository
{
    Task<Reservation[]> ReadReservations(DateTimeOffset date);
    Task<int> Create(Reservation reservation);
}

MaîtreD级,然而,这并不需要这些方法是异步的,所以现在的DIP被违反。我认为这是一个泄漏的抽象,因为实现细节会迫使客户端进行更改。TryAccept现在,该方法还必须变得异步:

public async Task<int?> TryAccept(Reservation reservation)
{
    var reservations =
        await Repository.ReadReservations(reservation.Date);
    int reservedSeats = reservations.Sum(r => r.Quantity);

    if (Capacity < reservedSeats + reservation.Quantity)
        return null;

    reservation.IsAccepted = true;
    return await Repository.Create(reservation);
}

域逻辑没有异步的内在理由,但是为了支持实现的异步,现在需要这样做。

更好的选择

在NDC Sydney 2018上,我就这个话题做了演讲。在其中,我还概述了一个不会泄漏的替代方法。我也将在2019年的几场会议上发表演讲,但现在更名为Async injection了

我计划还发布一系列博客文章,以配合此次演讲。这些文章已经写好并坐在我的文章队列中,等待发布,请继续关注。


我认为这是一个意图问题。如果我的抽象看起来像应该以一种方式运行,但是某些细节或约束破坏了所呈现的抽象,那是一个泄漏的抽象。但是在这种情况下,我明确地向您介绍该操作是异步的-这不是我要抽象的内容。我的想法与您的示例(无论是否明智)试图抽象出存在SQL数据库并且仍然暴露连接字符串的事实在我的脑海中截然不同。也许这是语义/观点的问题。
蚂蚁P

因此,我们可以说抽象绝不是“本身”泄漏的,相反,如果一个特定实现的某些细节从暴露的成员中泄漏出来并约束消费者更改其实现,以满足抽象的形状,那么它就是一个泄漏。 。
Enrico Massone

2
有趣的是,您在解释中强调的观点是整个依赖注入故事中最容易被误解的观点之一。有时,开发人员忘记了依赖关系反转原理,而是先尝试设计抽象,然后改编用户设计以应对抽象本身。相反,该过程应以相反的顺序进行。
Enrico Massone

11

根本不是泄漏的抽象。

异步是对函数定义的根本更改-这意味着调用返回时任务尚未完成,但是这也意味着您的程序流将几乎立即继续,而不会出现长时间的延迟。执行相同任务的异步和同步功能本质上是不同的功能。异步不是实现细节。它是函数定义的一部分。

如果该函数公开了如何使该函数异步化,那将是泄漏的。您(不必/不必)关心它的实现方式。


5

async方法的属性是一个标签,指示需要特别的护理和处理。因此,它需要泄漏到世界各地。异步操作很难正确组合,因此让API用户注意是很重要的。

相反,如果您的库正确地管理了其内部的所有异步活动,那么您可以承受得起,不要让asyncAPI泄漏。

软件难度分为四个维度:数据,控制,空间和时间。异步操作跨越所有四个维度,因此需要最注意。


我同意您的观点,但“泄漏”表示不好,这是“泄漏抽象”一词的意图-抽象中不希望有的东西。在异步与同步的情况下,没有泄漏。
StarTrekRedneck

2

泄漏抽象是设计时考虑到特定实现的抽象,因此某些实现详细说明了抽象本身的“泄漏”。

不完全的。抽象是一种概念性事物,它忽略了更复杂的具体事物或问题的某些元素(使事物/问题更简单,易于处理或由于其他好处)。因此,它必然与实际的事物/问题有所不同,因此在某些情况下它将是泄漏的(即,所有抽象都是泄漏的,唯一的问题是在多大程度上-意味着,在哪种情况下是抽象对我们有用,它的适用范围是什么)。

就是说,当涉及到软件抽象时,有时(或也许足够多?)我们选择忽略的细节实际上不能被忽略,因为它们会影响对我们很重要的软件某些方面(性能,可维护性……)。 。因此,泄漏抽象是一种旨在忽略某些细节的抽象(假设这样做是可能且有用的),但随后发现其中一些细节在实践中很重要(它们不能被忽略,因此它们“泄漏”)。

因此,一个暴露实现细节的接口本身并不是泄漏的(或者,一个接口,孤立地看,它本身并不是泄漏的抽象)。相反,泄漏性取决于实现该接口的代码(它是否能够实际支持该接口表示的抽象),还取决于客户端代码所做出的假设(等同于一个概念上的抽象,它补充了由接口,但本身不能用代码表示(例如,语言的功能表达能力不足,因此我们可以在文档中对其进行描述等)。


2

请考虑以下示例:

这是在返回之前设置名称的方法:

public void SetName(string name)
{
    _dataLayer.SetName(name);
}

这是设置名称的方法。在返回的任务完成之前,调用者无法假定已设置名称(IsCompleted= true):

public Task SetName(string name)
{
    return _dataLayer.SetNameAsync(name);
}

这是设置名称的方法。在返回的任务完成之前,调用者无法假定已设置名称(IsCompleted= true):

public async Task SetName(string name)
{
    await _dataLayer.SetNameAsync(name);
}

问:哪个不属于其他两个?

答:异步方法并不是一个独立的方法。一个独立的方法是返回void的方法。

对我来说,这里的“泄漏”不是async关键字。这是该方法返回Task的事实。那不是泄漏;它是原型的一部分,也是抽象的一部分。返回任务的异步方法做出与返回任务的同步方法完全相同的承诺。

因此,不,我认为引入async形式本身并不会构成泄漏抽象。但是您可能必须更改原型以返回Task,该Task通过更改接口(抽象)来“泄漏”。而且由于它是抽象的一部分,从定义上来说,它不是泄漏。


0

当且仅当您打算让所有实现的类都创建异步调用时,这才是泄漏的抽象。您可以创建多个实现,例如,为您所支持的每种数据库类型创建一个实现,并且假设您不需要知道整个程序中使用的确切实现,那就完全可以了。

虽然您不能严格执行异步实现,但名称暗示它应该是。如果情况发生变化,并且由于某种原因可能是同步调用,那么您很可能需要考虑更改名称,因此,我的建议是仅在您认为这不太可能发生时才这样做。未来。


0

这是一个相反的观点。

我们并没有从返回Foo返回,Task<Foo>因为我们开始想要Task而不是Foo。当然,有时我们会与进行交互,Task但在大多数实际代码中,我们会忽略它而仅使用Foo

而且,即使实现可能是异步的也可能不是异步的,我们经常定义接口来支持异步行为。

实际上,返回a的接口Task<Foo>告诉您实现是否可能是异步的,尽管您可能在乎或不在乎。如果抽象告诉我们的信息超过我们所需了解的实现信息,那么它就是泄漏的。

如果我们的实现不是异步的,则将其更改为异步,然后我们必须更改抽象以及使用该抽象的所有内容,这是一个非常泄漏的抽象。

那不是判断。正如其他人指出的那样,所有抽象都是泄漏的。这确实产生了更大的影响,因为它需要在整个代码中产生异步/唤醒的连锁反应,只是因为在其结尾的某个地方可能实际上存在一些异步的东西。

听起来像是在抱怨吗?这不是我的意图,但我认为这是一个准确的观察。

一个相关的观点是“接口不是抽象”的主张。马克·西曼(Mark Seeman)简洁地说了些话。

即使在.NET中,“抽象”的定义也不是“接口”。抽象可以采用许多其他形式。接口可能是一个较差的抽象,或者可能非常紧密地反映其实现,从某种意义上说,它根本不是一个抽象。

但是我们绝对使用接口来创建抽象。因此抛出“接口不是抽象”是因为一个问题提到接口,而抽象却没有启发性。


-2

GetAllAsync()实际异步?我的意思是确保名称中包含“异步”,但是可以将其删除。所以我再问一遍...是否不可能实现一个返回a Task<IEnumerable<User>>并同步解决的功能?

我不知道.Net Task类型的细节,但是如果无法同步实现该功能,则请确保它是一个泄漏抽象(以这种方式),否则就不是。我确实知道,如果它IObservable不是一个Task,而是可以同步或异步实现的,所以该函数之外的任何信息都不知道,因此不会泄漏该特定事实。


Task<T> 表示异步。您会立即获得任务对象,但可能必须等待用户的顺序
Caleth,

可能需要等待并不意味着它一定是异步的。等待就意味着异步。大概,如果基础任务已经运行,则不必等待。
Daniel T.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.