State Management Using Protocol-Oriented Programming + Generics

Boris Bugor
Better Programming
Published in
4 min readJul 22, 2023

--

Photo by Lukas Tennie on Unsplash

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:

  • You ordered a product on Amazon, and your order is assigned the status Initiated
  • After the purchase, your product enters the state of the need for payment (ReadyForPayment)
  • After payment, it enters the processing state (Pending)
  • After checking the availability of the goods, the fact of payment, and transferring it to the delivery service, your order enters the Delivering status
  • After delivery of the goods, the status changes to Delivered, with the possibility of returning the goods within 30 days
  • After 30 days, the status of the product changes to Finished, 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:

  • After ordering the goods (status ReadyForPayment), payment for the goods was not made within 10 minutes, and the order was put into the Cancelled status
  • After processing the order (status Pending), it turned out that the product was out of stock, which is why the status of the order changed to CancelledWithRefunded
  • After exceeding the pre-announced delivery time, you requested a refund, and the order status changed to CancelledWithRefunded
  • After receiving the order, the product turned out to be of inadequate quality, and you also requested a refund in exchange for returning the product. The order status changed to 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 here.

And finally, don’t hesitate to contact me on Twitter if you have any questions. Also, you can always buy me a coffee.

--

--

Senior iOS Developer | Co-founder of VideoEditor: Reels & Stories | Founder of Flow: Diary, Calendar, Gallery | #ObjC | #Swift | #UIKit | #SwiftUI