You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: _posts/2020-06-24-the-command-pattern.md
+20-20Lines changed: 20 additions & 20 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -26,14 +26,14 @@ Forget about whatever language or framework you’re using and try to think abou
26
26
27
27
Before we start, let’s take a moment to establish our domain.
28
28
I’m going to choose the admin dashboard because it’s pretty broadly applicable and most software projects have at least some notion of an admin dashboard.
29
-
However, **at a highlevel**, I think you could apply almost everything below to pretty much any domain you want.
29
+
However, **at a high-level**, I think you could apply almost everything below to pretty much any domain you want.
30
30
31
31
So from here on out, our “program” is an “admin dashboard”.
32
32
33
33
In my experience an admin dashboard usually serves two main roles:
34
34
35
35
1. Exposing an insider look at data to aid in debugging and monitoring.
36
-
1. Exposing actions that only certain people are able to perform under certain conditions
36
+
1. Exposing actions that only certain people can perform under certain conditions
Regardless of the naming scheme you choose, try to follow these rules:
48
48
49
49
1. Name the command after the behavior it implements _(this usually involves a verb)_
50
-
1. Do this in such a way that behaviors in the same domain live near one another _(this may evolve over time)_
50
+
1. Do this in such a way that behaviors in the same domain live near one another _(expect this to evolve)_
51
51
52
52
As domains get more mature, they often become more specialized.
53
53
54
-
Eventually your admin dashboard might have tens of commands related to subscriptions.
54
+
Eventually, your admin dashboard might have tens of commands related to subscriptions.
55
55
If this is the case, maybe you go with something like `Subscriptions::Cancel` instead.
56
56
Renaming or reorganizing shouldn't be a herculean effort.
57
57
@@ -61,35 +61,35 @@ Given the admin dashboard (program) and our desire to cancel subscriptions (dire
61
61
62
62
In this case, our task is concerned with two questions:
63
63
64
-
1. Are the conditions such that I am able to cancel the subscription?
65
-
1. If able, how do I go about cancelling the subscription?
64
+
1. Are the conditions such that I can cancel the subscription?
65
+
1. If able, how do I go about canceling the subscription?
66
66
67
-
I really like that the definition uses the words _specific_ task.
67
+
I like that the definition uses the words _specific_ task.
68
68
In other words, if it doesn’t have to do with either of these two questions, do it somewhere else :)
69
69
70
-
If we do need a piece of data in order answer either of these questions, we can pass it into our command so long as our command doesn’t know or care where it came from.
70
+
If we do need a piece of data to answer either of these questions, we can pass it into our command so long as our command doesn’t know or care where it came from.
71
71
This will make your command more re-usable and easier to test.
72
72
73
-
For instance, our `CancelSubscription` command likely needs a subscription, a date the cancellation is to go into effect, the reason it’s being cancelled, and maybe the administrator that is performing the cancellation.
73
+
For instance, our `CancelSubscription` command likely needs a subscription, a date the cancellation is to go into effect, the reason it’s being canceled, and maybe the administrator that is performing the cancellation.
74
74
75
75
### The Task: Am I Able?
76
76
77
-
Before we perform the task, we need to make sure we are able to perform the task.
77
+
Before we perform the task, we need to make sure we can perform the task.
78
78
This is where you implement your business rules.
79
79
80
-
For instance, a couple usual suspects:
80
+
For instance, a couple of usual suspects:
81
81
82
82
* Only administrators with certain permissions can cancel subscriptions
83
83
* The effective date must be between the subscription start date and the subscription end date
84
84
* A cancellation reason must be supplied and be one of several defined reasons
85
85
86
86
There are certainly other libraries out there to choose from for both Ruby and other languages.
87
87
I think a lot of this comes down to personal preference and willingness to learn new APIs.
88
-
As a heads up, Commands may go by different names such as: Interactors, Mutations, Operations, and others I’m sure.
88
+
As a heads up, Commands may go by different names such as Interactors, Mutations, Operations, and others I’m sure.
89
89
90
-
Whatever they do, they likely do something similar, but vary in syntax/DSL and feature set (e.g type coercion, checking, etc).
90
+
Whatever they do, they likely do something similar but vary in syntax/DSL and feature set (e.g type coercion, checking, etc).
91
91
92
-
When using Ruby I tend to gravitate towards `ActiveModel` (and friends) since it’s _good enough_, almost guaranteed to be present, and usually avoids any sort of holy war, letting us focus on stuff that actually matters (i.e cancelling subscriptions!).
92
+
When using Ruby I tend to gravitate towards `ActiveModel` (and friends) since it’s _good enough_, almost guaranteed to be present, and usually avoids any sort of holy war, letting us focus on stuff that matters (i.e canceling subscriptions!).
93
93
94
94
```ruby
95
95
classCancelSubscription
@@ -152,17 +152,17 @@ That’s the beauty of the command.
152
152
153
153
```ruby
154
154
defexecute
155
-
# Mark subscription as cancelled as of some date
155
+
# Mark subscription as canceled as of some date
156
156
# Maybe create a cancellation audit record documenting whodunnit/reason
157
157
# Maybe send out cancellation email?
158
-
# Maybe publish event to external system?
158
+
# Maybe publish an event to an external system?
159
159
end
160
160
```
161
161
162
162
Sure, ideally it’s expertly modeled code that checks all the boxes that you subscribe to.
163
163
In reality, it’s probably less than ideal and that’s okay.
164
164
165
-
Because we used the command pattern, folks that want to cancel a subscription don’t have to _care_ exactly how a subscription is cancelled — they just need to source the dependencies needed to perform the cancellation.
165
+
Because we used the command pattern, folks that want to cancel a subscription don’t have to _care_ exactly how a subscription is canceled — they just need to source the dependencies needed to perform the cancellation.
166
166
167
167
## Summary
168
168
@@ -217,13 +217,13 @@ This frees us up to create representations that aren't 1-1 with database models
217
217
218
218
We’re better positioned to handle new requirements because we can always make a new command variant or even compose commands with one another.
219
219
220
-
In addition, we’re able to write highvalue tests without making a single request/response (you should still write end-to-end tests, just maybe fewer than you otherwise might).
220
+
Also, we’re able to write high-value tests without making a single request/response (you should still write end-to-end tests, just maybe fewer than you otherwise might).
221
221
222
222
### Going a Step Further: Result Objects
223
223
224
224
Depending on the size and discipline within your codebase, you may want to limit the surface area exposed by your commands.
225
225
226
-
Rather than expecting folks to initialize the command and call execute on it, you might consider exposing a classlevel method that does this for you under the covers and returns a result object.
226
+
Rather than expecting folks to initialize the command and call execute on it, you might consider exposing a class-level method that does this for you under the covers and returns a result object.
227
227
228
228
While you can certainly do this in many ways, I usually make a simple object that exposes two methods: `success?` and `payload`.
229
229
@@ -254,7 +254,7 @@ def self.run(**kwargs)
254
254
end
255
255
```
256
256
257
-
Usually when doing this it’s because I’m exposing something that might be used by another team and I want to control their access to the internals.
257
+
Usually, when doing this it’s because I’m exposing something that might be used by another team and I want to control their access to the internals.
258
258
259
259
This usually means taking extra care to ensure that both the arguments into the command and the result’s payload are POROs.
0 commit comments