There's enough context for me to start putting up the horns and cheering, because AR is one of the more persistent boils on my ass when I'm building things in Ruby (because I use Ruby for Ruby, rather than Rails, my frontends are all JavaScript and my services are all Grape). His last point is sufficient to me to demonstrate inadequate separation of concerns--the better design is the one that doesn't require an active database connection to know what fields the object has in it. Answering "how do I build an SOA without every service having a line to every database?" inevitably becomes "don't use ActiveRecord, ever".
What solnic has done with Virtus (which I use in all my projects, it's awesome, he's one of my favorite current Ruby people because of it and I'm sad that he may be leaving) is to return to the novel notion that an object knows what's in it. Dumb data objects are in almost every case a better long-term strategy, and solnic's frustration, if not expressed perfectly, is one that inevitably burns even Rails projects that grow to scale. And while this is a single example of Rails's monolithic nature choking out a lot of the Ruby ecosystem, it's not the only one (see his comments on ActiveSupport).
They are both critcisms of ActiveRecord, for sure, but quite a bit different in content. One (AR objects not knowing their own properties) feels, at least to me, like an unfortunate early design decision, while the other (encapsulating everything involved with saving a model in a single statement, "magic avoidance" be damned) is very much in line with the core philosophy of ActiveRecord and Rails.
Hence "the last point" (though you might have caught me mid-edit). I view these as functionally the same problem: not only are you instantiating the object, but it must immediately go to live in the database or it's useless.
But you're right in that it is a philosophical problem. Rails is wrong. =)
However, just because something is heavily abstracted doesn't mean it's a bad approach, at least from a productivity standpoint. Without certain things being heavily abstracted, we'd go insane.
Just as
User.create(params[:user])
gets translated to thousands of lines of Ruby, so also CoolClass.new('foo', 3)
gets translated to thousands of lines of C, and printf("It's %d degrees today in %s.\n", temperature, city);
gets translated to thousands of lines of assembly.Are they all heavy abstractions? Yes. Is it oftentimes very nice to be able to use heavy abstractions? Also yes.
printf has a well design side effect (printing to stdout). Coolclass.new has an expectation of an instantiation of object in memory. If Coolclass.new also called an external service, then that would be a violation in my opinion. Thankfully, we generally don't do that.
User.create is not only capable of tons of side effects (just like Coolclass.new) but it is encouraged to do so (via callbacks and such).
Also I would like to point that "translates" is a little misleading word. There are two ways of implementing an abstraction from a higher level system to a lower level one.
You can generate code or you can write a runtime framework that does the work dynamically. Although from the higher place both may seem the same, there's a huge difference between them that has consequences.
I wouldn't doubt it; compilers have become amazing at optimizing. But printf is hundreds of lines of C, at very least: http://opensource.apple.com//source/Libc/Libc-1082.20.4/stdi...
And most important, a call is not the same as the implementation of the called function, my point to begin with, please see my comment in the context of yours.
In any case, the complexity of the called function is exactly what we're discussing — it's why the author objects to "User.create(params[:user])"
http://www.monkeyandcrow.com/series/reading_rails/
Sure, Rails is complex, but it takes on that complexity to let us be productive. When you run into that complexity, read the source.
This is very much not Merb's design. But that doesn't make it an invalid design; it just means that understanding a Rails app might best start with the models, not the controllers.
(There's a lot not to like about Rails! It's been years since I worked in it full time and I don't miss it.)
In 2016, with systems regularly decomposing into services for both perf and code management purposes, I'm trying really hard to find a reason to say this isn't an invalid approach except for what amount to toy problems (the kind where "if you don't know where you're trying to go, any road will get you there"). ActiveRecord being a mudball of "what X is" and "how to get X" is a really, really big problem, and my view of it as an observer--heavy in Ruby, heavy in avoiding Rails at all costs--is that it seems to be sourced from an unwillingness of the Rails community to consider that not everything is Rails. Services become intractable, as I note in my reply to your first post--and I don't even mean "microservice all the things", just the profoundly unsexy and unbloggable "SOA".
Source: "The Rails Doctrine" http://rubyonrails.org/doctrine/
I think maybe your broader thesis is we almost always need all 6 things. And if that's true for you then Rails is a good tool for you.
If we only ever need one out of the 6 things at a time then ActiveRecord is only adding extra complexity for us.
I think OP is basically saying I usually only need one or two and that they'd rather not have the extra layer of indirection... "save(andValidate)" is fine for them. They don't need the orchestration abilities that the declarative "afterSave(validate)" step affords.
DHH's suppress really is a good example. If you are calling save(andValidate) everywhere, you can't really do something like suppress(), because your validation logic is disbursed.
My opinion, and I think maybe OP would agree, is that there's usually a nice, composeable way to get the same problem solved, without resorting to a giant engine that understands all the different parts. But that's just an empirical assertion on our part. Obviously your mileage will vary.
I think you want "dispersed" here.
Also, in Django models are explicitly defined. There's no ActiveRecord magic of reading the database to define the model. Which is not to say that AR's approach is bad or wrong, but it is pretty significantly different from Django.
Further smaller changes have been made since then for the same purpose: explicit over implicit, code or configuration over convention is part of Django's philosophy.
IMO that also makes it easier to graduate from a beginning to an intermediate level use of the framework because while there are sane defaults in many places, in others you're required to define your specific implementation explicitly from the start. It's a tiny bit more code up front, but all the details are exposed and it's obvious how to override default behavior. And at the same time the framework supports keeping your code reasonably DRY and takes care of much of the tedious, repetitive stuff.
There's still tight coupling in some places (ModelForms, for instance, are arguably part of the controller layer and are tightly coupled to the ORM). But they're reasonably optional (you can use plain Django Forms instead of ModelForms in your views or not use the Forms library at all.)
My experience with Django (for instance: class-based views and admin parts) is actually that you have dozens of magical classes or methods and if you don't know them all, you're screwed. Often when I try to do something, many aswers on StackOverflow boil down to "just override a_method_that_is_hidden_somewhere()" or "just use ListButWithSpecificBehaviorView(), easy you see?".
Are there any specific example you have in mind where Rails do something magical that requires more code in Django but where Django is more explicit?
PS: not trying to start a flamewar, both are OK-frameworks, bla bla bla ;-)
I feel like people really forget what it's like to be a beginner when they say "read the source".
Now, this line of code is not simple, it’s easy to write it, but the code is extremely complicated under the hood because:
- params must often go through db-specific coercions
- params must be validated
- params might be changed through callbacks, including external systems causing side-effects
- invalid state results in setting error messages, which depends on external system (i.e. I18n)
- valid params must be set as object’s state, potentially setting up associated objects too
- a single object or an entire object graph must be stored in the database
This lacks basic separation of concerns, which is always damaging for any complex project. It increases coupling and makes it harder to change and extend code.
This feels like the core of the argument of the whole post. It does not seem correct. In isolation, the call to User#create seems magical. But there's not enough context here to criticize it. We don't know enough to say whether there's inadequate separation of concerns.
No matter how we handle "users", we are going to need to do all 6 things in the list the author presented. No matter how we structure those 6 things, there is going to be a single entrypoint function that kicks them off --- end users won't be pushing 6 different buttons to drive them. So: what's the better design here?