Hello! My name is Attila Domokos and my twitter handle is
@adomokos
"Simple and Beautiful Rails Code with Functional Style" is the name of this talk.
I am not going to teach you functional programming. I am not going to try bending the Ruby language to be a functional one, either. Hopefully I'll show you refactored and elegant Ruby code. So if you're ready, let's roll.
I have a 25 minutes commute to work and I listen to audio books while I drive. A friend recommended reading the book: "The EMyth Revisited".
This sentence got stuck in my brain.
Translated to our profession, we could say: "The kind of code you write is a reflection of who YOU ARE."
I am obsessed with the quality of roads. Unfortunately, in Northeast Ohio the roads are in really bad shape. We blame it on the weather, but I think the quality of work has a part to it, too. This pothole took more than a couple of weeks to grow this big.
And when it comes to fixing it, this is what we do. Another winter and the pothole is the same size or bigger than it was.
I found this photo a couple of months ago when I was going through the news. The owner of this house refused to sell his condo to the Chinese government. Since they couldn't wait building the highway, they just paved around it.
I was pretty sure nothing can top the house in the middle of the highway news. I was wrong: some other people in China refused to move the remains of their relatives and since the building had to be built they just went ahead with the construction.
A friend of mine made a trip to Japan last year. This is one of his photos.
What do you think you see on this picture? It's a hotel room. Recognize the simplicity and
pureness in this room. There is no TV or furniture laying around.
When you see a class like this, I'd like you to picture that Japanese hotel room. No extra junk, no second-guessing. Only 18 lines of code where you can pretty much tell what this code does.
Do you remember your room growing up? It might looked like this. How many hours did you spend looking for something? Your book, your key or your favorite t-shirt?
And that messy room is translated into this code. This puppy is a model from one of the Rails apps we have to maintain. It has 2869 LOC. The calculate_shipping has 207 lines. And the recalculate_shipping has 196 lines in it! Somebody just copied the calculate_shipping method and made a recalculate_shipping from it.
I wanted to find a tool that measures code complexity. The best one out there is the FLOG gem. Understanding the result is simple:
"The higher the score the harder it is to test the code". It uses ABC metrics: assignments, branching and calls.
I analyzed that model you saw in the previous slide: it has a total flog value of 2592!
I've worked on many Rails app in the last couple of years. The ideas I am talking about in this presentation emerged while
working on those apps. I wanted to use one example to show you the different solutions I've tried.
This is a tracking app, where you can provide all the data in one text and the system parses it out into category and track records.
It will also check if the category already exists and will use that to file that track.
You can optionally prepend the date in the front in case you want to use a date other than today's date.
Early on, we put most of our domain logic in the controller.
Using my example app, I put all the parsing logic in the controller action. The action has 48 lines of code, which is not bad considering
what we all did a decade ago: all logic went in the views. Do you remember those good old ASP days?
But seriously: 48 lines of domain logic
in the controller?! Yeah: WTF.
I am comparing the TracksController and Track model with Flog. The Track model has already some logic around displaying dynamic properties. But the action has a Flog value of 46.2.
Having all logic in one method is bad. After extracting a couple of methods the controller is a bit easier to read, but I think this code is still a mess. At least the total LOC for this action went down from 48 to 25.
The total Flog increased slightly for the controller, but our action's complexity went down from 46.2 to 20.6.
The Rails community eventually figured out that putting domain logic in the controller isn't so great. By moving the logic from the controller into the models we let the controller do its work: orchestrate between views and models.
After moving the parsing logic into the model the controller now has only 12 lines of code. The controller sends the message to the model,
the model is saved by the controller and handles the result accordingly.
This controller code is simple and easy to understand, something I'd be happy with.
The Flog change between the controller and model clearly shows that I only moved methods around. All the parsing logic is unchanged and is now in the model.
This is where my story begins. After going through a couple of tutorials and placing most of the logic in models I soon started
breaking out of Rails and putting domain logic in Rails-independent service objects.
The benefits to me were huge: I was able to run
my specs under sub-seconds instead of 23 seconds.
The controller looks very similar to the version where I had the logic in the model. Instead of calling the model, the controller sends a message to the service object.
Once the logic was moved into the service, the model's Flog value went back to its original size.
This is how I initially developed my service objects. One main method was called from the controller, and that "organizer" method
called other methods within the same object.
In the context of this example the controller called the "for_track" main method on this service object.
I broke some obvious OO principles when I kept my internal methods "public" for the sake of easier testing. However, when I refactored the service's internal structure, some of the unit tests broke. This did not work...
I made all the methods on the service object private except the "for_track" which was called by the controller. This led to incredibly
complex test setups. As I added more logic, the tests became even more brittle.
Listening to my tests made it pretty clear: this will not work in the future.
Take a look at how I name my services. I don't use nouns, I name them with verbs. They mean actions to me.
When I see a class named
"ParsesFeed" I know exactly what this class should do. It's not for saving the category or track records in the
database. It forces me to keep the object's single responsibility.
I attended CodeRetreat in Cleveland right after CodeMash last year. Corey Haines mentioned this blog post which is an amazing writing. Do yourself a favor and read it! You will name your classes differently.
In my Java days I used the Chain of Responsibility
pattern to brake up complex workflows into simple, single responsible actions.
It was obvious the design of my service object was sub-par. Testing of it was very difficult, I violated SRP all over my object. I had to find a much cleaner organization of my code. Two types of objects emerged: the single organizer object and the small action objects.
I enjoy the kind of code that reads like a chapter from a book. I fell in love wit the readability of RSpec and the Ruby language itself.
Most of the things we do are a series of tasks. The parsing logic I used in my example has several steps: it splits the feed to parts,
it parses out the recorded at date, validates the remainder of the feed, etc.
I wanted to preserve the readability of the tasks in the organizer object. The organizer calls the actions in order by iterating over them one by one.
What are the actions? They are the atomic building blocks of the domain logic workflow. They are unaware of each other's existence.
The organizer invokes them by providing the data in a special kind of context.
Using the Chain of Responsibility made sense in a statically typed language like Java, but it was just too big of a ceremony in Ruby. Influenced by this Gof4 pattern I came up with a slight variation of it: The Series of Actions. Instead of actions calling the next one in the chain I let the organizer set up the order and call them one by one.
My goal was to keep the actions as simple as possible.
Consider it as a black box: you put some stuff in it, it does its magic and it either alters an object already in the context or
adds a new one to it.
Now the total Flog value went up for the service objects after refactoring them into Series of Actions. However, and this is important, the Flog class average went down significantly. It's a lot easier to understand a class with 13 Flog value than one with around 80 or 2500.
Well, I am not newing up objects with instance variables and other instance methods. I don't maintain state in the class object.
I am basically building up the domain logic from small functions wrapped into class object methods.
The actions are working on the data provided to them. The data carrier, or context is a special kind of hash. A key-value pair with behavior added to it.
The organizer object initializes the context. It's being passed to the actions when the organizer iterates through them.
At the very end this context object is returned to the controller filled with all the data it needs.
The actions know how to pull data from the context. It has to have an item in order to execute the action successfully.
And when an error occurs it pushes the context into a failure state.
The actions are guarding themselves against a context which is in a failed state. In case the second action fails, the third and fourth actions will not be executed.
As I went from one job to the other and I used the same ideas in different Rails projects, I ended up copying files a lot.
Eventually I gathered all the functionalities I needed and released a gem that I named Light Service.
The whole gem right now has only two classes. That's it. I want to keep this as "light" as possible.
I realized all my actions were doing the same thing: guarding execution from the context in failure state and returning the context object at the very end of the method.
With Light Service you don't have to remember adding the guard condition and the return statement. It creates a method for you with the name "execute" which does it for you.
Either add it to your gemfile or install it through rubygems.
Your customers are going to ask for changes or new features in the software. The question is: where will you put those changes?
I compared two solutions: the one where the logic is in the controller and the one where I use the Series of Actions. This is our initial state.
And I doubled the code in the controller by copying the methods and appending 1 to the method names, so now there is a "builds_category"
and a "builds_cateogory1" method. I also doubled my action classes in the services as well.
Although the Flog total is slightly higher in the services, the Flog class average remained around 13.
Just to see the trend clearer, I tripled both the controller code and the number of classes I had in the services. What's really interesting
is that the Flog class average remained almost unchanged.
If I keep doing this I am sure I can reach 2500 Flog points in the controller and the Flog class average for action classes will remain around 13.
We will always understand a class with the Flog value of 13.
To visualize my experiment I came up with this graph. While the complexity grew with O(n) in the controller the Series of Actions remained static with O(1).
Don't do this every time! Take advantage of Rails, don't introduce additional complexity to it.
I was asked when I would build out Series of Actions: well, I might put a conditional in the controller action,
but once the code gets more complex I break it out into services and test drive the logic there.
The biggest benefit for me for using the Series of Actions is locating code. I don't have to hunt down logic in Active Record callbacks or mixed-in concerns, I can follow the trail right from the controller.
Let's say I have to change the way a new category is being initialized. I have not worked on this app for a couple of months, so my memory is a bit hazy and I need to find that code. First I look at the controller action and I see which organizer object is called.
I locate the organizer object, where the Series of Actions are mapped out clearly. I see that the Category has to be initialized in the the BuildsCategoryAction, so I open that file.
And Voila! I am there. I don't need to jump into the debugger or fire up Rails console. I can just follow the trail right from the code.
And to sum this all up: get out of the framework as complexity arises. Elegant code should tell you a story. Don't use state if you don't have to, it increases complexity. Separate behavior from data, it let's you follow SRP. Introduce new classes or new actions instead of adding new methods to a model or a service.
And most importantly: make your code simple!
You can get in touch with me through my social links.
And thank you for your attention!