Hacker News new | ask | show | jobs
by patio11 4477 days ago
This feels like it would be the sort of architectural decision which would be well-served by saying "Here is the app motivating this stunning architectural purity. You will note that this app both exists and was more complicated than the 5 Minute Build-A-Blog demo."

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."

4 comments

My thoughts exactly. On reading:

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.

Exactly. If they are only one line, why not just have the "app code" handle the routing as well?
Well, to separate concerns. ActionController does what it's good at (routing and rendering), and your application does what it's good at (business logic). One tangible benefit of separating concerns in this way is that it makes it possible to use the application itself as a module behind different adapters. So you can put it behind a raw tcp api, or a 0mq bus, or a command line front-end, etc., without needing to change anything but the adapters.

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.

I hear this often but when the frack do you ever interact with a raw tcp api or a 0mq bus that's existentially the same application as your web app??
Well, it's interesting. My example is partly contrived and partly not contrived. I worked on a big application where the web front end was very much the tip of the iceberg. We had the Rails monolith problems that you frequently hear decried. One thing we at least thought we would really like to do was move to an architecture with a bunch of little services talking to each other. We thought about having them talk to each other over tcp or 0mq, and we thought it would have been really nice if it were easy to take our existing application logic and put tcp or 0mq adapters on top of it. I keep saying "we thought", because we never actually did any of that, which may have been the case either because our application logic was so tied up with Rails that it was impractical to pull it apart (as some of us thought), or because it's a silly idea to begin with.

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.)

It is useful, the controller in this case handles web concerns and translates a HTTP message into a message that is meaningful to application.
I've worked on two large applications that would both benefit from Clean Architecture and they don't feel like outliers, they feel like the natural progression from any MVC framework to an app of significant complexity.

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.

Most developers using this type of architecture are working on projects for clients that wouldn't appreciate having their code slapped up on pingpongwithdhh.com

I agree with the article that rails controllers should not contain business logic. I disagree that writing another ORM layer is a good idea.

There's a difference between "controllers should not contain business logic" and "controllers must be one line". There's plenty of sane responsibilities for controllers that don't directly involve business logic - mapping request parameters to something the business layer understands, coordinating view mapping, applying decorators to models, authentication, authorization, etc.

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 agree with this. Statements like "you can only have one line per controller action" are just dogma. What's the point of a controller?

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.

1. http://pragprog.com/articles/tell-dont-ask

Yeah I agree. I'd be interested to see what that `rails_adapter` implementation looks like. In my experience, this sort of design often just moves a lot of the tricky questions about how to pass data around to the adapter instead of the controller.

Doing rendering based on the application telling the adapter what happened does seem to work pretty well though.

Hi Tom

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.

I agree, but I see "one line per controller action" as a guideline that doesn't need to be followed religiously. Almost every rails app that I come across has too much domain logic in the controllers. I'm refactoring an ecommerce app where the developers put 400 lines of payment processing code inside one controller.

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.

If you had a view decorator that applied presentational logic to a model, I don't see a problem at all in including that in a controller. I wouldn't have any issue at all with a controller action that looked like:

    def index
      respond_with PersonListDecorator.wrap(Person.all)
    end
Yeah I think that is a good idea. But to be fair: your example is only 1 line and has no business logic.
I honestly can't think of any job I'd be happy leaving in the hands of a controller beyond the obvious HTTP routing and view rendering concerns.

Even 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.

So I'm not actually advocating writing an ORM just a data mapper. You map from one hash of data extracted form data base to something like a Struct. There's no SQL generation involved.