4 simple rules are pretty easy to remember, but a bit harder to understand and apply.
A key concept of East-oriented Code is to enforce the use of commands by returning the object receiving a message.
Here's a simple example of what that looks like:
def do_something
# logic for doing something omitted...
self
end
It's incredibly simple to follow.
Here are the rules I set forth in my presentation at RubyConf:
- Always return self
- Objects may query themselves
- Factories are exempt
- Break the rules sparingly
The first three are hard rules. The fourth, obviously, is more lenient. We'll get to some guidance on breaking the rules in the future but for now let's look at applying this to your code.
Rule 1: Always return self
Although this rule is simple at first, it inevitably leads to the queston of getter methods.
What if your objects had no getters? What if an object's name
attribute simply was inaccessible to an external object?
You can make your data private by either marking your attr_accessor
s as private:
attr_accessor :name
private :name
Or you can use the private
method to mark all of the following defined methods to be private:
private
attr_accessor :name
How you choose to do it will depend upon your code, but this would help you remove any getter methods.
Now this leaves you with a conundrum. How do you use the information?
If you have a need for that name
, what can you do?
The only answer is to create a command which will apply the data to the thing you need.
def apply_name_to(form)
form.name = name
self
end
The restricitions we put in our code are often self-imposed.
We can make whatever we want, so what's to stop us from putting Rails model data manipulation in it's view template? Nothing concrete stops us from doing so.
The same goes for getter methods like name
. If it is publicly accessible by external objects, then we can create whatever if
and case
statements we want. We can put logic wherever we want.
If we create our own restrictions, we can guide ourselves and other programmers to the direction we intend for our application's structure.
Creating restrictions
I've written about the Forwardable library in the past not only because of it's usefulness, but because we can copy the same pattern to create our own DSL.
Forwardable provides methods which create getter methods for related objects. But what if we created our own DSL for commands to related objects? What if we could pass the messages on, but allow the related object to handle the values?
Here's what that could look like:
class Person
command :send_email => :emailer
end
person = Person.find(1) # get some record
person.emailer = Emailer.get # get some object to handle the emailing
person.send_email
That's a lot of pseudo-code but the parts we care about are sending the command to a related object. Commands return the receiving object, queries will return a value.
Here's what that code would look like without our (yet unimplemented) command
DSL.
class Person
def send_email
emailer.send_email
self
end
end
Any code which uses a Person
will have to rely on the command to do its own thing. This prevents a programmer from leaking logic out of the person.
What should happen when the email is sent? With the structure above, this code, can't make decisions:
if person.send_email
# do one thing
else
# this will never work now
end
If you find that you often write code like the if
statement above, you might wonder "where does that logic go now?" Now, you'll be forced to write this code:
person.send_email
And this means that your send_email
now has the job of handling what to do:
class Person
def send_email
emailer.send_email
# do some other things...
self
end
end
That might provide you with better cohesion; the related behaviors remain together.
Getting back to that command
DSL we used above...
This was the final point of my presentation at RubyConf: you can build guidlines like this for yourself.
I created a gem called direction
to handle enforcing this East-oriented approach. I'll write more about that later, but it shows that I can create signals to other developers on my team. I can take a simple concept like a command and simplify my code to show other developers what's happening:
class Person
command :send_email => :emailer
end
Building a DSL can aid in communication. The language and terminology we use can compress ideas into easily digestible parts.
If you like this, check out my new book: Ruby DSL Handbook designed to be a guide to help you build your own compressions of concepts.