跳轉到

Polymorphism

The goal of this pattern is to illustrate a safe, flexible, and sustainable way to bring one of the most powerful features of object-oriented programming into the world of resource-oriented APIs

Implementation

API definition

abstract class ChatRoomApi {
  @post("/{parent=chatRooms/*}/messages")
  CreateMessage(req: CreateMessageRequest): Message;
}

interface CreateMessageRequest {
  parent: string;
  resource: Message;
}

interface Message {
  id: string;
  sender: string;
  type: 'text' | 'image' | 'audio' | 'video';
  content: string | Media
}

interface Media {
  uri: string;
  contentType: string;
}

Exercises

  1. Imagine you're creating an API that relies on URL bookmarks for a web browser that can be arranged into collections or folders. Does it make sense to put these two concepts (a folder and a bookmark) into a single polymorphic resource? Why or why not?

  2. Why should we rely on a string field for storing polymorphic resource types instead of something like an enumeration?

In code this is usually not an issue at all; however, when looking through logs of requests it can become quite burdensome.

Further, when new enumeration values are added on the server, the client will be required to update their own copy of that local mapping (often requiring a client library update) rather than simply sending a different value. Even scarier, if an API decides to add a new enumeration value and the client hasn’t been told what that value means, the client code will be left confused and unsure what to do.

For example, consider the scenario where an API adds a new Color value such as Hazel (#4). Unless the client-side code has been updated to accommodate this new value, we might end up with a pretty confusing scenario. On the other hand, if we use a different type of field (such as a string), we might end up with a similar error due to a previously unknown value, but we won’t be confused by the meaning of that value ("hazel" is much clearer than 4).
  1. Why should additional data be ignored (e.g., providing a radius for a Shape resource of type square) rather than rejected with an error?
Backward Compatibility: Ignoring additional data allows for a more flexible API that can evolve over time without breaking existing clients. If a client adds extra information that the server doesn't use, the server can still process the request correctly without causing an error. This is particularly important for public APIs where you want to avoid breaking changes.
  1. Why should we avoid polymorphic methods? Why is it a bad idea to have a polymorphic set of standard methods (e.g., UpdateResource())?
Maybe we want to support soft deletion or validation requests on specific resources but not all resources.

We don’t have a crystal ball to help predict the future of an API, it’s far safer to assume that things will change and will do so in their own unique ways, not as a uniform group.

Summary

  • Polymorphism in APIs allows resources to take on varying types to avoid duplicating shared functionality.

  • Generally, it makes sense to rely on polymorphism when users can reasonably expect to list resources of multiple types together (e.g., listing messages in a chat room where each message might be of a different type).

  • The type information for a polymorphic resource should be a string field, but the possible choices for this field should be changed (added to or removed from) carefully to avoid breaking existing client code.

  • Rather than throwing errors for invalid input data, validation should instead check whether the required data is present and ignore any irrelevant data.

  • Polymorphic methods should generally be avoided as they prohibit future deviation in behavior over time.