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
+81-46Lines changed: 81 additions & 46 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,16 +4,14 @@ author: Tony Schneider
4
4
title : The Command Pattern
5
5
date : 2020-06-24
6
6
tags : software
7
-
published: false
8
7
---
9
8
10
9
If you google the word “command”, you’ll eventually find a definition that’s something to the effect of:
11
10
12
11
> A command is a **directive** to a computer **program** to perform a specific **task**.
13
12
14
-
In other words, a command is a way to encapsulate behavior.
15
-
16
-
Ok, so now that we got all the super generic stuff out of the way, let’s try to get more specific than “program”, “directive” and “task”.
13
+
This definition is super generic because the command pattern is very broadly applicable.
14
+
Let's start by defining a "program", "directive" and "task" to paint a more concrete picture.
17
15
18
16
## The Program
19
17
@@ -29,7 +27,7 @@ Before we start, let’s take a moment to establish our domain.
29
27
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.
30
28
However, **at a high-level**, I think you could apply almost everything below to pretty much any domain you want.
31
29
32
-
So from here on out, our “program” is an “admin dashboard”.
30
+
So from here on out, our “program” is an admin dashboard.
33
31
34
32
In my experience an admin dashboard usually serves two main roles:
35
33
@@ -44,6 +42,7 @@ One example of a directive in our fictional admin dashboard might be the ability
44
42
45
43
Naming is certainly hard, but I think we have this one under control for now.
46
44
Let’s call our command `CancelSubscription`.
45
+
My preference is to put place our commands in a namespace for collocation, making it `Commands::CancelSubscription`.
47
46
48
47
Regardless of the naming scheme you choose, try to follow these rules:
49
48
@@ -53,7 +52,8 @@ Regardless of the naming scheme you choose, try to follow these rules:
53
52
As domains get more mature, they often become more specialized.
54
53
55
54
Eventually, your admin dashboard might have tens of commands related to subscriptions.
56
-
If this is the case, maybe you go with something like `Subscriptions::Cancel` instead.
55
+
If this is the case, maybe you go with something like `Commands::Subscriptions::Cancel` instead.
56
+
57
57
Renaming or reorganizing shouldn't be a herculean effort.
58
58
59
59
## The Task
@@ -84,49 +84,54 @@ For instance, a couple of usual suspects:
84
84
* The effective date must be between the subscription start date and the subscription end date
85
85
* A cancellation reason must be supplied and be one of several defined reasons
86
86
87
-
There are certainly other libraries out there to choose from for both Ruby and other languages.
88
-
I think a lot of this comes down to personal preference and willingness to learn new APIs.
89
-
As a heads up, Commands may go by different names such as Interactors, Mutations, Operations, and others I’m sure.
87
+
There are plenty of command libraries out there to choose from for both Ruby and other languages.
88
+
The choice comes down to personal preference and willingness to learn new APIs.
89
+
As a heads up, Commands may go by different names such as Interactors, Mutations, Operations, ServiceObjects and others I’m sure.
90
90
91
91
Whatever they do, they likely do something similar but vary in syntax/DSL and feature set (e.g type coercion, checking, etc).
92
+
I've found the conversation around this terminology to be largely a distraction.
92
93
93
94
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!).
errors.add(:administrator, "does not have permission to cancel subscriptions")
124
+
end
122
125
end
123
126
end
124
127
end
125
128
```
126
129
127
-
Including `ActiveModel::Validations` defines a`valid?` that returns `true` or `false`.
130
+
Including `ActiveModel::Validations` defines an instance method called`valid?` that returns `true` or `false`.
128
131
If `valid?` returns `false`, it populates the `errors` on the `CancelSubscription` instance.
129
132
133
+
We only want to execute our command when it's valid.
134
+
130
135
In the case of our admin dashboard, we’d probably want to use these errors to re-render an invalid form or construct a JSON payload.
131
136
132
137
#### Required
@@ -146,12 +151,16 @@ Worth noting that as written, if the user doesn’t make a selection, the comman
146
151
147
152
### The Task: How do I?
148
153
149
-
So, assuming we got past our validations, how does one cancel a subscription?
154
+
Here's a couples rules I try to follow:
150
155
151
-
The good part is that it **_really doesn’t matter_** so much. :rainbow:
152
-
That’s the beauty of the command.
156
+
* Implement an instance method called `execute` (`call` is also a popular choice, but I don't use it because it makes me think of `block.call`)
157
+
* The command doesn't expose instance methods that take arguments (this means you need to pass something smarter into the constructor)
158
+
159
+
So, assuming we got past our validations, how does one cancel a subscription?
153
160
154
161
```ruby
162
+
# In our command
163
+
#
155
164
defexecute
156
165
# Mark subscription as canceled as of some date
157
166
# Maybe create a cancellation audit record documenting whodunnit/reason
@@ -160,20 +169,32 @@ def execute
160
169
end
161
170
```
162
171
163
-
Sure, ideally it’s expertly modeled code that checks all the boxes that you subscribe to.
164
-
In reality, it’s probably less than ideal and that’s okay.
172
+
Given this is a fictional example, I don't know.
173
+
But the point is, it doesn't matter.
174
+
You've built a home for it.
165
175
166
-
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.
176
+
When we're in the command, we care deeply about the implementation details of how a subscription is canceled.
177
+
We do whatever we have to do to achieve that goal.
178
+
From the outside of the command, once we have a reliable implementation, we literally can stop caring (until we are forced to :sweat_smile:)
167
179
168
-
## Summary
180
+
In other words, we've **encapsulated** the behavior of canceling a subscription.
169
181
170
-
### Fat Models or Fat Controllers?
182
+
That’s the beauty of the command.
183
+
They free us from implementation detail, freeing us to talk and think at a higher level.
184
+
185
+
Sure, ideally it’s expertly modeled code that checks all the boxes that you passionately subscribe to.
186
+
In reality, it’s probably the way it _has to work_ in today's system and that's okay.
187
+
Ideally with the command as your boundary and a reasonable test harness, you're in a good position to make improvements when the time comes.
171
188
172
-
How about neither? :sweat_smile:
189
+
## Usage
173
190
174
191
For example, let’s imagine we’re exposing the `CancelSubscription` command as a form in our admin dashboard.
175
192
176
-
We might have a controller that looks something like this:
193
+
Form objects are a nice use case for the command pattern because they fit the mold of our _task_ perfectly.
194
+
195
+
If the form (command) is valid, we want to submit (execute) the form (command).
196
+
197
+
Our controller might look something like this:
177
198
178
199
```ruby
179
200
moduleAdminDashboard
@@ -218,7 +239,7 @@ This frees us up to create representations that aren't 1-1 with database models
218
239
219
240
We’re better positioned to handle new requirements because we can always make a new command variant or even compose commands with one another.
220
241
221
-
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).
242
+
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:sweat_smile:)
222
243
223
244
### Going a Step Further: Result Objects
224
245
@@ -259,6 +280,20 @@ Usually, when doing this it’s because I’m exposing something that might be u
259
280
260
281
This usually means taking extra care to ensure that both the arguments into the command and the result’s payload are POROs.
261
282
283
+
### Going a Step Further: Command Composition
284
+
285
+
In a codebase that frequently reuses commands outside of forms, or performs the same action from many perspectives, you might consider composing commands.
286
+
287
+
In this case maybe you have two forms that orchestrate the cancelation of a subscription.
After some minor adjustment to hoist up any admin specific behavior, both of these form objects could call our underlying `Commands::CancelSubscription`) command.
293
+
262
294
—-
263
295
264
-
So, in summary, just give the computer program the directives it needs to perform some tasks and you’ll be fine.
296
+
In summary, the command pattern is an extremely forgiving and broadly applicable method of encapsulating behavior.
297
+
298
+
Identify your domain (program), define a directive (name) and implement a specific task (command).
0 commit comments