Otherwise it's "Rails Project Day 1: Scratch-build a stripped-down ORM. Day 59: That might have been a bit more involved than I expected."
Otherwise it's "Rails Project Day 1: Scratch-build a stripped-down ORM. Day 59: That might have been a bit more involved than I expected."
Controllers actions are allowed a single line of code
I wonder, why do these controllers exist at all?
def show
app_of_things.show_thing(rails_adapter)
end
Is code like this really useful code? It looks like it was written by a very short perl script. Why is he using rails at all at this point, rather than ditching the framework and using a simpler router which talks directly to his app endpoints? What does this controller add aside from a level of indirection which will never be useful?The lack of an example with code running in production makes it very hard to judge where all this is leading or why you would want to do this.
It is very reasonable to question whether this is really all that useful or whether doing it from the start is a premature optimization, but it isn't pointless.
We did have a good deal of success pulling out libraries though, and separating concerns helps with that too. (So maybe I chose the wrong argument to make in my comment before.)
The first was a very large PHP app that ran a fundraising website that processed millions of dollars in donations. It grew organically over time and there was need to change database structure or swap out whole modules of the infrastructure like the site search. It was mostly untestable and really could benefit some automated testing.
Over time the app was moved to a Ruby web API with Sinatra with a decent Service layer with great test coverage. It's been a massive improvement over what was there originally and moved towards more Clean Architecture principles over time. It never went full Clean Architecture because getting team buy-in was difficult and until you feel the pain of a complex, monolithic codebase with minimal tests and a few botched late night deploys, it is hard to feel the pain that Clean Architecture solves.
The second app I've seen that would benefit from Clean Architecture is a Rails app that is pretty vanilla Rails. Normal MVC and all that. Lots of ORM and so on. The tests ran slow and over time the required reporting complexity made the database queries slow and terrible. We've pulled things into a service layer and presenters which helps a bit, but doesn't solve everything. Now we are moving the app reporting into a very functional, immutable approach where we calculate and cache everything all at once in the background. We are only using the models for basic CRUD and for easily pulling out our data structure from the DB, which is then handed to a bunch of functions which calculate everything. It's very much a functional core, mutable shell approach. Even still, this isn't a fully Clean Architecture approach, but it's a lot closer, and it solves a lot of the problems that default Rails MVC doesn't.
Clean Architecture is probably best thought of as something people move towards over time as they feel the pain of not having it. Once you feel that pain, it is completely reasonable to want to start your projects clean and keep them that way, but good luck convincing other developers to join you if they haven't felt the pain.
I agree with the article that rails controllers should not contain business logic. I disagree that writing another ORM layer is a good idea.
By rigidly applying an inane rule like "you can only have one line per controller action", you're going to end up just re-implementing controllers, and leaving your controllers doing nothing but pointless indirection.
I also don't like ceding control-flow from the controller into the application by injecting the controller context in the form of "rails_adapter", in the name of "Tell, don't ask". It seems like a misunderstanding of the principle.
"As the caller, you should not be making decisions based on the state of the called object that result in you then changing the state of the object. The logic you are implementing is probably the called object’s responsibility, not yours."[1]
Say you have a module that performs some complex business logic: ShipOrder.perform(args). It's responsibility might be to ensure the right items are combined into a package and shipped. It might write some logging data to an injected logging object. It might tell a label-printing to output the right address label. Ultimately, it should succeed or fail, and pass that information back to the caller.
The controller's responsibility, on the other hand, is to direct the control-flow of the request and pass data between models & views (and your other non-model POROs). That's its core responsibility. Passing the controller context into your application object so the application can call-back methods on the controller just seems like a recipe for spaghetti code.
It also means you can't use this object on its own, or composed with other objects, without implementing a complex caller-object that you can pass in to receive the callbacks. If the object just returns success/failure, and exposes a limited API so you can access its internal state if required, it becomes straightforward to re-use & compose the object in places outside the confines of your controller.
Doing rendering based on the application telling the adapter what happened does seem to work pretty well though.
I have to disagree, by returning a value and switching on it, not only are you querying state but you risk repeating the query code all over the place.
My approach allows you to write a small adapter for your delivery mechanism and use polymorphism to eliminate conditions and duplication.
Most objects cannot be used on their own as they have collaborators. My design allows me to write a trivial console adapter that can puts the results of my service object / interactor.
Things like authorization should be mostly contained in the root controller and not sprinkled around your controller actions.
What kind of decorators are appropriate to apply to models from a controller action? I'm sure there are valid scenarios, but most of that should also go somewhere else.
def index
respond_with PersonListDecorator.wrap(Person.all)
endEven adding a presenter, as I prefer, serializing your data should be handled by your service layer.
I'm not advocating writing an ORM, just some simple data translations from the raw database row data to a persistence-free domain object.