当然,可以调用泄漏抽象定律,但这并不是特别有趣,因为它假定所有抽象都是泄漏的。可以为这种猜想辩护和反对,但是如果我们不对抽象的意思和漏水的意思有共同的理解,那将无济于事。因此,我首先尝试描述如何看待这些术语:
抽象
我最喜欢的抽象定义来自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了。
我计划还发布一系列博客文章,以配合此次演讲。这些文章已经写好并坐在我的文章队列中,等待发布,请继续关注。