使用Domain-Driven创建Hypermedia API
POST /api/customer
POST /api/customer/order
PUT /api/customer
POST /api/customer/notification
上图是一个API文档片段,他们通过HTTP动作加上统一资源标识符(URI)来描述自己的意图,也许还需要一份不错的文档来描述他的参数,返回类型等,就能被消费端调用和使用。市面上也有类似Swager这样高效的产品,用起来也很方便。但是这样的API或多或少有一些设计方面的小问题:
1. 无法通过API描述上下文
2. API消费端知道的太多
3. 易碎的设计
业务需求:
一个叫做RestAirline的航空公司提供在线机票出售业务,用户可以按照搜索条件搜索到所有可用的航班(trip) 当乘客选中一条可用的航班(trip)就开始了整个预定(booking)流程 一旦乘客选择了一条可用的航班就可以修改航班(change trip)和选择座位(seat) 当乘客选择完座位还可以添加一些额外的服务,如:接送机服务(transfer service)等, 最后通过不同的支付方式完成支付(payment) 乘客在飞机起飞前,还可以做在线登机手续(checkin)并打印登机牌(boardingpass),在Checkin的过程中还可以重新选择座位
1. 设计Booking领域模型
2. 实现Booking Domain
所有属性都是private set,意味着领域模型内部属性是靠自己维护的;
AirportTransfer为Maybe类型,意味着在一个完整的Booking中,可以不选择接送机服务(TransferService);对于Trip属性而言,即便从语言层面上来讲他是引用类型,可以为null,但是一个包含空Trip的Booking是不存在的,所以一个完整的Booking领域模型中,一旦一个非Maybe类型的属性为null,那我们就可以认为这个Booking就是无效的;
该类的构造函数被修饰为private,意味着Booking领域模型只能通过选择可用的航班来创建,代码的含义诠释了业务需求;
public class Booking
{
public Guid Id { get; }
public IReadOnlyList<Passenger> Passengers => _passengers.AsReadOnly();
public Trip Trip { get; }
public IReadOnlyList<Maybe<Seat>> Seats => _passengers.Select(p => p.SelectedSeat).ToList().AsReadOnly();
public Maybe<AirportTransfer> AirportTransfer { get; private set; }
private readonly List<Passenger> _passengers;
private readonly CheckinProcess _checkinProcess;
private Booking(Trip trip, List<Passenger> passengers)
{
Id = Guid.NewGuid();
_checkinProcess = CheckinProcess.CreateCheckinProcess(this);
Trip = trip;
_passengers = passengers;
}
public static Booking SelectTrip(Trip trip, List<Passenger> passengers)
{
//Validation for trip and passengers in here
var booking = new Booking(trip, passengers);
return booking;
}
public void ChangeFlight(Flight flight)
{
// Checking is it eligible for changing flight;
Trip.ChangeFlight(journey.Id, flight);
}
public void AssignSeat(Seat seat, Passenger passenger)
{
//Validation in here
var p = _passengers.Single(s => s.Name.Equals(passenger.Name));
p.AssignSeat(seat);
}
//... Other capabilities
}
POST /api/booking/trip
var booking = Booking.SelectTrip(trip, passengers)
站在领域模型的角度,这一能力创建了一个Booking,同时还将一个可用的航班(Trip)和乘客列表添加到了Booking领域模型中, 此时的Booking就拥有了一些初始状态,同时还具备了一定的能力:分配座位(seat)和修改航班(flight)。 站在API消费者的角度,在消费者消费完毕trip这个API之后,除了能够得到一些必要的返回值,还拥有了调用下面三个API的能力:
GET api/booking/{id}
PUT api/booking/{id}/seat
PUT api/booking/{id}/flight
public class TripResource
{
private readonly IUrlHelper _urlHelper;
public TripResource(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");
}
public class TripResource
{
private readonly IUrlHelper _urlHelper;
public TripResource(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());
}
public class TripResource
{
private readonly IUrlHelper _urlHelper;
public Trip(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);
}
{
"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"
}
}
}
var homeResource = restAirlineApiNavigator.Execute();
var searchTripsCommand = homeResource.SearchTripsCommand;
searchTripsCommand.SearchCriteria = TripSearchCriteria.DefaultTripSearchCriteria();
var tripAvailabilityResource = restAirlineApiNavigator.PostCommand(searchTripsCommand);
var selectTripCommand = tripAvailabilityResource.SelectTripCommand;
selectTripCommand.Trip = tripAvailabilityResource.AvailableTrips.First();
var tripResource = restAirlineApiNavigator.PostCommand(selectTripCommand);
public interface IApiNavigator<TResource>
{
TResource Execute();
TResourceToFetch PostCommand<TResourceToFetch>(HypermediaCommand<TResourceToFetch> command);
SubApiNavigator<TTargetResource, TResource> FollowLink<TTargetResource>(
Func<TResource, Link<TTargetResource>> navigator);
}
getProducts(): Observable<ProductsResource> {
const products = this.apiNavigator
.followLink(start => start.productHome)
.followLink(product => product.products)
.execute();
return products;
}
- 相关阅读 -
点击【阅读原文】可至洞见网站查看原文&绿色字体部分的相关链接。
本文版权属ThoughtWorks公司所有,如需转载请在后台留言联系。