REST-API 模型的最佳实践

Best practices for REST-API models

我正在研究 REST-API,运行 遇到架构问题。

模型 'Book' 表示具有属性和基于 CRUD 的函数的单本书。它通过读取函数从数据库加载自身。

但是,如果我想获取数据库中的所有书籍怎么办?当前的书籍模型不涵盖此用例。

我尝试了几种方法:

1.) 第二个模型叫做 'Books'。它有一个读取函数,其中 returns 一个图书对象列表。

2.) 模型'Book'本身有一个读取所有书籍的函数。

3.) 模型 'Book' 没有功能,它只有属性。相反,'BookStorage' class 加载数据并填充 one/multiple 模型。

我对这些方法都不满意。这种情况是否有最佳实践?

1.) A second model called 'Books'. It has a read function which returns a list of book objects.

这没关系,但项目的用户和未来的开发人员可能不清楚您同时拥有 Book 和 Books 的事实。所以一定要记录 API,并对代码进行注释。此外,您确实需要考虑输入过滤器来限制结果,或者至少需要一种对结果进行分页的方法。 Google曾经估计有1.3亿本书,你想一次搞定吗?

例如SERVER/Books/?skipRecords=0&limit=100

2.) The model 'Book' itself has a readAll function which loads all books.

这并不理想,因为它违反了单一责任原则。大多数情况下,在面向对象的情况下,让单个书籍实体能够列出它的所有兄弟实体并没有多大意义。

例如SERVER/TheHobbit/readAll.. 恶心。

3.) The model 'Book' is non-functional, it only has properties. Instead a 'BookStorage' class loads the data and fills one/multiple models. If you can expose these functional extension via the API, then that is an fine solution as well. It all comes down to documentation.

也许它最终看起来像这样

例如

SERVER/BookStorage/GetAllBooks?skipRecords=0&limit=100

SERVER/BookStorage/GetBook?title=霍比特人

3.) The model 'Book' is non-functional, it only has properties. Instead a 'BookStorage' class loads the data and fills one/multiple models.

此方法与 Repository Pattern 类似,并且很常见。 BookStorage 将是一个 BookRepository,其接口如下:

public interface IBookRepository
{
    Book GetById(int id);
    IEnumerable<Book> GetAll();
    // Other methods for creating, updating and deleting books.
}

在您的控制器中,您将拥有:

public class BooksController: ApiController
{
    private readonly IBookRepository _bookRepository;

    public BooksController(IBookRepository bookRepository)
    {
        _bookRepository = bookRepository;
    }

    [Route("api/books/{id}")]
    public IHttpActionResult Get(int id)
    {
        var book = _bookRepository.GetById(id);

        // ...
    }

    [Route("api/books")]
    public IHttpActionResult Get()
    {
        var books = _bookRepository.GetAll();

        // ...
    }
}

如果你想要分页,你可以添加一个像 Get(int page = 0) 这样的过滤器,然后根据你的页面大小,使用像 bookRepository.GetAll().Skip(PAGE_SIZE * page).Take(PAGE_SIZE).

这样的东西

模型不应自行加载,否则会违反 Single responsibility principle

Martin Fowler 的文章 Richardson Maturity Model 是设计 RESTful 网络服务的重要资源。

总结一下您的 book 案例:

  • 使用POST /book创建一本书
  • 使用PUT /book更新一本书
  • 使用GET /book?author=Shakespeare&year=1602&someOtherParam=someValue查找书籍
  • 使用 GET /book/:id 检索特定书籍的详细信息
  • 使用DELETE /book/:id删除某本书

您可能还想遵循 HATEOAS 原则,因此您需要将一本书的所有相关 link 包含在 links 属性 中,这样 API 的客户在想要 add/edit/find/delete 一本书时就不需要构建自己的 link。

虽然乍一看很简单,但设计一个好的 RESTful 网络服务并不是那么容易,但是 HATEOAS 很有帮助,因为 url 服务器端的模式更改不会影响客户端,因为它们不会对 CRUD 操作所需的 URL 进行硬编码。您需要做的就是提供一个起点,例如一个基础 url 可以发现所有内容,客户可以从那里开始浏览您的网络服务。

阅读此处的 CRUD Operations 部分。在设计 REST API

时,您会发现要遵循的 REST 最佳实践