Creating the Hypermedia API using Domain-driven

Source: Internet
Author: User

In reality, we encounter a variety of complex scenarios, "there is not a right-way" to describe the API design method is more appropriate, there is no one API design to deal with all scenarios. Unlike "Consumer-driven contract", this article describes another way to design an API: Domain-driven API. This is not a standard method of API design, but he may be able to inspire you to help you design more expressive APIs.

Post/api/customer
Post/api/customer/order
Put/api/customer
Post/api/customer/notification

is an API document fragment, they use HTTP action plus a Uniform Resource Identifier (URI) to describe their intentions, perhaps also need a good document to describe his parameters, return type, etc., can be invoked and used by the client. On the market there are similar swager such efficient products, it is also very convenient to use. But such an API is more or less a minor design problem:

    • Unable to describe context through API

Even though an HTTP verb and a noun that describes the API's resources can basically describe its intentions, an API document seems to be indispensable in the course of use. Over the past several years I have removed the bad habit of writing comments to code because I realize that good organization and code are self-describing. When we designed the API, however, we accepted the fact that we were writing the document. In the "Consumer-driven contract" process, we also write a contract test to drive the service side to ensure the consistency of the contract. Is it possible for API resources to include this contract while allowing consumers to abide by the contract?

    • The API consumer knows too much

In the API documentation fragment, do you know when the following API should be called?

Post/api/customer/notification

You may not know, perhaps when the user placed the order, or the user paid the order, depending on the demand. Seems reasonable, but this scenario bodes well for the suspicion that some domain logic has shifted to the consumer's side. For example, you go to a restaurant to eat, the waiter brought over a menu, when you ordered a soup waiter told you this menu has its own rules, only you order an egg fried rice, you can order this soup. At this point you have only one choice. That is to remember this rule, next time the egg fried rice. Is it possible not to impose this rule on the consumer side?

    • Fragile design

The API provides the service in a way that provides a URI, and the URI is essentially a string, and as a strongly typed player, I don't want such a string scattered around the corner, imagine I renamed a URI, I had to search for and modify all the code that used this resource.

First, design domain

What are we doing when we drive the design in the field of practice? Identify domain boundaries, find aggregate roots, divide domain, and make an abstract and well-designed model based on domain capabilities. Domain's process of providing business requirements is the process of changing the state of the domain model.

In the same way, what do we design the API for to achieve? I hope my API will not only be able to complete the addition and deletion of the search, but also more expressive force. Each API is not independent, they are the domain model at a certain moment of state and ability of the embodiment, each API resources in the state of the current domain to inform consumers at the same time, but also tell the consumer what the current domain has the ability to what consumers can do next, That is, consumers can request which API resources.

So the API design is actually closely related to the design of domain capabilities, I decided to use the airline's ticket sales business to illustrate.

Business requirements:

    • An airline called Restairline offers an online ticket sales service that allows users to search all available flights (trip) by search criteria
    • The entire booking (booking) process begins when the passenger selects an available flight (trip)
    • Once a passenger chooses an available flight, it can modify the flight (change trip) and select the seat (seat)
    • When the passengers choose to complete the seat can also add some additional services, such as: Pick-up services (transfer service), etc., the final payment (payment)
    • Passengers can also make online check-in (checkin) and print their boarding pass (Boardingpass) before the plane takes off, and can also re-select seats during checkin

Note: the English terminology in parentheses can be understood as the company's domain language, and we use the same terminology when modeling in the domain, thus reducing the cost of communication with domain experts.
We can easily analyze a number of domain:booking, Payment, Trip avalability for the above requirements.

    • Design Booking Domain
      We use booking domain as an example to describe the design process, the following interaction diagram clearly describes the ability of booking

    • Implementing booking Domain
      The implementation process is also fairly straightforward, and if you read the code below, it is almost exactly the same as the business requirements described earlier. The implementation of Booking domain requires attention to the following points:

    • All properties are private set, which means that the internal properties of domain are maintained by themselves;
    • Airporttransfer is Maybe<T> a type, meaning that in a complete booking, you can choose not to pick up the shuttle service (Transferservice), whereas for the trip attribute, even if he is a reference type at the language level, it can be null, But a booking that contains an empty trip does not exist, so in a complete domain, once a non- Maybe<T> type property is NULL, we can assume that the booking is invalid;
    • The constructor of the class is decorated as private, meaning that booking can only be created by selecting the available flights, meaning that the code interprets the business requirements
    • Checkin is designed to be sub-domain, because the checkin implementation process is slightly complex and whether it is a sub-domain depends on the design;

 Public classbooking{ PublicGuid Id {get;} PublicIreadonlylist<passenger> passengers = _passengers.asreadonly(); PublicTrip trip {get;} Publicireadonlylist<maybe<seat>> seats = _passengers.Select(P = p.selectedseat).ToList().asreadonly(); PublicMaybe<airporttransfer> Airporttransfer {get;PrivateSet }PrivateReadOnly list<passenger> _passengers;PrivateReadOnly checkinprocess _checkinprocess;Private Booking(Trip-trip, list<passenger> passengers) {Id = Guid.}NewGuid(); _checkinprocess = checkinprocess.createcheckinprocess( This);        trip = Trip;    _passengers = passengers; } Public StaticBookingSelecttrip(Trip-trip, list<passenger> passengers) {//validation for trips and passengers in herevar booking =New Booking(trip, passengers);returnBooking } Public void Changeflight(Trip.JourneyJourney, Flight Flight) {//Checking is it eligible for changing flight;Trip.Changeflight(Journey.Id, flight); } Public void assignseat(Seat Seat, passenger passenger) {//validation in herevar p = _passengers. Single(s = = S.)Name.Equals(Passenger.Name)); P.assignseat(seat); }//... Other capabilities}
Second, the design of a domain-capable API

Based on the above-designed domain, we can easily design the first api:tripselection to express domain capabilities:

Post/api/booking/tripselection

In fact, the implementation of this API is to directly invoke the corresponding domain capabilities:

var booking = Booking.SelectTrip(trip, passengers)
    • In terms of domain, this capability creates a booking and adds an available flight (trip) and passenger list to the booking,
      At this time, booking has some initial state, but also has a certain capacity: allocation of seats (seat) and modified flights (flight).
    • From the perspective of the API consumer, after the consumer has finished tripselection this API, in addition to getting some necessary return values, but also has the ability to invoke the following three APIs:

GET Api/booking/{id}
PUT api/booking/{id}/seatassignment
PUT Api/booking/{id}/changeflight

These three APIs are consistent with the capabilities that domain has at this time. the idea of the Hypermedia API is that the API resources, in addition to the necessary return values, can also tell the API consumer the next domain-owned capabilities and the state of the domain at this time, that is, API consumers can request what kind of API next.

Third, implement the Hypermedia API

Based on the above analysis, we tried to model the first version of the resources returned by the Tripselection API, with an initial version as follows:

public   tripselectionresource{private  readonly iurlhelper _urlhelper; public  tripselectionresource     (Iurlhelper Urlhelper)    {_urlhelper = Urlhelper;    } public  Guid bookingid {get; set;} public  string bookingresource = _urlhelper. action     ( "getbooking" ,  "Booking" ); public  string flightchange = _urlhelper. action     ( "Changeflight" ,  "Booking" ); public  string seatassignment = _urlhelper. action  ( "assignseat" ,  "Booking" );}  

Where bookingresource , flightchange , seatassignment are the corresponding API URI addresses, respectively, Uses the urlhelper.action ("ActionName", "Controllername") method provided by the ASP. NET Web API to generate a URL. Such a method takes two strings to generate a URL address, but this is not a strong type of gameplay, so it is immediately thought that by parsing lambda expression tree to generate a URL, extend a method on Iurlhelper to make the code easier to support refactoring.

public   tripselectionresource{private  readonly iurlhelper _urlhelper; public  tripselectionresource     (Iurlhelper Urlhelper)    {_urlhelper = Urlhelper;    } public  Guid bookingid {get; set;} public  string bookingresource = _urlhelper. link     ((bookingcontroller c) = C.getbooking  (Bookingid)); public  string flightchange = _urlhelper. link     ((bookingcontroller c) = C.changeflight  ()); public  string seatassignment = _urlhelper. link  ((bookingcontroller c) = C.assignseat  ());  

Theoretically all APIs can be divided into two classes, Command and Query (refer to CQRS pattern), where the API that can change the state of the domain can be considered an API consumer sends a Command , another type of API can be divided into query, no matter how many times the API consumer requests will not change the state of domain, usually refers to a GET request.
for the three APIs contained in Tripselectionresource , we can also divide it into two categories:

public   tripselectionresource{private  readonly iurlhelper _urlhelper; public  tripselectionresource     (Iurlhelper Urlhelper)    {_urlhelper = Urlhelper;    } public  Guid bookingid {get; set;} public  Link<bookingresource> Booking = _urlhelper. link     ((bookingcontroller c) = C.getbooking  (Bookingid)); public  Changeflightcommand changeflight = new  changeflightcommand  (_    Urlhelper); public  Assignseatcommand assignseat = new  assignseatcommand  (_ Urlhelper);}  

The API of the query class is abstracted as a Link<T> type, and Command the API for the class, like ChangeFlightCommand , not only tells the API consumer the URI of the API, but also tells the API consumer to abide by the contract.

publicclass ChangeFlightCommand : HypermediaCommand<FlightChangeResource>{    publicChangeFlightCommand(IUrlHelper urlHelper) :             base(urlHelper.Link((BookingController c) => c.ChangeFlight(null))) { }    public Trip.Journey Journey { get; set; }    public Flight Flight { get; set; }}

One of the tripselection resources returned by the above modeling method is as follows:

{"Bookingid":"6cedc5fc-afed-4e34-8906-2ddc4b8cac6f","Booking": {"Uri":"localhost:3000/api/booking/6cedc5fc-afed-4e34-8906-2ddc4b8cac6f"},"Changeflight": {"Bookingid":"6cedc5fc-afed-4e34-8906-2ddc4b8cac6f","Journey": {"Id":"00000000-0000-0000-0000-000000000000",//Ignore other fields},"Flight": {"Number":NULL,//Ignore other fields},"PostURL": {"Uri":"Localhost:3000/api/booking/6cedc5fc-afed-4e34-8906-2ddc4b8cac6f/flightchange"}    },"Assignseat": {"Bookingid":"6cedc5fc-afed-4e34-8906-2ddc4b8cac6f","Seat": {"Number":NULL,"Seattype":0},"Passenger": {"Name":NULL,"Passengertype":0,"Age":0,"Email":NULL},"PostURL": {"Uri":"Localhost:3000/api/booking/6cedc5fc-afed-4e34-8906-2ddc4b8cac6f/seatassignment"}    }}

This resource contains the service-side return value Bookingid, which also returns the list of APIs that the API consumer will then be able to use, with the command-type API containing the contract content.

Iv. how to gracefully consume hypermedia API

According to the design ideas provided in this article, because we designed the API will always be able to return the next available API list, so we can think of all the API is hierarchical, then the server will certainly provide a top-level API for consumers to use. Imagine how a consumer can consume such an API?

The first round, it must be the API consumer to get the top of the API address, we expect the consumer to get some useful information through this API:

var startResource = restAirlineApiNavigator.Execute();

The second round, from 搜索可用航班 the API address obtained from the previous resource, sends the request according to the contract:

var searchTripsCommand = startResource.SearchTripsCommand;searchTripsCommand.SearchCriteria = TripSearchCriteria.DefaultTripSearchCriteria();var tripAvailabilityResource = restAirlineApiNavigator.PostCommand(searchTripsCommand);

The third round, from the above resources to get 选择可用航班 the API address, according to the contract to send the request:

var selectTripCommand = tripAvailabilityResource.SelectTripCommand;selectTripCommand.Trip = tripAvailabilityResource.AvailableTrips.First();var tripSelectonResource = restAirlineApiNavigator.PostCommand(selectTripCommand);

Above is a C # version of the API consumer, Restairlineapinavigator is a typed API navigator, he has the following interface:

publicinterface IApiNavigator<TResource>{    Execute();    TResourceToFetch PostCommand<TResourceToFetch>                        (HypermediaCommand<TResourceToFetch> command);    SubApiNavigator<TTargetResource, TResource> FollowLink<TTargetResource>(        Func<TResource, Link<TTargetResource>> navigator);}

Of course, if your API consumer is JavaScript, you should not be able to write such API navigator to help you do type assurance, but you can write a typescript version of the API Navigator, A typical hypermedia consumption process is as follows:

getProducts(): Observable<ProductsResource> {    constthis.apiNavigator        .followLink(start => start.productHome)        .followLink(product => product.products)        .execute();    return products;}

This design may not be sufficient for all scenarios, but he can help you create more expressive APIs to some extent, while also making the API consumer less dependent on the document. Compared to the theory, this article is more in the discussion of practice and implementation of details, of course, a lot of content inevitably have flaws, welcome to correct.

Creating the Hypermedia API using Domain-driven

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.