【问题标题】:Define abstractions for proper constructor injection and ISP (of SOLID)为正确的构造函数注入和 ISP(SOLID)定义抽象
【发布时间】:2013-03-14 12:26:48
【问题描述】:

假设出于不同的原因我想对集合进行抽象操作:

现在为了简单起见,让我们对一个集合进行推理

class Book {
  public string Title { get; set; };
  public string SubTitle { get; set; }
  public bool IsSold { get; set; }
  public DateTime SoldDate { get; set; }
  public int Volums { get; set; }
}

我的类型只需要搜索Book::Title(区分大小写或不区分大小写),因此我可以定义我的抽象:

interface ITitleSearcher {
  bool ContainsTitle(string title);
}

然后实施

class CaseSensitiveTitleSearcher : ITitleSearcher { ... }
class NoCaseSensitiveTitleSearcher : ITitleSearcher { ... }

并将其用作

class TitleSearcherConsumer  {
  public TitleSearcherConsumer(ITitleSearcher searcher) { // <- ctor injection
  }
}

直到这里我都清楚了,据我所知,Interface Segregation Principle 也被遵守了。

继续开发我必须满足其他要求,所以我定义然后实现其他接口,例如ITitleSearcher,例如:

class CaseSensitiveSubTitleSearcher : ISubTitleSearcher { ... }
class SoldWithDateRangeSearcher : ISoldDateRangeSearcher { ... }

为了不违反 DRY(不要重复自己),我可以围绕 IEnumerable&lt;Book&gt; 创建一个包装器:

class BookCollection : ITitleSearcher, ISubTitleSearcher, ISoldDateRangeSearcher
{
  private readonly IEnumerable<Book> books;

  public BookCollection(IEnumerable<Book> books)
  {
    this.books = books;
  }
  //...
}

现在,如果我有像 TitleSearcherConsumer 这样的消费者,我可以毫无问题地传递 BookCollection 的实例。

但如果我有这样的消费者:

class TitleAndSoldSearcherConsumer {
  public TitleAndSoldSearcherConsumer(ITitleSearcher src1, ISoldDateRangeSearcher src2) {

  }
}

我无法将BookCollection 实例注入TitleAndSoldSearcherConsumer ctor;我必须传递每个接口的实现。

是的,我可以用其他接口的所有方法定义一个IBookCollection,并在所有消费者中使用它,但这样做不违反ISP?

我可以同时靠近 ISP/SOLID 和 DRY 吗?

【问题讨论】:

标签: c# design-patterns dependency-injection abstraction


【解决方案1】:

是的,我可以用其他的所有方法定义一个 IBookCollection 接口并在所有消费者中使用它,但这样做并不违反 互联网服务供应商?

您不会违反 ISP,但您的藏书将开始承担太多责任,您将违反单一责任原则。

让我担心的另一件事是ITitleSearcher 接口的多个实现。我不确定这里是否违反了某些设计原则,但您的设计中似乎有些含糊不清,您可能应该看看。此外,对于每个搜索操作,您都在创建一个新的抽象。您已经拥有ITitleSearcherISubTitleSearcherISoldDateRangeSearcher,并且可能还会添加数十个。我认为您在这里缺少的是对系统中查询的一般抽象。所以这是你可以做的:

为查询参数定义一个抽象:

public interface IQuery<TResult> { }

这是一个没有成员的接口,只有一个泛型类型TResultTResult 描述了该查询的返回类型。例如,您可以如下定义查询:

public class SearchBooksByTitleCaseInsensitiveQuery : IQuery<Book[]>
{
    public string Title;
}

这是接受Title 并返回Book[] 的查询的定义。

您还需要对知道如何处理特定查询的类进行抽象:

public interface IQueryHandler<TQuery, TResult>
    where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

看看该方法如何接受TQuery 并返回TResult?实现可能如下所示:

public class SearchBooksByTitleCaseInsensitiveQueryHandler :
    IQueryHandler<SearchBooksByTitleCaseInsensitiveQuery, Book[]>
{
    private readonly IRepository<Book> bookRepository;

    public SearchBooksByTitleCaseInsensitiveQueryHandler(
        IRepository<Book> bookRepository) {
        this.bookRepository = bookRepository;
    }

    public Book[] Handle(SearchBooksByTitleCaseInsensitiveQuery query) {
        return (
            from book in this.bookRepository.GetAll()
            where book.Title.StartsWith(query.Title)
            select book)
            .ToArray();
     }
}

现在消费者可以像这样依赖特定的IQueryHandler&lt;TQuery, TResult&gt; 实现:

class TitleSearcherConsumer  {
    IQueryHandler<SearchBooksByTitleCaseInsensitiveQuery, Book[]> query;
    public TitleSearcherConsumer(
      IQueryHandler<SearchBooksByTitleCaseInsensitiveQuery, Book[]> query) {
    }

    public void SomeOperation() {
        this.query.Handle(new SearchBooksByTitleCaseInsensitiveQuery
        {
            Title = "Dependency Injection in .NET"
        });
    }
}

这究竟给我带来了什么?

  • 通过定义IQueryHandler&lt;TQuery, TResult&gt; 查询,我们定义了系统中非常常见的模式(查询)的一般抽象。
  • IQueryHandler&lt;TQuery, TResult&gt; 定义一个成员并遵守 ISP。
  • IQueryHandler&lt;TQuery, TResult&gt; 实现实现单个查询并遵守 SRP。
  • IQuery&lt;TResult&gt; 接口允许我们对查询及其结果进行编译时支持。消费者不能错误地依赖返回类型不正确的处理程序,因为它不会编译。
  • 常见的IQueryHandler&lt;TQuery, TResult&gt; 抽象允许我们将各种横切关注点应用于查询处理程序,而无需更改任何实现。

尤其是最后一点很重要。诸如验证、授权、日志记录、审计跟踪、监控和缓存等横切关注点都可以使用装饰器轻松实现,而无需更改处理程序实现和消费者。看看这个:

public class ValidationQueryHandlerDecorator<TQuery, TResult>
    : IQueryHandler<TQuery, TResult>
    where TQuery : IQuery<TResult>
{
    private readonly IServiceProvider provider;
    private readonly IQueryHandler<TQuery, TResult> decorated;

    public ValidationQueryHandlerDecorator(
        Container container,
        IQueryHandler<TQuery, TResult> decorated)
    {
        this.provider = container;
        this.decorated = decorated;
    }

    public TResult Handle(TQuery query)
    {
        var validationContext =
            new ValidationContext(query, this.provider, null);

        Validator.ValidateObject(query, validationContext);

        return this.decorated.Handle(query);
    }
}

这是一个装饰器,可以在运行时围绕所有命令处理程序实现进行包装,从而增加对其进行验证的能力。

有关更多背景信息,请查看这篇文章:Meanwhile... on the query side of my architecture

【讨论】:

  • +1,回答,@Steven。一如既往的圆满。我会读你的文章(已经读过你博客中的各种文章)。你给了我很多材料去调查。曝光极佳!特别感谢泛型对定义抽象的帮助。
【解决方案2】:

你的界面太具体了。 有一个谓词

interface ISearcher {
  bool IsAMatch(Book book);
}

并从中获得您的搜索者。 另外,不要将搜索功能放入您的收藏中 - 收藏用于存储和迭代。也许我只是描述了访问者模式。

【讨论】:

  • +1 @durilka,很好。现在保留TitleAndSoldSearcherConsumer这种类型的样本,你将如何通过ISearcher的两个不同的concrete?样本中的两个参数?还是一个 IEnumerable (并用 linq OfType&lt;T&gt; 区分它)?
  • 如果您知道总会有两个而且只有两个 - 将它们作为命名参数保留(很高兴知道您正在使用什么)。否则,一切都以IEnumerable 结尾,形式为params 或数组。
猜你喜欢
  • 2019-04-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-20
相关资源
最近更新 更多