DEV Community

Fedor Koshel
Fedor Koshel

Posted on

Principles over patterns

In this article I would like to speak about clean-coding principles, how they can be used, and why they can be a good next step for learning after patterns.

Why principles?

Let's imagine that we have a model:

class MyModel < ApplicationRecord
  include SomeBehavior

  DEFAULT_LOCALE = 'ru'

  belongs_to :parent
  delegate :group, to: :parent # Decorator

  has_and_belongs_to_many :other_records
  accept_nested_attributes_for :other_records # FormObject

  scope :good, -> { where(good: true) } # QueryObject

  validates :name, presence: true # Validator
  validate :dont_update_frozen_attributes, unless: :new_record?

  before_validation :do_something # Builder
  after_create :send_callback

  def for_api # ApiPresenter
    as_json(root: true, include: :other_records)
  end

  def for_report # ReportPresenter
    as_json(only: %i[id name])
  end
end
Enter fullscreen mode Exit fullscreen mode

It doesn't look very big and complex. But in a real project we can have tens of relations and validations, then it'll be more than 200 lines of code and it's without business logic. I assume that it's already moved to services. How it can be improved? - With patterns. I've already added comments with pattern names that can be used to improve the code. If we apply all these patterns, our model will only represent database structure in our code base. It's great! But we've already used 6 patterns, the full list is much longer. And to use all of them you should learn them, understand and remember. Yukihiro "Matz" Matsumoto said on the Rails Club 2016: "Be lazy!". I learned patterns but I'm too lazy to remember them. Principles are more abstract, but their number is smaller. Another problem is that patterns work well in standard situations and for specific languages. For example, in Ruby we don't have interfaces and some of patterns can't be used in our case. Also, it can be hard to find a proper pattern if you write something completely new and not standard. Principles can help us because they are more flexible.

What principles?

There are many of them, the trick is to make a list that you like. Almost all Rails developers know DRY (don't repeat yourself) principle. I personally don't like it too much, but it's famous. Another example is SOLID, it's very popular set, at least for interview questions.
My personal list is:

  • Composition over inheritance
  • SOLID (mostly S, O and D)
  • KiS (Keep it Simple)
  • You aren't going to need it
  • Avoid premature optimization​

Further, I'll try to describe with examples what every principle means, why I like it and how to use it.

Composition over inheritance

In simple words, this principle recommends writing classes that implement specific logic and reuse them in different places instead of write implementation in the parent class and inherit it.
It's my favorite one. It allows me to almost never use inheritance at all. Using this approach, I can write similar code style using Golang, Elixir or any other not-an-OOP language. Ruby code also looks much clearer, from my perspective. Actually, most of external libraries use this approach. We don't inherit their behavior; we just use them:
Faraday.get(url), File.read(path), e.t.c.

Let's look at some quite simple examples. Assume, we have three different APIs. They have different endpoints and do different stuff. But they all use the same authentication based on JWT tokens. Using inheritance, we can create an ApiController and then inherit others from it:

class ApiController
  before_action :authenticate_with_token!

  def authenticate_with_token!
    # Some complex logic here
  end
end

Class ApiOneController < ApiController; end
Class ApiTwoController < ApiController; end
Class ApiThreeController < ApiController; end
Enter fullscreen mode Exit fullscreen mode

It works fine but it is hard to add some logic that is similar for some APIs and different for others. There might be many intersections and differences in various combinations but only one entry point - ApiController.

If we use composition over inheritance, we create a special class that does one simple thing - authentication. And then we use it, where we need this behavior.

class JwtAuthenticator
  def self.call(token)
    # Some authentication logic
  end
end

class ApiOneController
  before { JwtAuthenticator.call(headers['Api-Token']) }
end

class ApiTwoController
  before { JwtAuthenticator.call(headers['Authentication']) }
end

class ApiThreeController
  before { JwtAuthenticator.call(params['auth_token']) }
end
Enter fullscreen mode Exit fullscreen mode

As you can see, we already shared the same logic between different classes but also got some flexibility (tokens are taken in different ways for every APIs). And what should we do if another API uses different authentication method? We should just write a different authenticator. It this case we give up a DRY principle, but believe me, if you implement different logic, it’s better to do it in different classes. Otherwise, in several months it might turn into a huge complex class with multiple if – else statements in every methods.
In Ruby we can use Modules to do almost the same thing, they give us a multiple inheritance opportunity. But it is still an inheritance, using modules you should care about the ancestor's queue. And it is a language specific solution. Composition over inheritance can be used anywhere, including functional and procedural languages.

SOLID

I am primarily a ruby developer, and some of the SOLID principles are not applied for ruby well enough. But I will describe all of them just because they are popular, and it is better to know but forget than just don’t know.

Single responsibility

The best and most important principle in the set from my point of view. And it also works great with composition over inheritance. The Wikipedia descriptions says:

A class should have only one reason to change.

It is not completely clear. I prefer the definition: Class has one job to do. And to do one atomic change, you need to change only one class. In other words, one class for one independent piece of logic. And if you need a combination – you write a special class that is responsible only for the combination and uses other classes. Do you remember our JwtAuthenticator class from the previous chapter? It has only one method and do only one thing. That why it is so easy to use it, you don’t need to keep in mind any callbacks or hidden actions.

Open-closed

Again, using Wikipedia:

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

we get not the clearest description. The best one, I know, sounds: Class is happy (open) to be used by others. Class is not happy (closed) to be changed. There are two points – first, classes should be open for different usage. if we apply previous two principles, we have classes that can be used anywhere without any problems. Second – it is better to never change already written code. There are many reasons to don’t touch it. You can break the already working behavior, you can change it in the way that it will be hard to even understand that something is wrong. And finally, as a “lazy” developer, you just don’t want to do the same work twice. So, how can we change class behavior without changing its code? In ruby we have metaprogramming. This instrument is immensely powerful, but again language specific. It also makes code unreadable and very complex for investigation in case of any problems. I would recommend using metaprogramming only for writing DSL in all other situation we can do the job in a better way.
One of the main instruments used by Open-Closed principle is dependency injection. Instead of changing the behavior inside of the class, we can just pass a new behavior from the outside. Let us look at a simple example. Imagine, that we have a Report class that generates different reports. We want to have them in different formats, for example json and html. The simplest way is using if-else condition:

class Report
  def self.generate(params, type)
    # Creating a data structure​
    data = collect_data(params)

    if type == :json
      JsonRender.render(data)
    else
      HtmlRender.render(data)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

The problem here is that every time when we need to add additional render or remove unused one, we must change the class code. Every time we can break something accidentally. Using dependency injection, we can pass a render class as an attribute and then just use it:

class Report
  def self.generate(params, render_class)
    # Creating a data structure​
    data = collect_data(params)
    render_class.render(data)
  end
end

Report.generate(params, JsonRender)
Report.generate(params, HtmlRender)
Report.generate(params, XmlRender)
...
Enter fullscreen mode Exit fullscreen mode

Now we should only care about similar interfaces for all render classes. Some languages like Java supports special syntax for additional restrictions, in ruby we must care about it by our own. But almost every language have some opportunity to use dependency injection.

Liskov substitution

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it
Enter fullscreen mode Exit fullscreen mode

In simpler words – we should keep as a contract, that we do not change public interfaces of children classes. Methods should have same names with same attributes, and they mast return similar values. If you add new parameters, they should be optional and have defaults. If you change the response type, it must be like the parents one, for example, be its children. Let’s look at simplest example:

class Animal
  def walk(distance)
    puts "Move forward to #{distance} meters"
  end
end

class Mover
  def self.move(actor, distance)
    actor.walk(distance)
  end
end

class Dog < Animal; end
class Human < Animal; end

Mover.move(Dog.new, 10)
Mover.move(Human.new, 10)
Enter fullscreen mode Exit fullscreen mode

There is no interfaces or strong types in Ruby, so we can just assume that we pass a kind of animal to the Mover class. And it will work perfect until we implement a Whale that is also an animal but has swim method instead of walk. And it is a quite simple situation to shout your leg while writing code. Liskov substitution protects us from such problems but raises other questions. When should we use inheritance, if it is so easy to break the system logic? Answers might be different. From my perspective inheritance should be used when you create objects semantically identical in the scope of your business logic. It does not matter how are they represented in the real world, much more important what they mean and what they do in the code. Also, inheritance might be used for the fine tuning of already existing behavior. In other situations it is better to use composition over inheritance and be safe.

Interface segregation

Clients should not be forced to depend upon interfaces that they do not use.
Enter fullscreen mode Exit fullscreen mode

This principle is not applied for ruby because we do not have interfaces. But the general idea is to make interfaces small and simple. If you have a class called Calculator it should ideally have one method calculate, or maybe several methods like plus, minus, multiply, e.t.c. But it should not contain all possible calculations for currencies, or equations from quantum physics. And if you already apply Single responsibility principle you usually get interface segregation automatically.

Dependency Inversion​

High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces). Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
Enter fullscreen mode Exit fullscreen mode

For me personally, it was the most unclear principle. Its definition says nothing concrete. Usually, it is described with interfaces. If we have one class that has specific methods with specific parameters and another class uses it, they depend on each other and if you change one parameter in the first class, you must also change the second one. The better way is connecting classes though the interface. Then you can change the class but keep the interface and protect yourself from unnecessary changes. But, as I said before, we do not have interfaces in ruby. In this case we should just keep contracts that are used by duck typing. The abstraction will be created and used by ruby under the hood.
But abstraction is not always an interface. It can be any middleware that helps you connect two classes and hides the concrete implementation. For example, ActiveRecord stays between our code and the database client. We do not care about the proper SQL request for Postgresql, Mysql, or Oracle. The middleware does it for us.
I would not recommend applying this principle everywhere because it is hard to understand the system where everything is hidden behind abstractions. But it is good to know that when the connection between classes becomes too complex, or you have to connect different things to one, it might be a good idea to place something in between. This principle covers several patterns like facade, bridge, adapter, e.t.c.

KiS (Keep it Simple)

You aren't going to need it

Avoid premature optimization​

This group, from my perspective, tells us almost the same thing in different ways. If you need to add two numbers, you do not need to create several classes with abstractions between them. Just do the simple math and keep it until you really need something more flexible and powerful. It makes sense at least because in the modern development process requirements change very fast and during the development process. You can implement a complex system for one situation, but it will be used for something completely different.
The same idea works for "possible future usage". You aren't going to need it - if it's not required right now, do not waste your time and do not do it.
And of course, do not think too much about performance optimization until you really have to. There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton. You do not need to care about caches, horizontal scaling, thousands request per minute, e.t.c. if you have not even started your project on heroku. And if you try to care about all of this in advance, you will never start it.

Oldest comments (0)