In this article, I would like to show one of the most convenient ways to manage the state of an object using a protocol-oriented approach and generics.
This approach is widely used in marketplaces, the banking environment, the service sector, and so on, and assumes that for each state of the object, it is predetermined into which state it is allowed to convert.
Consider an example:
Initiated
ReadyForPayment
.Pending
.Delivering
statusDelivered
, with the possibility of returning the goods within 30 daysFinished
, and the possibility of returning the goods ceases.
In addition to the above positive cases, there are also possible pre-foreseen negative cases, such as:
ReadyForPayment
), payment for the goods was not made within 10 minutes, and the order was put into the Cancelled
status;Pending
), it turned out that the product was out of stock, which is why the status of the order changed to CancelledWithRefunded
;CancelledWithRefunded
;Refunded
.
In more detail, a possible status change map is shown in the figure:
The designed system is required to support status routing and the ability to change / scale at the request of the business.
Let’s start with the most important unit of the system being designed:
struct Order<T> {}
As a generic parameter, we will use the order status. Let’s create for each order status, by the requirements described above, its own object:
struct Initiated {}
struct ReadyForPayment {}
struct Pending {}
struct Delivering {}
struct Delivered {}
struct Finished {}
struct Cancelled {}
struct CancelledWithRefunded {}
struct Refunded {}
The cancellation/refund capability implementation will be done using the “Strategy” design pattern. This will reduce the effort to change the statuses for which the business provides the possibility of cancellation/refusal/return of goods.
Let’s create our own protocol for each of the negative scenarios:
protocol Cancellable {}
protocol CancellableWithRefund {}
protocol Refundable {}
Taking into account the current statement of the problem, we will provide the possibility of canceling/refusing/returning goods for previously created statuses:
struct Initiated {}
struct ReadyForPayment: Cancellable {}
struct Pending: CancellableWithRefund {}
struct Delivering: CancellableWithRefund {}
struct Delivered: Refundable {}
struct Finished {}
struct Cancelled {}
struct CancelledWithRefund {}
struct Refunded {}
This completes the design of the system skeleton. Let’s move on to designing status visibility zones. The generic component of the order will help us with this.
1. According to the task statement, the Initiated
status can only be changed to ReadyForPayment
:
extension Order where T == Initiated {
var readyForPayment: Order<ReadyForPayment> {
Order<ReadyForPayment>()
}
}
2. ReadyForPayment
status changes to Pending
after successful payment:
extension Order where T == ReadyForPayment {
var pending: Order<Pending> {
Order<Pending>()
}
}
3. Pending
status after checking the availability of goods, the fact of payment and transfer it to the delivery service changes to Delivering
:
extension Order where T == Pending {
var delivering: Order<Delivering> {
Order<Delivering>()
}
}
4. The status of Delivering
changes to Delivered
after delivery of the goods:
extension Order where T == Delivering {
var delivered: Order<Delivered> {
Order<Delivered>()
}
}
5. Delivered
status after 30 days excision changes to Finished
:
extension Order where T == Delivered {
var finished: Order<Finished> {
Order<Finished>()
}
}
6. Statuses that provide for Cancellable
in advance can be changed to Cancelled
:
extension Order where T: Cancellable {
var canceled: Order<Cancelled> {
Order<Cancelled>()
}
}
7. CancellableWithRefund
statuses can be changed to CancelledWithRefund
:
extension Order where T: CancellableWithRefund {
var canceledWithRefund: Order<CancelledWithRefund> {
Order<CancelledWithRefund>()
}
}
8. Statuses that provide for the possibility of returning Refundable
goods in advance can be changed to Refunded
:
extension Order where T: Refundable {
var refunded: Order<Refunded> {
Order<Refunded>()
}
}
The system is ready. Now, we can use the constructor to collect any movement of statuses within predetermined possibilities.
For example, an order for which payment has not gone through moves like this:
let cancelled = Order<Initiated>().readyForPayment.canceled
The order for which it was delivered to the client moves like this:
let finished = Order<Initiated>().readyForPayment.pending.delivering.delivered
One of the important aspects: at each stage, the order movement occurs according to a pre-implemented routing. You cannot move an order from the Pending
status to the Finished
status bypassing all other intermediate statuses (if this was not allowed by your transition map).
Project sources can be found
Don’t hesitate to contact me on