将接口隔离原则应用于服务层接口

Applying Interface Segregation Principle to service layer interface

以书为例,假设我有一个书资源如下API(还有update和delete等,为了简单这里只显示两个)

GET /book/{id}
POST /book

这些 API 中的每一个都会调用其他 API 来获取结果,而不是典型的 CRUD 数据库操作。并且根据要求/现有框架约束,有两个单独的控制器class GetBookControllerCreateBookController。控制器用于处理请求和响应。所以实际的业务逻辑和检索/创建书籍都在服务层。

那么问题来了,书本操作的每一个(GetBookServiceCreateBookService)是应该有一个单独的接口,还是只有一个(BookService)?

基于接口隔离原则,该原则规定“客户不应被迫依赖于他们不使用的接口”。这里的GetBookControllerclass是客户端,只需要查询book,不需要创建,所以只需要GetBookService。如果是用BookService,就没有用createBook的方法,好像有违ISP原则。但是,如果使用单独的接口,将导致创建许多接口和实现 classes。我是不是理解错了ISP原理?

@Controller
public class GetBookController {
    
    @Autowired
    private GetBookService getBookService;
}

@Controller
public class CreateBookController {
    
    @Autowired
    private CreateBookService createBookService;
}

public interface GetBookService {
    Book getBook(long id);
}

public interface CreateBookService {
    Boolean createBook(Book request);
}

public interface BookService {
    Book getBook(long id);
    Boolean createBook(Book request);
}

所以,我通常为像书这样的 CRUD 实体做的是单个 BookControler。您不需要多个书籍控制器,因为它是一个控制器 - 它控制与书籍相关的事物。所以它应该是一个 class.

同样的服务逻辑。一本书的服务接口就足够了。它处理书籍的需求和 returns 请求的结果。

所以,是的,如果每个方法都做一个接口,那么接口太多了,随着项目规模的扩大,不好处理。此外,它使每个接口做的事情很少(在某些情况下是需要的,但不是这个)。

一个ISP原理的类比是:你不想做咖啡机需要实现如何打印一张纸,而打印机需要实现如何煮咖啡。但是制作不同咖啡的咖啡机也在其范围内。

 @Controller
 public class BookController {
     private final BookService bookService;

     @Autowired
      private BookController (BookService bookService) {
          this.bookService = bookService;
      }
  }
    
  public interface BookService {

       Book getBook(long id);
    
       Boolean createBook(Book request);
  }

例如,您可以在构造函数中使用自动装配,以便于模拟。

ISP 的原则目标是通过将软件拆分为多个独立的部分来减少副作用和所需更改的频率。

但问题是拆分软件组件需要多小。

在你的情况下,你过度应用了原则,例如假设你有 class :

class Student {
String name;
Integer age;
ClassRoom classRoom;

.
.
.
setters ....
getters ....
}

一位开发者认为最好将它分开,所以他最终为每个 setter 和 getter 制作了单独的 classes(这显然是不必要的)

与您的情况相同,最好有一个 BookController 和一个涵盖所有必需操作(创建、获取、更新、删除)的 BookService。

除非您有不同类型的 BookService(例如 ElectronicBook、PaperBook),否则您可以有不同的 BookService 接口实现。

您在设计 BookService 接口时不应该好像您知道它可能拥有的所有客户端一样。通常应该只有一个BookService接口,目的是提供对图书的增删改查

此规则的一个例外是图书服务支持具有不同访问级别的不同客户端角色。如果是这种情况,您可能有一个仅提供 R 的 BookReaderService,以及一个扩展它并提供完整 CRUD 的 BookUpdaterService

现在,您可能想要编写 客户端 以便它们不依赖于它们不需要的操作 (ISP)。在这种情况下,您将在客户端定义客户想要的接口,然后使用适配器根据适当的图书服务提供这些接口……但在这种情况下,这确实有点过分了。您的控制器 已经 适配器,因此添加另一个适配器层是多余的。

it would result in many interface and implementation classes being created

是的,你是对的

The question then is, should there be a separate interface for each of book operation(GetBookService and CreateBookService), or to have just only one (BookService)?

ISP 是如何使用接口的。所以,是否需要使用接口的其他方法,取决于你需要的。

使用 ISP 的 HDD 示例:

public interface IReadable
{
    string Read();
}

public interface IWriteable
{
    void Write();
}

public class HDD : IReadable, IWriteable
{
    public string Read() { }

    public void Write()  { }
}

通过为 Read()Write() 方法创建一个接口,class 将有义务在 class 中实现 两个 方法].但是有些 classes 只想读取数据,其他人想写入数据,有些则两者兼而有之。例如。 card reader 可能想要读取数据。所以在这种情况下最好创建单独的接口。

那么让我们看另一个 CardReader 的例子。 CardReader只读数据,不写数据。所以,如果我们继承一个接口,方法是Read()Write(),那么我们就违反了ISP原则。违反ISP的例子:

public interface IWriteReadable
{
    string Read();
    void Write();
}

public class CardReader : IWriteReadable
{
    // this is a necessary method
    public string Read() { }

    // here we are obligating to implement unnecessary method of interface
    public void Write() { }
}

因此,通过应用 ISP,您只需将客户端所需的方法放在接口中 class。如果你的class/client只是想读取数据,那么你需要使用IReadable接口,而不是IReadableWriteable

在我看来,就像 建议的那样,最好为书本创建一个控制器。如果这些方法有很多变体并且控制器变得非常大,那么为读取和创建操作设置单独的控制器可能没问题。