Ruby - Specification Pattern
- 12 March 2017
- Patterns Ruby Design
- Ruby - Specification Pattern
Hi guys! I've been digging into ideas of Domain-Driven Design for a while and decided to share one cool pattern that I found in book: "Domain-Driven Design: Tackling Complexity in the Heart of Software" by Eric Evans (The Blue Book). If you haven't read this book yet - do that. Twice. Because it's so hard to get all concepts first time :) In this book Eric Evans describes Specification pattern which can make application more flexible. Today we're going to learn how to implement it.
Wiki:
The specification pattern is a particular software design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic.
Here is a quote from book:
Pattern provides ability to explicitly define business rules, avoiding conditional operators.
Sometimes you feel that particular class is not the best place to contain business logic and you can not find the proper place.
If we put all logic into one class - we're going to have god-object. Something like ActiveRecord model, which usually bloated by business logic, validations, relations, etc.
If we put domain logic into Controllers, it's going to be even worse, because we will have if...else
statements all over the place.
Specification pattern allows us to check object if it satisfies business rules or not. Each specification class should respond to is_satisfied_by?(candidate)
method and return true
or false
.
Let's consider simple example. Imagine we have simple blog post:
Post = Struct.new(:title, :tags, :author, :published)
post = Post.new("Specification Pattern", ["ruby", "patterns"], "Sergii", false)
We want to create specification which would check if post has a title. Code could look like this:
module Spec
module Post
class WithTitle
def is_satisfied_by?(post)
!post.title.empty?
end
end
end
end
Spec::Post::WithTitle.new.is_satisfied_by?(post) # => true
It looks like an over engineering at first sight. Because we've created so much code just to check if post has a title, but you will get the idea when we have more requirements and code.
Eric Evans says that Specification pattern fits for the following goals:
- Validation: check if object satisfies business rules and ready for some purpose.
- Select an object from collection. (If we follow DDD - we can pass Specification into Repository).
- To specify the creation of a new object to fit some need.
Wiki said that pattern allows us to combine rules together by chaining them and using boolean logic. And that's true, because each element of specification should return either true
or false
.
I couldn't find implementation of this pattern on Ruby, but for me its definition means something like that:
spec = Spec::Composite.new(Spec::Post::WithTitle).and(Spec::Post::WithTags).not(Spec::Post::Published)
spec.is_satisfied_by?(post)
Now you see how explicit definition of Specification is. You can read that as plain english language (if we omit module names, of course). We see that composed specification has the following criteria:
- Post with title
- And post with tags
- Not published
When we composed all criteria into one spec we can ask if post satisfies these rules. Looks cool, right?
We can just imagine how many if..else
we would need to describe this specification in code of controller. Also, one of benefits of this approach that these rules don't live inside Post
class. Post
class is just a data structure. Besides that we can combine specification into composite one or use each particular specification separately. For example:
Spec::Post::WithTags.new.is_satisfied_by?(post)
Other languages use interfaces to implement this pattern. Ruby doesn't support interfaces, so I implemented specification pattern this way:
module Spec
module Post
class WithTitle
def is_satisfied_by?(post)
!post.title.to_s.empty?
end
end
class WithTags
def is_satisfied_by?(post)
!post.tags.to_a.empty?
end
end
class Published
def is_satisfied_by?(post)
!!post.published
end
end
end
end
And here is main class which allows us to combine specifications using and
and or
methods:
module Spec
class Composite
def initialize(specs)
@specs = { truthy: Array(specs), falsy: [] }
end
def is_satisfied_by?(candidate)
truthy_check = ->(spec) { spec.new.is_satisfied_by?(candidate) }
falsy_check = ->(spec) { !spec.new.is_satisfied_by?(candidate) }
@specs[:truthy].all?(&truthy_check) && @specs[:falsy].all?(&falsy_check)
end
def and(specs)
@specs[:truthy] = (@specs[:truthy] + Array(specs)).uniq
self
end
def not(specs)
@specs[:falsy] = (@specs[:falsy] + Array(specs)).uniq
self
end
end
end
I feel that this code could be improved, but at this moment it's more like a proof of concept, rather than production-ready code.
Code I've wrote above allows us to combine specifications:
Post = Struct.new(:title, :tags, :author, :published)
post = Post.new("Specification Pattern", ["ruby", "patterns"], "Sergii", false)
spec = Spec::Composite.new(Spec::Post::WithTitle).and(Spec::Post::WithTags).not(Spec::Post::Published)
spec.is_satisfied_by?(post) # => true
I really like this approach. I see the following advantages of using it:
- all specifications are easy to test
- code of each specification is easy to understand
Post
class is just a data object- benefits of combining conditions using
and
andnot
methods - we can use each particular specification separately or combine with other ones
As always I'll appreciate any feedback.
Thanks for reading!