Skip to content

Commit 31693c6

Browse files
authored
Some editing inspired by Andrew’s feedback
Likely more to come
1 parent 72c0bbd commit 31693c6

File tree

1 file changed

+81
-46
lines changed

1 file changed

+81
-46
lines changed

_posts/2020-06-24-the-command-pattern.md

Lines changed: 81 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ author: Tony Schneider
44
title : The Command Pattern
55
date : 2020-06-24
66
tags : software
7-
published: false
87
---
98

109
If you google the word “command”, you’ll eventually find a definition that’s something to the effect of:
1110

1211
> A command is a **directive** to a computer **program** to perform a specific **task**.
1312
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.
1715

1816
## The Program
1917

@@ -29,7 +27,7 @@ Before we start, let’s take a moment to establish our domain.
2927
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.
3028
However, **at a high-level**, I think you could apply almost everything below to pretty much any domain you want.
3129

32-
So from here on out, our “program” is an admin dashboard.
30+
So from here on out, our “program” is an admin dashboard.
3331

3432
In my experience an admin dashboard usually serves two main roles:
3533

@@ -44,6 +42,7 @@ One example of a directive in our fictional admin dashboard might be the ability
4442

4543
Naming is certainly hard, but I think we have this one under control for now.
4644
Let’s call our command `CancelSubscription`.
45+
My preference is to put place our commands in a namespace for collocation, making it `Commands::CancelSubscription`.
4746

4847
Regardless of the naming scheme you choose, try to follow these rules:
4948

@@ -53,7 +52,8 @@ Regardless of the naming scheme you choose, try to follow these rules:
5352
As domains get more mature, they often become more specialized.
5453

5554
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+
5757
Renaming or reorganizing shouldn't be a herculean effort.
5858

5959
## The Task
@@ -84,49 +84,54 @@ For instance, a couple of usual suspects:
8484
* The effective date must be between the subscription start date and the subscription end date
8585
* A cancellation reason must be supplied and be one of several defined reasons
8686

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.
9090

9191
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.
9293

9394
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!).
9495

9596
```ruby
96-
class CancelSubscription
97-
include ActiveModel::Validations
98-
99-
attr_reader :subscription,
100-
:administrator,
101-
:effective_date,
102-
:reason
103-
104-
validates :subscription, presence: true
105-
validates :administrator, presence: true
106-
validates :effective_date, presence: true
107-
validates :reason, presence: true, inclusion: { in: Subscription::CancellationReasons::ALL }
108-
validate :authorized_administrator
109-
110-
def initialize(subscription:, administrator:, effective_date: nil, reason: nil)
111-
@subscription = subscription
112-
@adminstrator = administrator
113-
@effective_date = effective_date
114-
@reason = reason
115-
end
97+
module Commands
98+
class CancelSubscription
99+
include ActiveModel::Validations
100+
101+
attr_reader :subscription
102+
attr_reader :administrator
103+
attr_reader :effective_date
104+
attr_reader :reason
105+
106+
validates :subscription, presence: true
107+
validates :administrator, presence: true
108+
validates :effective_date, presence: true
109+
validates :reason, presence: true, inclusion: { in: Subscription::CancellationReasons::ALL }
110+
validate :authorized_administrator
111+
112+
def initialize(subscription:, administrator:, effective_date: nil, reason: nil)
113+
@subscription = subscription
114+
@adminstrator = administrator
115+
@effective_date = effective_date
116+
@reason = reason
117+
end
116118

117-
private
119+
private
118120

119-
def administrator_authorized
120-
unless can_cancel_subscription?(administrator, subscription)
121-
errors.add(:administrator, "does not have permission to cancel subscriptions")
121+
def administrator_authorized
122+
unless can_cancel_subscription?(administrator, subscription)
123+
errors.add(:administrator, "does not have permission to cancel subscriptions")
124+
end
122125
end
123126
end
124127
end
125128
```
126129

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`.
128131
If `valid?` returns `false`, it populates the `errors` on the `CancelSubscription` instance.
129132

133+
We only want to execute our command when it's valid.
134+
130135
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.
131136

132137
#### Required
@@ -146,12 +151,16 @@ Worth noting that as written, if the user doesn’t make a selection, the comman
146151

147152
### The Task: How do I?
148153

149-
So, assuming we got past our validations, how does one cancel a subscription?
154+
Here's a couples rules I try to follow:
150155

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?
153160

154161
```ruby
162+
# In our command
163+
#
155164
def execute
156165
# Mark subscription as canceled as of some date
157166
# Maybe create a cancellation audit record documenting whodunnit/reason
@@ -160,20 +169,32 @@ def execute
160169
end
161170
```
162171

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.
165175

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:)
167179

168-
## Summary
180+
In other words, we've **encapsulated** the behavior of canceling a subscription.
169181

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.
171188

172-
How about neither? :sweat_smile:
189+
## Usage
173190

174191
For example, let’s imagine we’re exposing the `CancelSubscription` command as a form in our admin dashboard.
175192

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:
177198

178199
```ruby
179200
module AdminDashboard
@@ -218,7 +239,7 @@ This frees us up to create representations that aren't 1-1 with database models
218239

219240
We’re better positioned to handle new requirements because we can always make a new command variant or even compose commands with one another.
220241

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:)
222243

223244
### Going a Step Further: Result Objects
224245

@@ -259,6 +280,20 @@ Usually, when doing this it’s because I’m exposing something that might be u
259280

260281
This usually means taking extra care to ensure that both the arguments into the command and the result’s payload are POROs.
261282

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.
288+
289+
* Admin cancels subscription (e.g `Forms::Admin::CancelSubscription`)
290+
* Customer cancels subscription (e.g `Forms::Customer::CancelSubscription`)
291+
292+
After some minor adjustment to hoist up any admin specific behavior, both of these form objects could call our underlying `Commands::CancelSubscription`) command.
293+
262294
—-
263295

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).
299+

0 commit comments

Comments
 (0)