MVC + Pattern Architecture (Rails)
Rails is popular for start-ups. The reason of adopting Rails, I think, is to maximize the development productivity, namely to build an application very quick with small amount of code. But if you try to do everything within the original MVC, you will easily end up messing up with fat controllers and/or fat models, and lose the maintainability and readability of your codes.
First let’s think about why your code gets unmaintainable easily.
Why does your code get unmaintainable?
First thing to realize is, for most cases, the business requirements are procedural (Take a few minutes to think if the statement is true). But Ruby is object oriented language (+ functional paradigm). You need to fill in the gap between the two somehow. And I think this is what causes the confusions or disagreements even in a team, which in turn make codebase unmaintainable.
What is maintainable code?
This style went well in my previous project (edit: updated a bit). Our code became beautifully loose-coupled. “Went well” here means, you can keep models single-responsible, well testable and fully re-usable and controllers well-readable even to non-techies. There are lots of items here, but keep YAGNI in your mind and choose only what you need.
Models
This is where domain logics reside. For most cases, logics that interact with non-ruby world. Persistence(ActiveRecord), File system, Outside service and so on. ActiveRecord model is not the only models. Remember ActiveResource that wraps up outside APIs was also models. Models cannot have more than one domain, for example, cannot have DB and File System functionality together in one model. Add logics which only relates to the model domain. All methods need to be reusable, free from any procedures and of course single responsible. Models should be completely object oriented parts of your codebase and thus reusable. When you enhance a gem for our own domain, have a model that extends the gem’s behavior (I often use delgation for this cases).
Sub-domains for Models (Optional)
First of all, we must make sure models have only one domain. For example, if the model is related to two infras (DB, File system, Outside service etc, the same as non-ruby world), we have to have separated models. But even after that, one model can still be huge. Often there are cases where you can find sub-domain(s) inside a domain model in such situations. You can have a sub-directory with the model name and put those sub-domain(s) into it. Examples are, object builders, factories, logics for composited models (such as associations) or other meaningful groups of behaviors. Some fit well as classes, others fit well as modules to be included inside the main model, but they are both namespaced by the main model name. (Remember rails has conventions between directory structures and class/module names.)
Controllers
Responsible for input processing, sessions, controlling flows and passing the results to view for rendering. They say controllers’ main responsibility is to control the flow. What exactly does that mean? I think it means that we should be able to tell what-to-dos (not how-to-dos) just by looking at the controller. Just write what-to-dos here so that your people can tell what happens in each action.
Views
Responsible for serializing Ruby objects into HTML, JSON or something else with templates.
Helpers (Optional)
Helper methods consumed by views reside here as Rails originally intended. Consider if decorator/presentor(below) is more suitable before you add a method here. No side effect methods exist here.
Services (Optional)
The original definition is here. Service Layer
If you consider models as car parts, services are people who put them together. Like I said above, for most cases, BR(Business Requirement) itself is procedural. If BR is complicated such as when it’s related to multiple models or if BR has a lot of procedures or conditions that our controller cannot handle, we should use a service. That’s what is so-called “application layer service”, whereas we can have “domain layer services”, too.
I have seen a naming confusions though. Some people try to put third party web service domain logics here which I think should go to models. (Again remember ActiveResource was for models)
Concerns (Optional)
The name “concerns” is confusing I don’t get why they adopted this word, although I can guess because concerns extend ActiveSupport::Concern.
The shared logics between models/controllers reside here. If the logic belongs to only one model, probably you should use “sub-domains” above. One example I used this is to save shared Identity Cache modules such as CachedByName
.
Value Objects (Optional)
If you need some computations for immutable objects, we can have value objects. But if that belongs to only one model, we should handle it as “sub-domain(s)” above.No side effect methods exist here.
Decorators / Presentors (Optional)
Logics that belong to a certain model but are consumed by views will come here. Aka presentors. For rails, decorators usually mean presentors whereas decorators has wider meaning originally. Use Active Decorator / Draper gem. No side effect methods exist here.
Cells (Optional)
For HTML widgets that show up across our web application. Use Cells gem. No side effect methods exist here.
Serializers (Optional)
When you want to serialize an object in a customized way, you should have this. ActiveModelSerializers might fit your needs.
Callbacks (Optional)
Extract callback methods from model. You can use the same callback file for multiple models, too, but be careful, callbacks across different domains are a no-no.
Validators (Optional)
Extract validators that inherits ActiveModel::Validator from models. You can checkRails Guides for details.
Socials Aka Policies (Optional)
Not sure who called it policy first, but social modules are to get access token from third party and also to get necessary social graph information. This could be a part of model by nature, but it makes sense to separate this to avoid too many number of models.
-But more importantly-
You should not start with all of the above (YAGNI). You don’t need to separate validations at first. But more importantly, you should always keep things loose-coupled as much as possible (which means, methods should be small and single-responsible) so that you can easily separete things later into reasonable modules whenever you feel the necessity.