【问题标题】:Reused abstraction principle in C#C#中的重用抽象原则
【发布时间】:2015-07-16 16:26:18
【问题描述】:

在我们的 C# MVC 应用程序中,我们有许多将 1 到 1 映射到实现它们的对象的接口。即:基本上,对于创建的每个对象,都执行了“提取接口”操作。

Moq 使用这些接口为我们的单元测试生成模拟对象。但这是唯一一次重复使用接口。

我们的系统中没有具体的对象实现多个接口。

谁能告诉我这是否会在未来造成问题?如果是这样,它们会是什么?

我在想,我们的应用程序有很多重复,例如在这 2 个接口中(编辑:在我们的 SERVICES 层中)唯一不同的是方法名称和它们采用的参数类型,但是从语义上讲,他们对发送消息的存储库做同样的事情:

interface ICustomer
{
    void AddCustomer(Customer toAdd);
    void UpdateCustomer(Customer toUpdate);
    Customer GetById(int customerId);
}

interface IEmployee
{
    void AddEmployee(Employee toBeAdded);
    void UpdateEmployee(Employee toUpdate);
    Employee GetById(int employeeId);       
}

这就是我认为重用抽象原则的用武之地,即将代码转换为:

public interface IEmployee: IAdd<Employee>, IUpdate<Employee>, IFinder<Employee>

这与存储库模式无关 - 这是关于接口在任何层,它们看起来共享语义相同的行为。是否值得为这些操作派生通用接口并让“子接口”继承它们?

至少它会保持方法的签名一致。但这会给我带来什么其他好处呢? (里氏替换原则除外)

现在,方法的名称和返回类型到处都是。

我阅读了 Mark Seemann 关于重用抽象原则的博客,但坦率地说,我不明白。也许我只是愚蠢:) 我还阅读了 Fowler 对 Header Interfaces 的定义。

【问题讨论】:

  • 听起来您需要一个通用存储库。
  • 我必须自己查找重用的抽象,但您的建议看起来像接口隔离原则,它是 SOLID 代码的一部分。这不是一件坏事:)
  • 我提到的这些方法实际上是服务(业务)层的一部分。我们没有使用 DDD。在我们的实现中,MVC 控制器使用“服务”(按照 EmployeeServices、CustomerServices 命名的对象),它们实现了与上述类似的接口。他们在另一层“使用”存储库,在实体框架中实现,具有非常相似的接口。
  • FWIW,我的博客不是了解 RAP 的合适来源 - Jason Gorman 是 original source of the principle
  • 等等,这是一个看起来很疯狂的界面。接口不应引用内部的具体类。 ICustomer AddCustomer 应该采用 ICustomer,而不是 Customer。你也想要那里的一些属性

标签: c# oop abstraction solid-principles


【解决方案1】:

所有这些都可以使用Repository pattern 联合起来......

public interface IRepository<TEntity> where TEntity : IEntity
{
    T FindById(string Id);
    T Create(T t);
    bool Update(T t);
    bool Delete(T t);
}

public interface IEntity
{
    string Id { get; set; }
} 

编辑

我们的系统中没有具体的对象实现多个接口。

谁能告诉我这是否会在未来造成问题? 如果是这样,它们会是什么?

是的,如果它还没有开始这样做会导致问题。

您最终会得到一堆界面,这些界面不会为您的解决方案添加任何内容,并且会占用您大部分时间来维护和创建它们。随着代码库规模的增加,您会发现并非所有内容都像您曾经想象的那样具有凝聚力

请记住,接口只是一种工具,一种实现某种抽象级别的工具。而抽象是多个独立实体共享的概念、模式和原型。

你已经总结了,

这不是关于存储库模式 - 这是关于任何层中的接口,它们看起来共享语义相同的行为。是否值得为这些操作派生通用接口并让“子接口”继承它们?

这不是关于interfaces,这是关于abstractionsRepository pattern 演示了如何抽象出为特定对象量身定制的行为。

我上面给出的示例没有任何名为AddEmployeeUpdateEmployee 的方法...这些方法只是浅接口,而不是抽象。

Repository pattern 的概念显而易见,因为它定义了一组行为,这些行为由许多不同的类实现,每个类都针对特定实体量身定制。

考虑到为每个实体(UserRepository、BlogRepository 等)实现了一个存储库,并且考虑到每个存储库必须支持一组核心功能(基本 CRUD 操作),我们可以采用该核心功能集并将其定义在一个接口,然后在每个 Repository 中实现该接口。

现在我们可以将我们从存储库模式中学到的知识应用到应用程序的其他部分,由此我们定义一组核心行为,这些行为由新接口中的多个对象共享,然后从那个接口派生。

public interface IVehicleOperator<TVehicle> where TVehicle : IVehicle
{
    void Accelerate();
    void Brake();
}

这样做我们不再有 1:1 的映射,而是一个实际的抽象。

虽然我们正在讨论这个主题,但可能也值得回顾一下decorator pattern

【讨论】:

  • @Scott 如果这回答了你的问题,请告诉我
  • 很好的答案,但我不明白为什么第二个示例是存储库模式。它只是看起来像一个使用泛型的漂亮接口。
  • 你说得对,它不是,我想展示的是我们可以从现有的设计模式中获取想法和概念,并在其他地方也可以使用它们,它们不必局限于特定的情况,当您查看IVehicleOperatorIRepository 时,实际上不同的是名称,其余代码是抽象...
  • Aydin,感谢您的帮助,但我认为您的第二个示例是存储库模式是为了抽象出数据存储的概念? SO 回复说“它允许您的所有代码使用对象而不必知道对象是如何持久化的”(stackoverflow.com/questions/11985736/…)-您的 IVehicleOperator 与持久性有什么关系?恐怕我很困惑。
  • 100% 正确,这是存储库模式的最终目的,即抽象出数据层。我试图指出一些完全不同的东西。每个实体都实现了一个存储库,每个存储库都可以从 IRepository 派生,因此我们没有为每个存储库 (1:1) 提供一个接口,而是有 1 个接口 (IRepository),然后由许多人实现。我们可以将这个概念延续到您的应用程序的任何部分
【解决方案2】:

鉴于此:

interface ICustomer{
    void AddCustomer(Customer toAdd);
    void UpdateCustomer(Customer toUpdate);
    Customer GetById(int customerId);
}

interface IEmployee
{
    void AddEmployee(Employee toBeAdded);
    void UpdateEmployee(Employee toUpdate);
    Employee GetById(int employeeId);       
}

我可能会像这样重新设计它:

interface IRepository<T>
{
    void Add(T toAdd);
    void Update(T toUpdate);
    T GetById(int id);
}

但是,这很可能仍然违反了Interface Segregation Principle,更不用说因为它也违反了Command-Query Responsibility Segregation 模式(不是架构),所以也不能这样做协或逆变。

因此,我的下一步可能是将这些拆分为Role Interfaces

interface IAdder<T>
{
    void Add(T toAdd);
}

interface IUpdater<T>
{
    void Update(T toAdd);
}

interface IReader<T>
{
    T GetById(int id);
}

此外,您可能会注意到 IAdder&lt;T&gt;IUpdater&lt;T&gt; 在结构上是相同的(它们只是在语义上不同),所以为什么不将它们合二为一:

interface ICommand<T>
{
    void Execute(T item);
}

为了保持一致,您也可以重命名 IReader&lt;T&gt;

interface IQuery<T>
{
    T GetById(int id);
}

本质上,您可以将一切简化为这两个接口,但对于某些人来说,这可能过于抽象,并且承载的语义信息太少。

但是,我认为不可能提供更好的答案,因为前提是有缺陷的。最初的问题是界面应该如何设计,但客户端却无处可见。作为APPP ch。 11 告诉我们,“客户 [...] 拥有抽象接口” - 换句话说,客户根据需要定义接口。不应从具体类中提取接口。

关于这个主题的进一步学习材料:

【讨论】:

  • 嗨,马克,信不信由你,我看过你的 Pluralsight 课程!这就是让我首先寻找 RAP 的原因,我们的界面“闻起来”。我查看了您的博客,但不明白为什么需要额外的努力。我需要向团队证明返工的合理性。
  • RAP 主要是对代码库的理想质量的描述。缺乏重用抽象通常是代码库违反LSP症状。这不是必然不好。 SOLID 原则的存在是为了解决代码的某些问题(主要是可维护性)。如果您没有这些问题,就没有理由尝试应用“解决方案”。
猜你喜欢
  • 1970-01-01
  • 2018-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多