Como modelar uma API RESTful com herança?

87

Eu tenho uma hierarquia de objetos que preciso expor por meio de uma API RESTful e não tenho certeza de como meus URLs devem ser estruturados e o que eles devem retornar. Não consegui encontrar as melhores práticas.

Digamos que eu tenha cães e gatos herdando de animais. Preciso de operações CRUD em cães e gatos; Eu também quero fazer operações em animais em geral.

Minha primeira ideia era fazer algo assim:

GET /animals        # get all animals
POST /animals       # create a dog or cat
GET /animals/123    # get animal 123

Acontece que a coleção / animals passou a ser "inconsistente", pois pode retornar e retirar objetos que não possuem exatamente a mesma estrutura (cães e gatos). É considerado "RESTful" ter uma coleção que retorna objetos com atributos diferentes?

Outra solução seria criar um URL para cada tipo concreto, assim:

GET /dogs       # get all dogs
POST /dogs      # create a dog
GET /dogs/123   # get dog 123

GET /cats       # get all cats
POST /cats      # create a cat
GET /cats/123   # get cat 123

Mas agora a relação entre cães e gatos está perdida. Se alguém deseja recuperar todos os animais, os recursos do cão e do gato devem ser consultados. O número de URLs também aumentará com cada novo subtipo de animal.

Outra sugestão era aumentar a segunda solução adicionando isto:

GET /animals    # get common attributes of all animals

Nesse caso, os animais retornados conteriam apenas atributos comuns a todos os animais, descartando atributos específicos para cães e gatos. Isso permite recuperar todos os animais, embora com menos detalhes. Cada objeto retornado pode conter um link para a versão detalhada e concreta.

Algum comentário ou sugestão?

Alpha Hydrae
fonte

Respostas:

41

Eu sugeriria:

  • Usando apenas um URI por recurso
  • Diferenciar entre animais apenas no nível de atributo

Configurar vários URIs para o mesmo recurso nunca é uma boa ideia, pois pode causar confusão e efeitos colaterais inesperados. Dado isso, seu único URI deve ser baseado em um esquema genérico como /animals.

O próximo desafio de lidar com toda a coleção de cães e gatos no nível "básico" já foi resolvido em virtude da /animalsabordagem URI.

O desafio final de lidar com tipos especializados como cães e gatos pode ser facilmente resolvido usando uma combinação de parâmetros de consulta e atributos de identificação em seu tipo de mídia. Por exemplo:

GET /animals( Accept : application/vnd.vet-services.animals+json)

{
   "animals":[
      {
         "link":"/animals/3424",
         "type":"dog",
         "name":"Rex"
      },
      {
         "link":"/animals/7829",
         "type":"cat",
         "name":"Mittens"
      }
   ]
}
  • GET /animals - obtém todos os cães e gatos, devolveria Rex e Mittens
  • GET /animals?type=dog - pega todos os cachorros, só retornaria Rex
  • GET /animals?type=cat - fica com todos os gatos, apenas as luvas

Então, ao criar ou modificar animais, caberia ao chamador especificar o tipo de animal envolvido:

Tipo de mídia: application/vnd.vet-services.animal+json

{
   "type":"dog",
   "name":"Fido"
}

A carga útil acima pode ser enviada com uma solicitação POSTou PUT.

O esquema acima fornece a você as características básicas semelhantes como herança OO por meio de REST, e com a capacidade de adicionar mais especializações (ou seja, mais tipos de animais) sem grandes cirurgias ou quaisquer alterações em seu esquema de URI.

Brian Kelly
fonte
Isso parece muito semelhante ao "lançamento" por meio de uma API REST. Também me lembra dos problemas / soluções no layout de memória de uma subclasse C ++. Por exemplo, onde e como representar simultaneamente uma base e uma subclasse com um único endereço na memória.
trcarden de
10
Eu sugiro: GET /animals - gets all dogs and cats GET /animals/dogs - gets all dogs GET /animals/cats - gets all cats
dipold
1
Além de especificar o tipo desejado como um parâmetro de solicitação GET: parece-me que você também pode usar o tipo de aceitação para fazer isso. Isto é: GET /animals Aceitarapplication/vnd.vet-services.animal.dog+json
BrianT.
22
E se o gato e o cachorro tivessem propriedades únicas? Como você lidaria com isso na POSToperação, já que a maioria dos frameworks não saberia como desserializar corretamente em um modelo, já que json não carrega boas informações de digitação. Como você lidaria com casos de postagem, por exemplo [{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat","name":"Sparkles","likesToPurr":"sometimes"}?
LB2
1
E se cães e gatos tivessem (na maioria) propriedades diferentes? por exemplo, # 1 POSTANDO uma comunicação para SMS (para, máscara) vs. um e-mail (endereço de e-mail, cc, bcc, para, de, isHtml), ou por exemplo, # 2 POSTANDO uma fonte de financiamento para cartão de crédito (maskedPan, nameOnCard, expiração) vs. a BankAccount (bsb, accountNumber) ... você ainda usaria um único recurso de API? Isso parece violar a responsabilidade única dos princípios SOLID, mas não tenho certeza se isso se aplica ao design da API ...
Ryan.Bartsch
5

Essa pergunta pode ser melhor respondida com o suporte de um aprimoramento recente introduzido na versão mais recente do OpenAPI.

Foi possível combinar esquemas usando palavras-chave como oneOf, allOf, anyOf e obter uma carga útil de mensagem validada desde o esquema JSON v1.0.

https://spacetelescope.github.io/understanding-json-schema/reference/combining.html

No entanto, no OpenAPI (antigo Swagger), a composição de esquemas foi aprimorada pelo discriminador de palavras-chave (v2.0 +) e oneOf (v3.0 +) para oferecer suporte verdadeiro ao polimorfismo.

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaComposition

Sua herança pode ser modelada usando uma combinação de oneOf (para escolher um dos subtipos) e allOf (para combinar o tipo e um de seus subtipos). Abaixo está uma definição de amostra para o método POST.

paths:
  /animals:
    post:
      requestBody:
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/Dog'
              - $ref: '#/components/schemas/Cat'
              - $ref: '#/components/schemas/Fish'
            discriminator:
              propertyName: animal_type
     responses:
       '201':
         description: Created

components:
  schemas:
    Animal:
      type: object
      required:
        - animal_type
        - name
      properties:
        animal_type:
          type: string
        name:
          type: string
      discriminator:
        property_name: animal_type
    Dog:
      allOf:
        - $ref: "#/components/schemas/Animal"
        - type: object
          properties:
            playsFetch:
              type: string
    Cat:
      allOf:
        - $ref: "#/components/schemas/Animal"
        - type: object
          properties:
            likesToPurr:
              type: string
    Fish:
      allOf:
        - $ref: "#/components/schemas/Animal"
        - type: object
          properties:
            water-type:
              type: string
dbaltor
fonte
1
É verdade que a OAS permite isso. No entanto, não há suporte para o recurso ser exibido na IU do Swagger ( link ) e acho que um recurso é de uso limitado se você não puder mostrá-lo a ninguém.
emft de
1
@emft, não é verdade. Quando escrevemos esta resposta, a IU Swagger já oferece suporte para isso.
Andrejs Cainikovs
Obrigado, isso funciona muito bem! No momento, parece verdade que a IU do Swagger não mostra isso totalmente. Os modelos serão exibidos na seção Esquemas na parte inferior, e qualquer seção de respostas que faça referência à seção oneOf será parcialmente exibida na IU (apenas esquema, sem exemplos), mas você não obtém nenhum corpo de exemplo para a entrada da solicitação. O problema do github para isso está aberto há 3 anos, então provavelmente continuará assim: github.com/swagger-api/swagger-ui/issues/3803
Jethro
4

Eu iria para / animals retornando uma lista de cães e peixes e o que mais:

<animals>
  <animal type="dog">
    <name>Fido</name>
    <fur-color>White</fur-color>
  </animal>
  <animal type="fish">
    <name>Wanda</name>
    <water-type>Salt</water-type>
  </animal>
</animals>

Deve ser fácil implementar um exemplo JSON semelhante.

Os clientes podem sempre contar com a presença do elemento "nome" (um atributo comum). Mas, dependendo do atributo "tipo", haverá outros elementos como parte da representação animal.

Não há nada inerentemente RESTful ou unRESTful em retornar tal lista - REST não prescreve nenhum formato específico para representar dados. Tudo o que diz é que os dados devem ter alguma representação e o formato para essa representação é identificado pelo tipo de mídia (que em HTTP é o cabeçalho Content-Type).

Pense em seus casos de uso - você precisa mostrar uma lista de animais mistos? Bem, então retorne uma lista de dados mistos de animais. Você precisa de uma lista apenas de cães? Bem, faça essa lista.

Se você faz / animals? Type = dog ou / dogs é irrelevante em relação ao REST, que não prescreve nenhum formato de URL - isso é deixado como um detalhe de implementação fora do escopo do REST. REST apenas afirma que os recursos devem ter identificadores - não importa o formato.

Você deve adicionar alguns links de hipermídia para se aproximar de uma API RESTful. Por exemplo, adicionando referências aos detalhes do animal:

<animals>
  <animal type="dog" href="https://stackoverflow.com/animals/123">
    <name>Fido</name>
    <fur-color>White</fur-color>
  </animal>
  <animal type="fish" href="https://stackoverflow.com/animals/321">
    <name>Wanda</name>
    <water-type>Salt</water-type>
  </animal>
</animals>

Ao adicionar links de hipermídia, você reduz o acoplamento cliente / servidor - no caso acima, você tira o fardo da construção de URL do cliente e deixa o servidor decidir como construir URLs (dos quais ele, por definição, é a única autoridade).

Jørn Wildt
fonte
1

Mas agora a relação entre cães e gatos está perdida.

De fato, mas tenha em mente que o URI simplesmente nunca reflete relações entre objetos.

G. Demecki
fonte
1

Eu sei que esta é uma questão antiga, mas estou interessado em investigar mais problemas em uma modelagem de herança RESTful

Sempre posso dizer que cachorro é animal e galinha também, mas galinha faz ovos enquanto cachorro é mamífero, então não pode. Uma API como

OBTER animais /: ID animal / ovos

não é consistente porque indica que todos os subtipos de animais podem ter ovos (como consequência da substituição de Liskov). Haveria um fallback se todos os mamíferos respondessem com '0' a esta solicitação, mas e se eu também habilitar um método POST? Devo ter medo de que amanhã haja ovos de cachorro em meus crepes?

A única maneira de lidar com esses cenários é fornecer um 'super-recurso' que agrega todos os sub-recursos compartilhados entre todos os 'recursos derivados' possíveis e, em seguida, uma especialização para cada recurso derivado que precisa dele, assim como quando fazemos o downcast de um objeto em oop

GET / animals /: animalID / sons GET / hens /: animalID / eggs POST / hens /: animalID / eggs

A desvantagem, aqui, é que alguém poderia passar um ID de cachorro para fazer referência a uma instância de coleção de galinhas, mas o cachorro não é uma galinha, então não seria incorreto se a resposta fosse 404 ou 400 com uma mensagem de motivo

Estou errado?

Carmine Ingaldi
fonte
1
Acho que você está colocando muita ênfase na estrutura de URI. A única maneira de chegar a "animals /: animalID / eggs" é através do HATEOAS. Portanto, primeiro você solicitaria o animal por meio de "animals /: animalID" e, para os animais que podem ter ovos, haverá um link para "animals /: animalID / eggs", e para aqueles que não têm, não haverá ser um link para ir do animal aos ovos. Se alguém de alguma forma acabar tendo ovos para um animal que não pode ter ovos, retorne o código de status HTTP apropriado (não encontrado ou proibido, por exemplo)
wired_in
0

Sim, você está errado. Também os relacionamentos podem ser modelados seguindo as especificações da OpenAPI, por exemplo, desta forma polimórfica.

Chicken:
  type: object
  discriminator:
    propertyName: typeInformation
  allOf:
    - $ref:'#components/schemas/Chicken'
    - type: object
      properties:
        eggs:
          type: array
          items:
            $ref:'#/components/schemas/Egg'
          name:
            type: string

...

Andreas Gaus
fonte
Comentário adicional: focar na rota da API GET chicken/eggs também deve funcionar usando os geradores de código OpenAPI comuns para os controladores, mas eu não verifiquei isso ainda. Talvez alguém possa tentar?
Andreas Gaus