为 Web 服务建模请求和响应对象的设计模式

45

我需要实现大约7个REST Web服务。这些Web服务中有一些有标准(相同)的响应,而有些则有不同的响应。

这些Web服务的请求不同,但某些请求和某些响应具有相同的基础数据对象。

我不确定是否必须为每个Web服务构建单独的请求/响应类,还是重用一个标准类。我想知道是否有设计模式可以为这些Web服务的请求对象和响应对象建模。

假设Account和Book是我的Web服务将要处理的两个REST资源。

class Account {
    String username;
    String id;
}


class Book {
    String title;
    String isbn;
}

我的 Web 服务长这样:

MYAPI/CreateAccountandBook
MYAPI/Account/Create
MYAPI/Book/Create
MYAPI/Book/Update/{isbn}
MYAPI/Account/Update/{id}
MYAPI/Account/getInfo/{id} 

等等其他内容。

现在,CreateAccountandBook请求将在有效负载中采取一个帐户对象和一组书籍的列表。 此外,MYAPI/Account/getInfo/{id}的响应对象具有与该帐户关联的帐户对象和书籍列表。但是,响应对象还包括一个statusCodeDescription

现在,我想以最好的方式为这些请求和响应对象创建类。

好的,先从以下两个抽象类StandardRequestStandardResponse开始。

所有请求类都将扩展标准请求类并进行相应的自定义。 所有响应类都将扩展标准响应类并进行相应的自定义。

但是,这些请求和响应可以非常相互不同,但仍可以重用相同的实体对象。

例如:

createAccountandBook请求对象如下所示:

class CreateAccountAndBookRequest {
   Account account;
   List<Book> books;
}

当调用 getInfo web 服务时,响应为:

class GetInfoResponse {
   Account account;
   List<Book> books;
   String statusCode;
   String description;
}

因此,请求和响应类之间存在重叠。我可以为每个 Web 服务创建两个(req/res)类。但是我想知道是否有更好的方法来建模这些类。


你想要一个设计模式来为你设计对象吗? - thatidiotguy
不完全正确!我想知道构建这些请求和响应类的最佳方式。 - DntFrgtDSemiCln
我认为你的问题没有可能得到任何答案。你没有提供关于你试图设计的这些对象的属性或行为的任何信息,因此无法帮助你。你应该尝试着去做,如果遇到问题再回来提出更具体的问题。 - thatidiotguy
更新了更多信息。 - DntFrgtDSemiCln
6个回答

67
  • Separation of Concerns: The request and response classes should only contain data related to the request and response, while the model class should only contain data relating to the entity being modeled. Combining them violates this principle.
  • Information Hiding: By tightly coupling the request and response classes to the model class, any changes in one can affect the others, leading to unintended consequences and potential bugs.
  • Encapsulation: Keeping the request and response classes separate from the model class allows for better encapsulation and modularity, making it easier to maintain and update the code in the future.
  • Therefore, the best solution is to have both the request and response classes (e.g. CreateBookRequest/Response) and the model class (e.g. Book) contain all the necessary data/getters/setters, as in option E. This approach may not be as intuitive, but it ensures that each class stays focused on its own responsibilities and reduces tightly coupled dependencies between them.

  • 您希望独立修改模型对象或请求对象。如果您的请求引用了您的模型(如 A-D 中所示),则对模型的任何更改都将反映在界面中,从而破坏您的 API。您的客户将根据请求/响应类所规定的 API 编写客户端,并且他们不希望每当您对模型类进行更改时就更改这些客户端。您希望请求/响应和模型之间独立变化。

  • 关注点分离。您的请求类 CreateBookRequest 可能包含各种接口/协议相关的注释和成员(例如 JAX-RS 实现知道如何执行的验证注释)。这些与接口相关的注释不应该在模型对象中。(如 A 中所示)

  • 从面向对象的角度来看,CreateBookRequest 不是 Book(不是 IS_A),也不包含书籍。

  • 控制流程应该如下:

  • 接口/控制层(接收 Rest-API 调用的层)应使用专门为该层定义的 Request/Response 类作为其方法参数(例如 CreateBookRequest)。让容器/基础架构从 REST/HTTP/其他请求创建这些类。

  • 接口/控制层中的方法应以某种方式创建模型类对象实例,并从请求类复制值到模型类对象中。

  • 接口/控制层中的方法应调用 BO/Manager/Whatever(在负责业务逻辑的模型层中...),传递给它模型类对象而不是接口类/方法参数类对象(换句话说,不是如 Luiggi Mendoza 在他的答案中所示的那样)。

  • 模型/BO 方法将返回某些模型类对象或某些“基本类型”。

  • 现在,接口方法(调用者)应创建一个接口类响应对象,并从模型/BO 返回的模型类对象中复制值。(就像 Luiggi Mendoza 在他的答案中所示的那样)

  • 然后容器/基础架构将从响应类对象创建JSON/XML或其他响应。

  • 现在来回答问题... 请求和响应类之间应该是什么关系?

    请求类应该从请求类扩展而不是扩展或包含响应类,反之亦然(正如提问者所建议的)。 通常,您会有一个非常基本的BaseRequest类,由诸如CreateRequest、UpdateRequest等扩展,在CreateRequest中包含所有创建请求的公共属性,然后由更具体的请求类(如CreateBookRequest)进行扩展...
    类似地,但与此并行的是响应类层次结构。

    提问者还问道,是否可以让CreateBookRequest和CreateBookResponse都包含相同的成员,例如(永远不是模型类!) BookStuffInRequestAndResponse,其属性适用于请求和响应?

    这不像请求或响应引用也被模型引用那样严重。问题在于,如果您需要更改API请求并在BookStuffInRequestAndResponse中进行更改,则会立即影响您的响应(反之亦然)。

    这不是很糟糕,因为1)如果您的客户端需要修复其客户端代码,因为您更改了请求参数,则他们可能会修复/处理更改的响应,2)最可能的是请求中的更改也需要对响应进行更改(例如添加新属性),但这并非总是如此。


    1
    嗨inor,非常好的实用解释,非常感谢。关于“控制流”,第2点)接口/控制层中的方法应以某种方式创建模型类对象的实例,并将值从请求类复制到模型类对象中。将接口/控制保持尽可能轻便并在BO类中“转换”请求模型为数据域模型的逻辑是否可行?还是最好(而不是它的责任)让BO类仅处理干净的数据模型? - Alex 75
    嗨Alex 75,BO绝对不应该处理请求对象,只能处理BO/Model对象。BO请求对象的创建可以由接口/控制层类调用的某个工厂方法完成。同样地,从BO/Model对象映射到响应对象也是如此。考虑使用Dozer作为映射工厂http://dozer.sourceforge.net/。 - inor
    @inor 这个回答中的“BO”是什么意思或指的是什么? - Ibrahim Mohamed
    2
    @IbrahimShendy BO代表业务对象。 - Vijay Kumar Rajput
    @inor 很好的解释。我已经设计了很多遵循相同模式的REST API(就像你所解释的那样 :-))。我们使用BaseRequest/BaseResponse分别扩展REST请求和响应,其中包含DO = 数据对象和转换服务Transfer将所有对象转换为BO = 业务对象,然后控制器将它们传递给相应的服务/提供者。在执行业务逻辑后,再次使用转换服务将BO转换为DO,然后控制器将REST API响应返回给调用者。 - Vijay Kumar Rajput

    13

    我曾经也面临过类似的两难选择;我采用了通用方案,目前效果不错,也从未回头。

    如果我有一个GetAccounts API方法,它的签名可能如下:

    public final Response<Account[]> getAccounts()
    

    自然地,相同的原则可以应用于请求。
    public final Response<Account[]> rebalanceAccounts(Request<Account[]>) { ... }
    

    在我看来,将单个实体与请求和响应解耦可以生成更整洁的域和对象图。

    以下是这样一个通用响应对象的示例。在我的情况下,我构建了一个通用响应服务器,以增强错误处理并降低域对象和响应对象之间的耦合。

    public class Response<T> {
    
      private static final String R_MSG_EMPTY = "";
      private static final String R_CODE_OK = "OK";
    
      private final String responseCode;
      private final Date execDt;
      private final String message;
    
      private T response;
    
      /**
       * A Creates a new instance of Response
       *
       * @param code
       * @param message
       * @param execDt
       */
      public Response(final String code, final String message, final Date execDt) {
    
        this.execDt = execDt == null ? Calendar.getInstance().getTime() : execDt;
        this.message = message == null ? Response.R_MSG_EMPTY : message;
        this.responseCode = code == null ? Response.R_CODE_OK : code;
        this.response = null;
      }
    
      /**
       * @return the execDt
       */
      public Date getExecDt() {
    
        return this.execDt;
      }
    
      /**
       * @return the message
       */
      public String getMessage() {
    
        return this.message;
      }
    
      /**
       * @return the response
       */
      public T getResponse() {
    
        return this.response;
      }
    
      /**
       * @return the responseCode
       */
      public String getResponseCode() {
    
        return this.responseCode;
      }
    
      /**
       * sets the response object
       *
       * @param obj
       * @return
       */
      public Response<T> setResponse(final T obj) {
    
        this.response = obj;
        return this;
      }
    }
    

    1
    这对于响应来说很好,但问题似乎在于如何链接请求和响应,特别是考虑到它们将是不同类型的。 - Rene Wooller
    6
    你说“降低域对象和响应对象之间的耦合度”这是一个很好的目标,但我认为你的Response<Account>响应对象与模型对象Account非常紧密地耦合在一起。任何对Account的更改都会立即影响流向用户的响应。如果你有一个API发布了响应包含的内容,那么对模型对象Account的更改将会破坏你的API,使客户不满意。 - inor
    1
    正如@inor所说,这个答案违反了单一职责和公共封闭原则。也许更好的方式是用Uncle Bob的Clean Architecture中的话来表达。引用书中的话:“你可能会想让这些数据结构包含对实体对象的引用。[...]这很有道理,因为它们共享很多数据。但要避免这种诱惑!随着时间的推移,它们会因不同的原因而发生变化,因此将它们绑在一起违反了公共封闭和单一职责原则。结果会产生大量的垃圾数据和大量的条件语句。” - Diego

    3

    我不知道是否有这种设计模式。 我这样做:

    • For GET requests, define the parameters in query string or in path. Preferred way is path. Also, you will have few parameters for your service. Each service will handle this on its own. There is no reusability here.
    • For POST requests, consume the parameters in JSON format that comes in the body of the request. Also, use an adapter (depending on the technology you're using) that will map the JSON content to a specific class that you receive as parameter.
    • For responses, there are two approaches:

      • You can create a custom ResponseWrapper class that will be your real response. This will contain the response code, the description and a field called value which stores the real content of the response in case of a success processing of the input data. The class will look like this:

        public class ResponseWrapper {
            private int statusCode;
            private String description;
            private String value;
        }
        

        In this case, String value will store the concrete response in JSON format. For example:

        @Path("/yourapi/book")
        public class BookRestfulService {
        
            @POST("/create")
            @Produces("json")
            public ResponseWrapper createBook(Book book) {
                ResponseWrapper rw = new ResponseWrapper();
                //do the processing...
                BookService bookService = new BookService();
                SomeClassToStoreResult result = bookService.create(book);
                //define the response...
                rw.setStatusCode(...);
                rw.setDescription("...");
                rw.setValue( convertToJson(result) );
            }
        
            static String convertToJson(Object object) {
                //method that probably will use a library like Jackson or Gson
                //to convert the object into a proper JSON strong
            }
        }
        
      • Reuse the HTTP Response Status Code, use 200 (or 201, this depends on the type of request) for successful requests and a proper status code for the response. If your response has status code 200 (or 201) then return the proper object in JSON format. If your response has a different status code, provide a JSON object like this:

        { "error" : "There is no foo in bar." }
        
    使用JSON或XML的RESTful服务存在一种权衡,即消费者可能不知道响应的结构所带来的复杂性代价。对于WS-* web服务,这种权衡则体现在性能方面(与RESTful方法相比)。

    那么你可能没有其他事情要做了。只需为消费者提供良好的服务文档,以便他们能够轻松地映射响应即可。 - Luiggi Mendoza
    @DntFrgtDSemiCln 对于jax-rs从模型对象生成响应是错误的!因为你的响应对象和你的模型对象应该独立变化。 - inor
    @Luiggi Mendoza,这是一个很好的例子,展示了如何处理响应。你是对的,将响应对象从模型对象中解耦出来。Jax-rs可以从ResponseWrapper生成xml/json。像Luiggi建议的那样返回模型对象是错误的。但是你处理请求的方式是错误的。正确的方法是将请求对象BookRequest与模型对象Book解耦。你需要"public ResponseWrapper createBook(BookRequest bookRequest)"。然后你的接口方法应该从请求对象BookRequest创建模型对象Book,并将Book传递给模型。 - inor
    @inor 我并没有说你应该直接使用模型类进行请求/响应通信。这只是一个示例。在现实生活中,您有不同的类用于数据传输对象和领域模型,并且有一层帮助您在这些对象之间进行翻译的层。 - Luiggi Mendoza
    1
    @Luiggi Mendoza 1)我认为这是一个“现实生活”问题。提问者想要一个现实生活的答案。他已经考虑了几种“样本”替代方案。2)你给出了一个好的“现实生活”回应,我只是指出你也应该将请求部分处理为“现实生活”解决方案。为什么要对请求进行“样本”处理?3)无论我看到哪里,我都看到“样本生活”实现(包括我曾在其中工作过的知名公司的实际生产代码)。 - inor
    显示剩余2条评论

    1

    关于如何建模请求对象和响应对象,如果有一个设计模式可以使用,标准的方法是考虑 命令设计模式。它允许您将命令请求封装为一个对象,并让您使用不同的请求对客户端进行参数化,排队或记录请求、响应并支持可撤销操作等。

    以下是一个示例实现:

      abstract class Request{
        public abstract void Execute();
        public abstract void UnExecute();
      } 
    
       class AccountAndBookRequest extends Request{
       Account account;
       List<Book> books;
       }
    

    6
    UnExecute是什么意思? - Jasmine
    5
    您的业务领域可能允许事务回滚。 - ekostadinov
    通过在请求(AccountAndBookRequest)中使用模型对象(Account,Book),您正在紧密耦合模型对象和请求对象。 - inor
    是的,这个例子可能会使用更多的抽象和接口比具体模型更好。例如,DI在这里是一个很好的选择。你是对的@inor - ekostadinov

    0
     public ResponseDto(){
         String username;
         int id;
         ArrayList books <Book> = new ArrayList<Book>();
    
        // proper getters and setters...
        @Override
        public String toString(){
           //parse this object to return a proper response to the rest service,
           //you can parse using some JSON library like GSON
        }
    }
    

    1
    这是错误的,因为您将响应对象ResponseDto与模型对象Book紧密耦合在一起。它们需要能够独立变化。在您的答案中,对模型的任何更改都会影响响应。 - inor
    泛型化响应DTO - 212

    -1

    你可以尝试六边形架构,它的核心是将业务逻辑放在中间,并通过rest、soap、smtp等方式访问。

    在你的Rest文件中,你只需要公开路径并调用一个包含所有需要做的事情的类,比如DAO或其他东西。

    如果服务引用相同,你可以将其留在单个类中,或根据服务的目的进行分离。

    对于你的问题,你可以创建一个DTO(数据传输对象),其中包含你需要的简单数据,比如账户号码、书籍、描述,你可以重写toString()方法以提供适当的响应或将数据转换为json响应。

    你可以查看http://alistair.cockburn.us/Hexagonal+architecture,我认为这是最佳实践。


    谢谢您的回答!但是我认为这与我所寻找的不同。 - DntFrgtDSemiCln

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接