Skip to content

Commit f6984e6

Browse files
committed
Publish post "Example: state change form"
1 parent 9ab3cd9 commit f6984e6

File tree

1 file changed

+110
-0
lines changed

1 file changed

+110
-0
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
---
2+
title: "Example: state change form"
3+
date: 2019-06-18 22:00:00 +0200
4+
categories: oop ruby
5+
---
6+
7+
Here I'll explore some existing code where a change gets introduced and ultimately how a deeper understanding leads to a new realization and a better implementation.
8+
9+
The main concept in this example is a _Shipment_. A _Shipment_ is identified by a tracking number and also has a state, like whether it is in transit. This is the type of data you would see when you search for a parcel on the websites of UPS or FedEx.
10+
11+
The example concerns the following piece of code:
12+
13+
```ruby
14+
module ShipmentViewHelper
15+
STATE_OPTIONS = [
16+
["Created", Shipment::States::CREATED],
17+
["Booked", Shipment::States::BOOKED],
18+
["In transit", Shipment::States::IN_TRANSIT],
19+
["Delivered", Shipment::States::DELIVERED],
20+
["Cancelled", Shipment::States::CANCELLED],
21+
]
22+
end
23+
24+
class StateChangeForm
25+
include ActiveModel::Model
26+
27+
attr_accessor :state, :tracking_number, :comment
28+
29+
def available_state_options
30+
all_options = ShipmentViewHelper::STATE_OPTIONS.dup
31+
all_options.reject! { |(_, option_for_state)| option_for_state == Shipment::States::CREATED } unless state == Shipment::States::CREATED
32+
all_options
33+
end
34+
end
35+
```
36+
37+
`StateChangeForm` is a basic form model that holds various pieces of data related to a Shipment which is used to update the state of the shipment.
38+
The interesting part is the method `StateChangeForm#available_state_options`. That method is used to list the available state values that the shipment can change to. The `CREATED` state is treated differently because it should not be able to go back to that state once it leaves it.
39+
40+
# Change
41+
42+
A new state is introduced: `CONFIRMED`. It is similar to the `CREATED` state in that it should not be possible to go back to the `CONFIRMED` state once it has left that state. A quick way to accomplish this:
43+
44+
```ruby
45+
module ShipmentViewHelper
46+
STATE_OPTIONS = [
47+
["Created", Shipment::States::CREATED],
48+
["Confirmed", Shipment::States::CONFIRMED],
49+
["Booked", Shipment::States::BOOKED],
50+
["In transit", Shipment::States::IN_TRANSIT],
51+
["Delivered", Shipment::States::DELIVERED],
52+
["Cancelled", Shipment::States::CANCELLED],
53+
]
54+
end
55+
56+
class StateChangeForm
57+
include ActiveModel::Model
58+
59+
attr_accessor :state, :tracking_number, :comment
60+
61+
def available_state_options
62+
all_options = ShipmentViewHelper::STATE_OPTIONS.dup
63+
all_options.reject! { |(_, option_for_state)| option_for_state == Shipment::States::CREATED } unless state == Shipment::States::CREATED
64+
all_options.reject! { |(_, option_for_state)| option_for_state == Shipment::States::CONFIRMED } unless [Shipment::States::CREATED, Shipment::States::CONFIRMED].include?(state)
65+
all_options
66+
end
67+
end
68+
```
69+
70+
# Understand and refactor
71+
72+
Let's go a bit deeper and try to understand what the underlying idea is for `StateChangeForm#available_state_options`:
73+
74+
- if in state `CREATED`, the shipment can change to any state
75+
- if in state `CONFIRMED`, the shipment can change to any state except for `CREATED`
76+
- for the other states, the shipment can change to any state except for `CREATED` and `CONFIRMED`
77+
78+
This hints at the fact that depending on the state, we want to filter out (aka reject) some states.
79+
This simplifies the implementation a lot:
80+
81+
```ruby
82+
module ShipmentViewHelper
83+
STATE_OPTIONS = [
84+
# ... same as the previous step
85+
]
86+
end
87+
88+
class StateChangeForm
89+
include ActiveModel::Model
90+
91+
attr_accessor :state, :tracking_number, :comment
92+
93+
EXCLUDED_STATES_MAPPING = {
94+
Shipment::States::CREATED => [],
95+
Shipment::States::CONFIRMED => [Shipment::States::CREATED],
96+
}
97+
EXCLUDED_STATES_MAPPING.default = [Shipment::States::CREATED, Shipment::States::CONFIRMED]
98+
99+
def available_state_options
100+
ShipmentViewHelper::STATE_OPTIONS
101+
.reject { |(_, option_for_state)| excluded_states.include?(option_for_state) }
102+
end
103+
104+
def excluded_states
105+
EXCLUDED_STATES_MAPPING.fetch(state)
106+
end
107+
end
108+
```
109+
110+
*This could be further optimized to basically a hash lookup but that's beside the point.*

0 commit comments

Comments
 (0)