DDD on Rails Part 2 - Models


Removing Business Logic from Models

DDD on Rails Series

Hello again!

In the last Post we talked about how you could remove logic from controllers and move it to Navigators so it would be framework agnostic.

Now we are going to talk about how can we remove logic from a multipurpose class to single purpose classes and keep your business logic apart from the persistence logic.

What's wrong?

Almost everybody has already faced a thousand lines User model at least once in their lives. I know, it is a scary sight! Maybe, the first thing you thought was: "How can I refactor all these crap? Where should I put that?". If that is your case, well... I think I have an answer for you.

But maybe you are asking: "What is the problem concentrating all the logic on the Model?"

Well, there are a lot of problems, I'll list some of them for you:

  • Models manipulating related models

    Let's say we have a system where, when a user is created, we create a validation token and send it to his email. After the validation is made, we have to invalidate the token.

    class User < ActiveRecord::Base
      has_many :validation_tokens
    
      before_create :create_token
    
      def create_token
        token = validation_tokens.create
    
        token && email_token(token)
      end
    
      def validate!(token_code)
        validation_token = ValidationToken.eager_load(:user).active.by_code(token_code)
    
        if(validation_token && validation_token.user == self)
          activate!
          validation_token.inactivate!
        end
      end
    end

    At first this seems harmless, but the code on the user model should only care about the user and not about it's related models. Using this approach we make the user model dependent on the validation_token model and add complexity to the tests.

  • Callbacks of any kind

    When we start a project we spread them around like crazy. Then the system grows and suddenly you realize it is acting strange, it is taking decisions on its own.

    They may come handy sometimes, but they are implicit and don't create a clear vision of the whole process. A new developer would have to check all the callbacks and find the implementations to understand what they do.

  • Code with logic from external dependencies, like libraries or other integrated services

    There are some projects that integrate with other external services like email marketing, data analysis, etc. We tend to write all those integrations directly on the model.

    class User < ActiveRecord::Base
      after_create :external_service
    
      def external_service
        response = HTTParty.post "https://external.service.com/destination",
          headers:{
            "X-API-Key" => "My-kEy",
            "Content-Type" => "application/json"
          },
          body: {
            "name": name
          }
    
        response.success?
      end
    end

    But this is not a good practice, if someday you decide to change the provider, you will have to look for all the code using the old provider and replace it with the new one.

  • Front end code without any relation to persistence

    Imagine we have a user model, with a bio field that we have to display in a view. Before displaying it, we have to remove all the cruft and make it HTML safe... Well, the bio is a field of the user, so we should move this logic to its model right?

    class User < ActiveRecord::Base
      def sanitized_bio
        ActiveSupport::Inflector.transliterate(bio).html_safe
      end
    end

    Wrong! The model should not be responsible for any display logic.

  • Validations on fields which are not on the database

    This problem is pretty simple, here is a sample code:

    class User < ActiveRecord::Base
      attr_accessor :password, :password_confirmation
    
      validates :password, :length => { :minimum => 6, :maximum => 60 }
    end

    As you can see, password and password_confirmation attributes are not database bound, they only exist to generate a crypted_password later. Yet there are some validations for the password attribute which definitely doesn't belong here.

  • Format validations on fields that are on the database

    I will use a classic example here: the validation of the email format on models:

    class User < ActiveRecord::Base
      validates_format_of :email, with: /A[^@ ]+@([^@ ]+.)+[^@ ]+z/
    end

    This validation has no relation to the database, the database would accept an email with another format. So it should be somewhere else.

What can we do?

Now that we know some problems, how can we prevent them from creating trouble?

DDD uses a much more detailed abstraction, but for our needs an adapted and simplified set is enough.

If we inspect our problems we can find that they are divided into 3 main kinds: display, business and persistence.

Persistence Logic

The model class is responsible for the persistence, so everything else should be extracted to other classes.

This leaves us with business and display logic.

Display Logic

In DDD an object defined by its identity is called an Entity. Therefore, we can say that the data retrieved from the database can be transported inside our System as Entities. Great, and who is responsible for retrieving the data from the database and providing Entities to the System? Well, the responsibile for this task is the Repository.

Repositories abstracts the persistence from the business logic in a way that if we choose to change the persistence mechanism, the business logic should not be affected. If we would follow DDD we should use Factories to create entities, but we are going to aggregate this responsibility on the Repository.

OK, the Repository gathers data from the database and provides Entities which are passed all the way to the views. Meaning that the display logic could reside on Entities.

Great, and what about Business Logic?

Business Logic

Business Logic should be on Domain Events, which are objects containing all the logic for a given event defined in the System. Or on Services, which are objects containing logic for the integration with an external component. I like to call them both Services to simplify things a little.

So it leaves us with something like this: Services are responsible for the Business Logic, which uses Entities to talk with Repositories, which, in turn, talks to the Model.

Wraping up

I am going to try and solve all the problems with the same implementation.

Let's start with the Models which are going to be almost empty:

class User < ActiveRecord::Base
  has_many :validation_tokens

  attr_accessor :password, :password_confirmation
end
class ValidationToken < ActiveRecord::Base
  belongs_to :user

  validates_uniqueness_of :code

  scope :active, -> { where valid: true }
end

The Models responsibility is the persistence, so they have only scopes and validations which are related to the database.

Most of the logic from the Models, like validations and the display logic is going to be moved to the Entities, and they look like this:

module Entities
  class User
    include ActiveModel::Validations

    attr_accessor :id, :name, :email, :bio, :validated, :password, :password_confirmation

    validates :password, :length => { :minimum => 6, :maximum => 60 }
    validates_format_of :email, with: /A[^@ ]+@([^@ ]+.)+[^@ ]+z/

    def initialize(attibutes)
      @id = attributes[:id]
      @name = attributes[:name]
      @email = attributes[:email]
      @bio = attributes[:bio]
      @validated = attributes[:validated]
      @password = attributes[:password]
      @password_confirmation = attributes[:password_confirmation]
    end

    def sanitized_bio
      ActiveSupport::Inflector.transliterate(bio).html_safe
    end

    def attributes
      { id: id, name: name, email: email, bio: bio, password: password, password_confirmation: password_confirmation }
    end
  end
end
module Entities
  class ValidationToken
    attr_accessor :id, :user_id, :code, :valid

    def initialize(attibutes)
      ...
    end
  end
end

You can see we moved to the Entities the email format and password length validations, which do not have a relation to the persistence. This way we can do most of our validations without touching the persistence layer. We also moved the sanitized_bio method to the Entity, so the views do not need Model instances to present our data.

I have not used any Gem besides Active Model for the validations, but you can use any Gem (like Virtus) or extract common functionality to a Base Class in order to simplify the Entities implementation.

Now we need some Repositories to abstract the persistence logic from the business logic. Again, this is just an example, you could extract a lot of code to a Base Class and reuse it on all Repositories.

module Repositories
  class User
    def initialize(entity)
      @entity = entity
    end

    def save
      user = ::User.new(@entity.attributes)

      !!user.save
    end

    def self.validate!(user_id)
      return false unless user_id

      user = ::User.find(@user_id)

      user ? user.update_attribute(:validated, true) : false
    end
  end
end
require 'securerandom'

module Repositories
  class ValidationToken
    def self.create(user_id)
      code = SecureRandom.hex(16)

      token = ::ValidationToken.new(user_id: user_id, code: code, active: true)

      token.save ? code : nil
    end

    def self.inactivate!(code)
      return false unless code

      token = ::ValidationToken.by_code(code)

      (token && token.update_attribute(:active, false)) ? token.user_id : false
    end
  end
end

We moved all integrations with the Models to the Repositories in order to encapsulate this logic from the System. We also moved the logic for creating the ValidationToken code into the Repositories, as it belongs to the Business Logic of the system and is related to the database.

Great, to the Services now. We have 2 problems left: the user authentication/validation and the logic for the external service.

So let's create the Authentication Service first:

class Authentication
  attr_reader :user, :token

  def initialize(attributes, mailer=SignUpMailer, service=ExternalIntegration)
    @user = Entities::User.new(attributes)
    @mailer = mailer
    @service = service
  end

  def self.create_user(attributes, mailer, service)
    self.new(attributes, mailer, service).create_user
  end

  def create_user
    if user.valid?
      Repositories::User.new(user).save

      @token = Repositories::ValidationToken.create(user.id)

      @token && email_token

      authentication_integrations
    end
  end

  def self.validate_user(token_code)
    user_id = Repositories::ValidationToken.inactivate!(token)

    Repositories::User.validate!(user_id)
  end

  private

  def email_token
    @mailer.welcome(user, token).deliver_later
  end

  def authentication_integrations
    @service.integrate(user)
  end
end

I made email_token and the authentication_integrations methods private, because we don't want people sending emails to our clients or sending data to our partners without our consent.

Now the Authentication Logic is no longer coupled to the persistence layer. So, if someday we decide to change it, the Repository is the only one impacted. After we fix it, everything is working again.

Let's check the External Integration Service:

class ExternalIntegration
  def self.integrate(user)
    response = HTTParty.post "https://external.service.com/destination",
      headers:{
        "X-API-Key" => "My-kEy",
        "Content-Type" => "application/json"
      },
      body: {
        "name": user.name
      }

    response.success?
  end
end

We simply moved the method from the Model to it's own class. Yet again, this is just an example, we could extract some data into a configuration file or Environment Variable so it is more secure and flexible.

With this, our External Integration is encapsulated. Any change of provider would only affect this Service and fixing it gets us back on track.

Conclusion

The Models, especially the User, tends to receive all kinds of logic and, with time, they can become a problem that reduces your team's productivity and motivation.

To prevent this from happening, we can use some simple strategies from the DDD to uncouple our code and divide responsibilities to specialized classes, encapsulating and keeping the Logic more clean and easy to understand and refactor.

At first it seems a lot of work, but this work pays off as your code will keep clean and readable for a much longer time.

Leave your doubts and comments below and till the next post!



comments powered by Disqus