This document discusses rethinking the architecture of Rails applications. It suggests that the conventional Rails architecture fails because there is no proper architecture, leading projects to struggle with maintainability. It proposes adding more abstraction layers like entities, mappers, repositories, queries, and use cases to decouple from ActiveRecord and slim down controllers. This "new way" aims to provide a cleaner architecture and better order while reducing complexity and improving maintainability.
3. A few facts about me
Blogger
kamil.lelonek.me
Live in
Wrocław,
Poland
CTO at
woumedia,
Denmark
Full-stack Developer
(mostly Ruby, Scala)
@KamilLelonek
45. Thank you for having me here
rails new app --skip-rails
Notas do Editor
Hello guys, in this presentation we're gonna rethink Rails architecture together, discuss some pitfalls
> and propose some solutions for a better development process.
At the very beginning I'd like to clarify what we won't talk about directly.
> It won't be about ...
> and some other fancy stuff. But still you don't know what to expect, right?
So, I can ensure that I will mention about Value Objects, Entities, Queries and Repositories.
> I'll try to present and describe a lot of useful building blocks for modern applications.
What is the reason for rearranging something that is already done? Well, when it's done poorly, it's worth to review it, because
...
But those cries go unheard, mostly.
Nowadays, Rails applications can end up in two ways:
> Either someone in the team is an experienced architect and leads the software to an advanced design with service layer, view components, maybe forms, and so on.
> or a classy way, when a project strictly follows the Rails Way and will end up as a code disaster.
I would like to apologise to anyone I have offended.
And to those I haven't offended yet. Please be patient. I will get to you shortly.
Based on what I said you may wonder now when to follow the Rails way without introducing any additional architecture.
There are some cases when that may be useful. > It's easy to learn and add new features, you can simply add new concepts by running scaffolding.
> For fast production-ready or production-convertible prototypes that you need to present to your potential customer as a proof of concept.
> It allows you to simple jump between different projects without diving into a custom architecture
> Perfect for CRUD applications of course.
However, usually you build bigger apps. You have a way more complex problems and provide not so simple solutions.
There's when an additional architecture comes handy.
> When you maintain long-term projects
> In case you have experienced programmers who probably want to try something more
> When your domain is big and business logic is complex
> When your team is distributed
> Once again, did we fall into some kind of trap? What price do we pay for Rails’ simplicity in trade for maintainability? Were we tempted by this simplicity without thinking about the maintenance?
> Is it worth writing models with thousands of lines of code and building a throw-away software kludge that is impossible to maintain, just for the sake of development speed?
> How fast are we on the very beginning compared to the future development? Is it worth blindly hacking away and following the Rails conventions without thinking about whether this is the right place for that code?
Here is how the majority of applications are created these days.
We just do rails new and that's it.
And here is how it actually should be done.
rails-api is an awesome gem that let you build applications which expose RESTful endpoints with JSON data for separated frontend apps.
Rails downright encourages us to write big, monolithic applications in an MVC manner instead of splitting them into separate layers or a hexagonal architecture.
Right now, instead of identifying and implementing new layers like form objects, things get violently pressed into the controller or alternatively the model, both in the framework itself and in your application code. All that just because of fearing over-abstraction.
> Why on earth is Rails constantly trying to solve incredible complex problems in one gigantic, monolithic class?
...
Should we drop Rails at all?
After all we have great frameworks such are
> Lotus
> Volt
> or rom-rb?
BTW, anyone use either of them?
We can still use Rails, but ...
The first building block to refactor our applications is a Value Object.
> Value object is an immutable object that describes some characteristic or attribute
> but carries no concept of identity.
This is an example of Value Object.
I'm using Virtus gem from a friend of mine which provides nice helpers for value objects. We can specify attributes and their types.
Entity is an object fundamentally defined not by its attributes.
> It differs by ID, which have to be unique within an aggregate, not necessary globally. Never share an entity between aggregates.
> A unique thing that has a life cycle and can change state.
Sometimes in one context something is an entity while in another it is just a value object.
(Aggregate — a cluster of domain objects that can be treated as a single unit to provide a specific functionality and for the purpose of data changes)
Can you find some examples of that?
Once again using Virtus, we can easily create Entity with given attributes and their types.
Taking data from one representation and converting it into another is done by using mappers.
> Mapper is an object that takes a record and turns it into a domain object.
You can design your domain objects exactly the way you want and configure mappings accordingly.
By defining a mapper you are specifying which entity class is going to be instantiated and what attributes are going to be used.
> Mapping is an extremely powerful concept. It can:
Rename attributes
Coerce values
Build aggregate objects
Build immutable value objects
...
This is a mapper that takes a record from a DB and turns is into a domain object with particular attributes.
Submitting form data is a common feature of web applications – allowing users to submit their information
> and giving them feedback whether the information is valid or not.
And for that purpose we use form objects.
> Form Object is an object that wraps incoming input from a user and provides a validation to ensure that only correct data is processed within an application anytime later.
And this is an example where I'm using Reform gem from my another friend @apotonick and this gem is used to wrap user's input, validate it, and map it into a particular entity.
So from given params we get completely valid domain object.
> Repository — mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.
> A repository implementation is also an example of an infrastructural service.
Therefore the specifics of the communication with durable storage mechanisms are handled in the infrastructure layer.
Our repository uses Dependency Injection to accept a particular adapter, which can be ActiveRecord during development and production or InMemoryAdapter while we are testing our code.
Later on we use injected adapter to create and store a particular object from our form object.
Database queries, even simple ones, can be both repetitive and hard to read and comprehend. With more complex queries, especially ones that embed data from multiple collections or tables, this process can get messy to write and even messier to maintain.
> Query object - provides a nice tool for extracting query logic and associated operations into a contained module, pulling the logic out into a more maintainable and readable structure,
> while also providing a very readable API where the query object is used.
In this query we are finding a record by it's id and then we map it onto our domain entity object.
In case of failure we raise not found error.
> Validators - are used to ensure that only valid models exist within your application
> and therefore only valid data is saved into your database. It may be important for your application to ensure that every user provides a valid email address and a phone number in a correct format.
There are several ways to validate data before it is saved into your database, including native database constraints, client-side validations, controller-level validations, but validators wrap all of them in a one place together.
This is an example of a validator that just checks if given params conform our form object constraints. There's no DB checking and other kind of validations.
> Use case - a component that is responsible for the coordination logic we tend to put into our controllers. And being a coordinator, a use case should not do any computation or state management. There is no one-to-one mapping between use cases and controllers. One controller can support multiple different use cases and vice versa.
> These use cases orchestrate the flow of data to, and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.
In this example I use solid-use-case gem which helps define steps for a specific flow. So when making an order we can validate incoming params, sore the result and dump read model into our database.
How fast are your tests?
Do they rely on a database?
How easily can you switch from relational schema to NoSQL documents or even YAML store?
Do you have fat Models that leak up to Controllers or even Views?
Does your application depend on details of a particular ORM implementation?
Using previously described building blocks we can easily avoid aforementioned problems. We define in-memory adapter that behaves similarly to active record and can be injected into our repositories.
Here you can see an example usage of it. We are creating an in-memory adapter and inject it in our repository that we are using later.
In more complex cases we can define an in-memory repository instead of just an adapter.
And that's all of the building blocks for now. I didn't mentioned about Service Objects, but it's a topic for yet another talk.
> What are the possible disadvantages of them?
...
> And what about benefits?
With a new architecture comes a new infrastructure as well.
In this picture I'm showing you an overall flow within the recent startup I've been doing. I will focus on particular parts right now.
Firstly, I've extracted a front-end part completely. I don't have views served by a rails application. Instead, I have a couple of lightweight front-end static single page applications build, compiled and minified using Brunch or Middleman with all goodness from CDN servers.
They are using REST API to connect with backend application.
The backend is divided as well. I'm using one core rails-api server, which connects to 3 different micro services using CQS flow, vertx queuing system and their own separated databases. Each microservice describes one bounded context and wraps a specific responsibilities that are called from the core server.
Besides the benefits I'be told you already there is also a note about deployment. I can independently deploy either front-end application or even a micro-service. Using pipelines and integration server I can easily extend only some part of my application which is extremely fast and efficient.
Here are some resources I'd like to share with you.
I've written a couple of blogposts related to the presented topic.
Moreover, I created a sample application to show you working examples with discussed building blocks.
Well, that's it for now. Have you guys any questions for me?