This post is a response (comment, really) to Tim Riley's 2017 presentation on functional architecture, specifically, how to write truly stateless and functional services.
The bad example from him
This approach allows re-using the service object.
I believe the ability to do so is a negative, but even if you disagree, I will show how the reuse comes at too high a cost:
- The API becomes dispersed, some stuff in init, some in call. Where do I put a new parameter?
- Memoisation becomes impossible.
- Extract-method refactoring becomes cumbersome to impracticality because we have to pass everything as arguments.
def call(feed)
# something very long
end
private
def split_out_of_call(feed)
end
def other_split(download_feed, some_loop_arg)
end
The alternative
Let's lean into the functional aspect of a service, and disallow instantiation! This will remove the ability to reuse (an actual win in my book, YMMW), and allow us to use private instances as full black boxes:
class ImportProducts
def self.call(...)
new(...).call
end
# This is key. *Everything* about the instance is private.
private
attr_reader :download_feed, :products_repo, :feed:
# yes, even the initializer is private
def initialize(download_feed:, products_repo:, feed:)
@download_feed = download_feed
@products_repo = products_repo
@feed = feed
end
# no params for #call
def call
# ...
end
def split_out_of_call
# #feed implicitly available
end
def other_split(some_loop_arg)
# #download_feed implicitly available
end
end
And now in action
ImportProducts.(
download_feed: download,
products_repo: repo,
feed: books_feed
)
This way there is never an instance which might get some @errors
populated to be accessed later. Everything needs to be in the #call
return value, often a hash, but ideally a Struct.