Introducing Lotus::Model


Almost all the Ruby frameworks for the Model layer mix up business logic with database details. This kind of architecture leads to god classes, slow build times and to a general bad design. These problems are well known to legacy projects’s maintainers.

What if we assign these roles to smaller components that are able to collaborate together? Imagine how life changing would be to work just with objects, without worrying how to persist them. How easy and fast would be testing them? How small and well defined would be your objects?

Let me introduce Lotus::Model.

It brings a new level of decoupling that is a huge step in that direction. The framework constitutes a boundary that offers a convenient public API to execute queries and commands to persist and fetch entities.

Entities

An entity is the core of an application, where the part of the domain logic is implemented. It’s a small, cohesive object that express coherent and meaningful behaviors.

It deals with one and only one responsibility that is pertinent to the domain of the application, without caring about details such as persistence or validations.

This simplicity of design allows you to focus on behaviors, or message passing if you will, which is the quintessence of Object Oriented Programming.

Consider this object:

class Article
  attr_accessor :id, :title, :text

  def initialize(attributes = {})
    @id, @title, @text =
      attributes.values_at(:id, :title, :text)
  end
end

It can be optionally expressed as:

require 'lotus/model'

class Article
  include Lotus::Entity
  self.attributes = :title, :text
end

Yes, optionally. Lotus::Model can work with pure objects, as long they implement that small interface above.

But how the framework knows how to handle these objects?

Data Mapper

We use a data mapper for the job. It’s a persistence mapper that keeps entities unaware of schema details. Good news are that it’s database independent, it can work with SQL, document, and even with key/value stores.

The role of a data mapper is to translate database columns into the corresponding attribute of an entity.

require 'lotus/model'

mapper = Lotus::Model::Mapper.new do
  collection :articles do
    entity Article

    attribute :id,    Integer
    attribute :title, String
    attribute :text,  String
  end
end

For simplicity sake, imagine that the mapper above is used with a SQL database. We use #collection to indicate the table that we want to map, #entity to indicate the kind of object to persist. In the end, each #attribute call specifies which Ruby type we want to associate for given column.

Repositories

Once we have in place all the entities and a mapping for them, we can use a repository to talk with a database.

A repository is an object that mediates between entites and the persistence layer. It offers a standardized API to query and execute commands on a database.

A repository is storage idenpendent, all the queries and commands are delegated to the current adapter.

This architecture has several advantages:

  • An application depends on an standard API, instead of low level details (Dependency Inversion principle)

  • An application depends on a stable API, that doesn’t change if the storage changes

  • You can postpone storage decisions

  • It confines persistence logic at a low level

  • Multiple data sources can easily coexist in an application

require 'lotus/model'

class ArticleRepository
  include Lotus::Repository
end

When a class includes Lotus::Repository it will expose CRUD methods such as .create, .update, .find. Aside from that, it offers a powerful private query API. This decision forces developers to define intention revealing APIs, instead leak storage details outside of a repository.

Look at the following code:

ArticleRepository.
  where(author_id: 23).
  order(:published_at).
  limit(8)

This is an example of implicit API, it means nothing in terms of the behavior of the domain model. It’s just a chain of method calls. From the caller perspective, it should be aware of the internal query mechanisms.

There is a better way to write it:

require 'lotus/model'

class ArticleRepository
  include Lotus::Repository

  def self.most_recent_by_author(author, limit = 8)
    query do
      where(author_id: author.id).
        order(:published_at)
    end.limit(limit)
  end
end

Look at how revealing is the name of that method. It encapsulates the implementation details, in favor of a clear and testable API.

If we change the type of database, the callers of that method will be unaffected.

Adapters

As mentioned above, Lotus::Model is database agnostic. A repository forwards method calls to its current adapter. An adapter is a concrete implementation of persistence logic for a specific database. The framework is shipped with two adapters:

  • SqlAdapter
  • MemoryAdapter

An adapter can be associated to one or multiple repositories and different repositories can have different data sources. For instance an application can have ArticleRepository that uses a SQL database and TweetRepository that talks to a third part JSON service.

Roadmap

For the next two months, I will focus on Lotus (the gem). The main goal is to make all the frameworks to work together in a full stack app. This will require improve the existing libraries and empower them with the missing features.

On June 23rd I will release Lotus.

{% include _lotusml.html %}

Luca Guidi

Family man, software engineer, Open Source indie developer, speaker.

Rome, Italy https://lucaguidi.com