Skip to content

Commit e850aef

Browse files
authored
Merge pull request #25 from tonywok/ts-command-pattern-thoughts
Command pattern post
2 parents 9ccc826 + ea0eb65 commit e850aef

File tree

2 files changed

+269
-0
lines changed

2 files changed

+269
-0
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
---
2+
layout: post
3+
author: Tony Schneider
4+
title : The Command Pattern
5+
date : 2020-06-24
6+
tags : software
7+
---
8+
9+
If you google the word “command”, you’ll eventually find a definition that’s something to the effect of:
10+
11+
> A command is a **directive** to a computer **program** to perform a specific **task**.
12+
13+
In other words, a command is a way to encapsulate behavior.
14+
15+
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”.
16+
17+
## The Program
18+
19+
No matter what you’re working on, you’re working in some sort of _domain_.
20+
It could be anything; a game, a specific menu inside that game, an insurance company, or maybe you’re just implementing some forms on an admin dashboard.
21+
22+
After all, from 10,000 feet, no matter what software you’re building, it pretty much fits into the mold of a computer program accepting directives that in return perform tasks.
23+
24+
If you think it doesn’t, I challenge you to step outside the specifics of whatever you’re working on.
25+
Forget about whatever language or framework you’re using and try to think about the problem you’re solving.
26+
27+
Before we start, let’s take a moment to establish our domain.
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 high level**, I think you could apply almost everything below to pretty much any domain you want.
30+
31+
So from here on out, our “program” is an “admin dashboard”.
32+
33+
In my experience an admin dashboard usually serves two main roles:
34+
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
37+
38+
Let’s focus on the second for now.
39+
40+
## The Directive
41+
42+
One example of a directive in our fictional admin dashboard might be the ability to cancel a user’s subscription.
43+
44+
Naming is certainly hard, but I think we have this one under control for now.
45+
Let’s call our command `CancelSubscription`.
46+
47+
Regardless of the naming scheme you choose, try to follow these rules:
48+
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)_
51+
52+
As domains get more mature, they often become more specialized.
53+
54+
Eventually your admin dashboard might have tens of commands related to subscriptions.
55+
If this is the case, maybe you go with something like `Subscriptions::Cancel` instead.
56+
Renaming or reorganizing shouldn't be a herculean effort.
57+
58+
## The Task
59+
60+
Given the admin dashboard (program) and our desire to cancel subscriptions (directive), we need to define our _specific_ task.
61+
62+
In this case, our task is concerned with two questions:
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?
66+
67+
I really like that the definition uses the words _specific_ task.
68+
In other words, if it doesn’t have to do with either of these two questions, do it somewhere else :)
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.
71+
This will make your command more re-usable and easier to test.
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.
74+
75+
### The Task: Am I Able?
76+
77+
Before we perform the task, we need to make sure we are able to perform the task.
78+
This is where you implement your business rules.
79+
80+
For instance, a couple usual suspects:
81+
82+
* Only administrators with certain permissions can cancel subscriptions
83+
* The effective date must be between the subscription start date and the subscription end date
84+
* A cancellation reason must be supplied and be one of several defined reasons
85+
86+
There are certainly other libraries out there to choose from for both Ruby and other languages.
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.
89+
90+
Whatever they do, they likely do something similar, but vary in syntax/DSL and feature set (e.g type coercion, checking, etc).
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!).
93+
94+
```ruby
95+
class CancelSubscription
96+
include ActiveModel::Validations
97+
98+
attr_reader :subscription,
99+
:administrator,
100+
:effective_date,
101+
:reason
102+
103+
validates :subscription, presence: true
104+
validates :administrator, presence: true
105+
validates :effective_date, presence: true
106+
validates :reason, presence: true, inclusion: { in: Subscription::CancellationReasons::ALL }
107+
validate :authorized_administrator
108+
109+
def initialize(subscription:, administrator:, effective_date: nil, reason: nil)
110+
@subscription = subscription
111+
@adminstrator = administrator
112+
@effective_date = effective_date
113+
@reason = reason
114+
end
115+
116+
private
117+
118+
def administrator_authorized
119+
unless can_cancel_subscription?(administrator, subscription)
120+
errors.add(:administrator, "does not have permission to cancel subscriptions")
121+
end
122+
end
123+
end
124+
```
125+
126+
Including `ActiveModel::Validations` defines a `valid?` that returns `true` or `false`.
127+
If `valid?` returns `false`, it populates the `errors` on the `CancelSubscription` instance.
128+
129+
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.
130+
131+
#### Required
132+
133+
In the example above, I used keyword arguments to indicate that `subscription` and `administrator` are required.
134+
135+
Without these two things, we’re not even going to try to perform our task.
136+
If this happens, something else must be wrong.
137+
138+
#### Optional
139+
140+
Similarly, I indicated that `effective_date` and `reason` are optional by having their values default to `nil`.
141+
142+
I have them as optional because they are likely set by the administrator’s selection in a form.
143+
In this example, I defaulted them to `nil`, but in real life, there might be a more reasonable default.
144+
Worth noting that as written, if the user doesn’t make a selection, the command will not execute due to our validations.
145+
146+
### The Task: How do I?
147+
148+
So, assuming we got past our validations, how does one cancel a subscription?
149+
150+
The good part is that it **_really doesn’t matter_** so much. :rainbow:
151+
That’s the beauty of the command.
152+
153+
```ruby
154+
def execute
155+
# Mark subscription as cancelled as of some date
156+
# Maybe create a cancellation audit record documenting whodunnit/reason
157+
# Maybe send out cancellation email?
158+
# Maybe publish event to external system?
159+
end
160+
```
161+
162+
Sure, ideally it’s expertly modeled code that checks all the boxes that you subscribe to.
163+
In reality, it’s probably less than ideal and that’s okay.
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.
166+
167+
## Summary
168+
169+
### Fat Models or Fat Controllers?
170+
171+
How about neither? :sweat_smile:
172+
173+
For example, let’s imagine we’re exposing the `CancelSubscription` command as a form in our admin dashboard.
174+
175+
We might have a controller that looks something like this:
176+
177+
```ruby
178+
module AdminDashboard
179+
module Subscriptions
180+
class CancellationsController < AdminDashboardController
181+
before_action :setup_subscription
182+
183+
def new
184+
@command = Commands::CancelSubscription.new(
185+
subscription: @subscription,
186+
administrator: current_user
187+
)
188+
end
189+
190+
def create
191+
@command = Commands::CancelSubscription.new(
192+
subscription: @subscription,
193+
administrator: current_user,
194+
**cancellation_form_params
195+
)
196+
197+
if @command.valid?
198+
@command.execute
199+
else
200+
render :new
201+
end
202+
end
203+
end
204+
end
205+
end
206+
```
207+
208+
We let the controller deal with authentication, sessions, parameter parsing, and orchestrating the usage of our commands.
209+
210+
We let the database models handle things that have to do with persistence and data integrity.
211+
212+
Our command owns the business rules.
213+
Because the command knows the calling context, we avoid the problem of bestowing behavior on all consumers of a database model.
214+
215+
The layer between our controllers and our database models decouples us from our database representation.
216+
This frees us up to create representations that aren't 1-1 with database models (avoiding any nested attributes shenanigans).
217+
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+
220+
In addition, 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+
222+
### Going a Step Further: Result Objects
223+
224+
Depending on the size and discipline within your codebase, you may want to limit the surface area exposed by your commands.
225+
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+
228+
While you can certainly do this in many ways, I usually make a simple object that exposes two methods: `success?` and `payload`.
229+
230+
Here’s a starting point that you can adapt to your own needs:
231+
232+
```ruby
233+
# As a Caller of the command
234+
#
235+
result = CancelSubscription.run(
236+
subscription: subscription,
237+
administrator: administrator,
238+
effective_date: Time.zone.today,
239+
reason: "just cuz"
240+
)
241+
result.success? # true/false
242+
result.payload # An interface to the external world
243+
244+
# In the command
245+
#
246+
def self.run(**kwargs)
247+
command = new(**kwargs)
248+
if command.valid?
249+
payload = command.execute
250+
Result.new(success: true, payload: payload)
251+
else
252+
Result.new(success: true, payload: command.errors)
253+
end
254+
end
255+
```
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.
258+
259+
This usually means taking extra care to ensure that both the arguments into the command and the result’s payload are POROs.
260+
261+
—-
262+
263+
So, in summary, just give the computer program the directives it needs to perform some tasks and you’ll be fine.

_site/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ <h2>
4545
<img class="emoji" title=":computer:" alt=":computer:" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4bb.png" height="20" width="20"> Software</h2>
4646
<ul>
4747
<li>
48+
<span class="post-meta mono">2020-06-24</span>
49+
<a href="/2020/06/24/the-command-pattern.html">
50+
The Command Pattern
51+
</a>
52+
</li>
53+
<li>
4854
<span class="post-meta mono">2020-06-09</span>
4955
<a href="/2020/06/09/consolidating-static-lists.html">
5056
Consolidating Static Lists

0 commit comments

Comments
 (0)