Skip to content

Commit 4978243

Browse files
committed
update README
1 parent 927e584 commit 4978243

File tree

2 files changed

+270
-3
lines changed

2 files changed

+270
-3
lines changed

README.md

+269-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,275 @@
11
# Interactify
22

3-
TODO: Delete this and the text below, and describe your gem
3+
Interactors are a great way to encapsulate business logic in a Rails application.
4+
However, sometimes in complex interactor chains, the complex debugging happens at one level up from your easy to read and test interactors.
45

5-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/interactify`. To experiment with that code, run `bin/console` for an interactive prompt.
6+
Interactify wraps the interactor and interactor-contract gem and provides additional functionality making chaining and understanding interactor chains easier.
7+
8+
### Syntactic Sugar
9+
10+
```ruby
11+
# before
12+
13+
class LoadOrder
14+
include Interactor
15+
include Interactor::Contracts
16+
17+
expects do
18+
required(:id).filled
19+
end
20+
21+
promises do
22+
required(:order).filled
23+
end
24+
25+
26+
def call
27+
context.order = Order.find(context.id)
28+
end
29+
end
30+
31+
# after
32+
class LoadOrder
33+
include Interactify
34+
35+
expect :id
36+
promise :order
37+
38+
def call
39+
context.order = Order.find(id)
40+
end
41+
end
42+
```
43+
44+
45+
### Lambdas
46+
47+
With vanilla interactors, it's not possible to use lambdas in organizers, and sometimes we only want a lambda.
48+
So we added support.
49+
50+
```ruby
51+
organize LoadOrder, ->(context) { context.order = context.order.decorate }
52+
53+
organize \
54+
Thing1,
55+
->(c){ byebug if c.order.nil? },
56+
Thing2
57+
```
58+
```
59+
60+
### Each/Iteration
61+
62+
Sometimes we want an interactor for each item in a collection.
63+
But it gets unwieldy.
64+
It was complex procedural code and is now broken into neat SRP classes (Single Responsibility Principle).
65+
But there is still boilerplate and jumping around between files to follow the orchestration.
66+
It's easy to get lost in the orchestration code that occurs across say 7 or 8 files.
67+
68+
So the complexity problem is just moved to the gaps between the classes and files.
69+
We gain things like `EachOrder`, or `EachProduct` interactors.
70+
71+
Less obvious, still there.
72+
73+
By using `Interactify.each` we can keep the orchestration code in one place.
74+
We get slightly more complex organizers, but a simpler mental model of organizer as orchestrator and SRP interactors.
75+
76+
```ruby
77+
# before
78+
class OuterOrganizer
79+
# ... boilerplate ...
80+
organize SetupStep, LoadOrders, DoSomethingWithOrders
81+
end
82+
83+
class LoadOrders
84+
# ... boilerplate ...
85+
def call
86+
context.orders = context.ids.map do |id|
87+
LoadOrder.call(id: id).order
88+
end
89+
end
90+
end
91+
92+
class LoadOrder
93+
# ... boilerplate ...
94+
def call
95+
# ...
96+
end
97+
end
98+
99+
class DoSomethingWithOrders
100+
# ... boilerplate ...
101+
def call
102+
context.orders.each do |order|
103+
DoSomethingWithOrder.call(order: order)
104+
end
105+
end
106+
end
107+
108+
class DoSomethingWithOrder
109+
# ... boilerplate ...
110+
def call
111+
# ...
112+
end
113+
end
114+
```
115+
116+
117+
```ruby
118+
# after
119+
class OuterOrganizer
120+
# ... boilerplate ...
121+
organize \
122+
SetupStep,
123+
self.each(:ids,
124+
LoadOrder,
125+
->(c){ byebug if c.order.nil? },
126+
DoSomethingWithOrder
127+
)
128+
end
129+
130+
class LoadOrder
131+
# ... boilerplate ...
132+
def call
133+
# ...
134+
end
135+
end
136+
137+
138+
class DoSomethingWithOrder
139+
# ... boilerplate ...
140+
def call
141+
# ...
142+
end
143+
end
144+
```
145+
146+
### Conditionals (if/else)
147+
148+
Along the same lines of each/iteration. We sometimes have to 'break the chain' with interactors just to conditionally call one interactor chain path or another.
149+
150+
The same mental model problem applies. We have to jump around between files to follow the orchestration.
151+
152+
```ruby
153+
# before
154+
class OuterThing
155+
# ... boilerplate ...
156+
organize SetupStep, InnerThing
157+
end
158+
159+
class InnerThing
160+
# ... boilerplate ...
161+
def call
162+
if context.thing == 'a'
163+
DoThingA.call(context)
164+
else
165+
DoThingB.call(context)
166+
end
167+
end
168+
end
169+
170+
class DoThingA
171+
# ... boilerplate ...
172+
def call
173+
# ...
174+
end
175+
end
176+
177+
class DoThingB
178+
# ... boilerplate ...
179+
def call
180+
# ...
181+
end
182+
end
183+
```
184+
185+
186+
```ruby
187+
# after
188+
class OuterThing
189+
# ... boilerplate ...
190+
organize \
191+
SetupStep,
192+
self.if(->(c){ c.thing == 'a' }, DoThingA, DoThingB),
193+
end
194+
195+
```
196+
197+
### More Conditionals
198+
199+
```ruby
200+
class OuterThing
201+
# ... boilerplate ...
202+
organize \
203+
self.if(:key_set_on_context, DoThingA, DoThingB),
204+
AfterBothCases
205+
end
206+
```
207+
208+
### Simple chains
209+
Sometimes you want an organizer that just calls a few interactors in a row.
210+
You may want to create these dynamically at load time, or you may just want to keep the orchestration in one place.
211+
212+
`self.chain` is a simple way to do this.
213+
214+
```ruby
215+
class SomeOrganizer
216+
include Interactify
217+
218+
organize \
219+
self.if(:key_set_on_context, self.chain(DoThingA, ThenB, ThenC), DoDifferentThingB),
220+
EitherWayDoThis
221+
end
222+
```
223+
224+
## FAQs
225+
- Is this interactor/interactor-contracts compatible?
226+
Yes and we use them as dependencies. It's possible we'd drop those dependencies in the future but unlikely. I think it's highly likely we'd retain compatibility.
227+
228+
- Is this production ready?
229+
It's used in production, but it's still early days.
230+
There may be minor syntax changes that are proposed in future, but I don't foresee any major changes to how this will be implemented in public API terms.
231+
We're bound by the interactor/interactor-contracts API, and bound by using it in production.
232+
233+
- Why not propose changes to the interactor or interactor-contracts gem?
234+
Honestly, I think both are great and why we've built on top of them.
235+
I presume they'd object to such an extensive opinionated change, and I think that would be the right decision too.
236+
If this becomes more stable, less coupled to Rails, there's interest, and things we can provide upstream I'd be happy to propose changes to those gems.
237+
238+
- Isn't this all just syntactic sugar?
239+
Yes, but it's sugar that makes the code easier to read and understand.
240+
241+
- Is it really easier to parse this new DSL/syntax than POROs?
242+
That's subjective, but I think so. The benefit is you have fewer extraneous files patching over a common problem in interactors.
243+
244+
- But it gets really verbose and complex!
245+
Again this is subjective, but if you've worked with apps with hundred or thousands of interactors, you'll have encountered these problems.
246+
I think when we work with interactors we're in one of two modes.
247+
Hunting to find the interactor we need to change, or working on the interactor we need to change.
248+
This makes the first step much easier.
249+
The second step has always been a great experience with interactors.
250+
251+
- I prefer Service Objects
252+
If you're not heavily invested into interactors this may not be for you.
253+
I love the chaining interactors provide.
254+
I love the contracts.
255+
I love the simplicity of the interface.
256+
I love the way they can be composed.
257+
I love the way they can be tested.
258+
When I've used service objects, I've found them to be more complex to test and compose.
259+
I can't see a clean way that using service objects to compose interactors could work well without losing some of the aforementioned benefits.
260+
261+
### TODO
262+
We want to add support for explicitly specifying promises in organizers. The benefit here is on clarifying the contract between organizers and interactors.
263+
A writer of an organizer may expect LoadOrder to promise :order, but for the reader, it's not quite as explicit.
264+
The expected syntax will be
265+
266+
```ruby
267+
organize \
268+
LoadOrder.promising(:order),
269+
TakePayment.promising(:payment_transaction)
270+
```
271+
272+
This will be validated at test time against the interactors promises.
6273

7274
## Installation
8275

lib/interactify/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module Interactify
4-
VERSION = '0.1.0'
4+
VERSION = '0.1.0-alpha.1'
55
end

0 commit comments

Comments
 (0)