The MVC architecture, today's standard for at least web development, if abused, often turns into a nightmare of objects sharing a lot of responsibilities, and spanning across different areas, knowing about different objects, essentially defeating the holy grail purpose of separation of concerns.
We shouldn't be polluting MVC, we should be embracing it. There's a reason that monolithic have become a problem, and while I certainly favor micro-services and SOA for complex applications, I'm aware that usually the problem with them it's not having everything under the same directory structure, it's not the team, and it's not isolating components, it's usually the code inside that app that has become a burden to maintain and refactor.
I'm a big fan of isolated presentation logic, logical models, and decorators/presenters and here is a quick post with my opinionated views and resources that will address some topics (mostly in Ruby/Rails development) that will help you keep your code DRY and maintainable.
At the core of your design: DCI
Data Context Interaction (DCI) is a big topic, but I'm going to address it first. It's not only a big topic, it's a big change to the way you program, and the design of your software, I'm in ❤ with it.
If you have the time and the interest in order to explore it, I'll recommend you to do your research, starting at the above resource, and maybe even pick Jim Gay's "Clean Ruby" book on the subject, it's great reading.
Your controller is not your API, sorry
act_as_api, etc. I think is a big no-no to have your controller provide your API endpoints. It gets so convoluted if you're serving multiple formats, but specially I believe its purpose gets blurred along the times, versioning and namespacing becomes an issue (once you're migrating and deprecating versions), parameter declarations and security, and in general, it's something that I wouldn't advise you to try to maintain.
Your API logic should be separate from everything else, (and so everything else from your API), and a way of doing that would be with the Grape API framework. But just don't use Grape, respect the lines, the architectural lines, use only Grape methods in your endpoints, everything else should be on your models, or contexts preferably.
No logic in views
Try to have logic-less views, prefer them, even enforce them. We're being a bit hypocritical for mocking PHP and JSP when we have the tools available and yet we fall back (to a minor extent, sure) to this PHP-itis in larger projects. There's a reason some client-side templating systems avoid all logic.
Here, Draper and Cells would help you, use them, think of a view like "put this here, put this there... I'm done".
And before you go "what 'if'?, I mean I need my
if", remember all conditionals could be replaced with polymorphism, just saying.
No reason for callbacks
I know we need to do "things" after our models get saved or created, I completely agree. However
ActiveSupport::Callbacks behavior is coupled by nature. This has been discussed a lot in the past so I won't rehash it. You can use publish/subscribe instead, and this great post takes a look at it, including my preferred tool of choice wisper (BTW if you're interested in contributing at some point to my MongoDB adapter for it, let me know).
Remember that the benefits of decoupling are "coupled" with the ones of introducing indirection 👌, and that's a good thing. In most cases it allows code and environmental(s) to change without having to change the core. Today it's a simple callback, tomorrow could be something that gets pushed to a background job or to a distributed system, and only tomorrow you'll thank yourself for having introduced this separation allowing you to, in most cases, just switch an Adapter.
Notice that while I do prefer
wisper you can do similar things with
ActiveSupport's instrumentation API.
EDIT: An interesting question was asked on Reddit, which I translated as what would happen when you need to halt the operation based on some condition, that's an interesting concern, I took a quick look at the code in particular, and except for asynchronous scenarios, it looks like raising an exception would have the same effect that it does on
ActiveSupport::Callbacks, but since this a very narrow implementation detail and answer, you might want to research your implementation of choice, however I still believe that generally-speaking the benefits of pub/sub outgrow the costs.
Present logical models
For an API you should be presenting logical models instead of physical ones. Here the tool support is not that broad, but you should certainly not be using the model's JSON representation or a variant of it, you need to be explicit, for security, for correctness.
The best project I've found for this is called representable, when coupled with Roar, and if I might, a project of mine called Radriar.
If you're interested in hypermedia at all, although can be used without it, oat is a very interesting presenter project. I will be expanding on hypermedia API development in Ruby in a future post.
Please review Hexagonal Rails, the Trailblazer project, and actively search for any other attempts to improve the architecture of our apps, which is its most important part from a code perspective.
That concludes what it will most likely be a multi-part series, let me know if you have any comments below; you can also join the Reddit thread.