Objectify: A Better Way to Build Rails Applications(jamesgolick.com) |
Objectify: A Better Way to Build Rails Applications(jamesgolick.com) |
First we have the false modularity. You see a bunch of separate stuff (classes, methods, whatever) and think "Wow, nice, now we can reuse this all over the place!" The truth is that these policies/services/renderers are going to be complected; you're going to create them in triplets as you implement your application. Moving things around doesn't make them modular. They can only be modular if the author actually implements them in a modular way (and you can do that in Rails today).
The DI-framework can't handle change at all:
class CurrentUserResolver
def initialize(user_finder = User)
@user_finder = user_finder
end
# note that resolvers themselves get injected
def call(session)
@user_finder.find_by_id(session[:current_user_id])
end
end
What if I later need the cookies/params to detect user session? If I changes the #call method, every single test breaks.And then there's the added complexity. Now I need to keep track of the route-mapping, default policy, route policies, the route service and the route renderer. Which are now in several different files. And there's not even a clean class-to-file mapping to help me locate it.
Oh, and the premise of testability. Well, this makes it very easy to test units itself, but the more you split your projects into smaller units, the more bugs occur when you mix the units. Sure, you can get your precious fast test runs (and a nice green test), but in reality you're only testing a single unit of a complected app.
EDIT: This reminds my of Rick Hickey's quote from keynote at RailsConf2012:
""We can make the same exact software we are making today with dramatically simpler stuff—dramatically simpler languages, tools, techniques, approaches. Like really radically simpler—radically simpler than Ruby which seems simple.""
Having said this, I respect the attempt, the problem it tries to solve certainly does exist to some extent, and the solution cannot be completely dismissed very easily.
But please, please, do learn both A) basic history and B) basic theory of the field you are working in. If you try to reinvent everything you decrease your chances of contributing something new manyfold.
http://en.wikipedia.org/wiki/Dynamic_dispatch
You also don't have encapsulation anymore, since to be able to perform the majority of the work in the services, the services have to operate directly on data owned by other objects.
How can you combine the policies in ways other than a simple logical AND? How do you combine services?
Others have also already pointed out other issues.
Please don't be offended by the above. Software engineering is unfortunately a field where some valuable content is buried underneath loads of BS, since it is so easy to "philosophize" about it even with just basic programming experience and this leads other people learning the field to easy misconceptions. I do not want to discredit the work you put into this and it might be that in some form something valuable will come from some of the ideas. It is an interesting problem to work on and for me it was an interesting solution to think about, I just don't think you are there yet.
It may work very well, but it's at least as functional as it is OO.
I don't think there is a single right answer that is applicable in all cases. But over time, I've come to believe that as a default, code works well when it resembles how you'd ideally explain the service to an extremely intelligent, non-technical person with domain experience. This makes for code that is easy to understand, collaborate around, and modify given changing objectives.
That means creating "Policy" and especially "Service" models--extra nouns--very sparingly. For example, an Elevator was once a nounification of a verb, but people understood it, so it makes sense as a class. ElevatorDoor's good too, but ElevatorDoorOpeningService is probably not good. This is absolutely at odds with strict application of the Single Responsibility Principle.
This idea of having an ubiquitous domain language and keeping applications and APIs RESTful have been two of the most beneficial ideas I've incorporated into my development in recent years.
http://code.google.com/p/objectify-appengine/
If you want your project to be found easily (on search engines) I always suggest:
1. Prefer a made up word to a non-English word (or a word in any live language for that matter); and
2. Don't choose a name that something else in the same or similar space shares. They've been around longer. It's just going to make your life harder.
YMMV.
Likewise I disagree with the idea that these two projects share the same space. A Java ORM and a Rails metaframework for avoiding ORM are not things which have a lot in common. I think the last time I observed a Rails hacker saying anything about AppEngine was 2007 or something.
What is wrong with writing your core application logic physically removed from any of these frameworks. Why do people insist on fitting their application into the Rails framework or any other web framework.
Write your core application code in Ruby (or your language of choice), package it in another gem, use tools and patterns we've had all along to plug that code into a dependency for persistence (if needed)...communicate with it through a standard API that you define for your delivery mechanism (i.e. Rails).
I just don't get this idea of trying to contain my application code within the confines of a specific framework.
Wrong. I'm hearing this more and more, but let's be clear: Active Record as a design pattern, before Rails, has always been a joining of business logic and persistence. (http://martinfowler.com/eaaCatalog/activeRecord.html) It's always been in violation of the single responsibility principle. If you want to avoid that, don't use Active Record! Use a Data Mapper, which specifically separates Mappers and Finders from Domain model classes. (http://martinfowler.com/eaaCatalog/dataMapper.html)
I'm not saying you can't move logic out into reusable modules or whatever is being recommended here, but please don't make it out like people are in the wrong when they're using the library or framework the way it was designed to be used.
Care to give an answer better than "some book some java dudes wrote 20 years ago said so"?
While I generally agree with SRP, when that single responsibility can be expressed in a single function, I don't see the win with have a class dedicated to it. Smells of pedant OO nonsense.
Is not instantiation etc a cross-cutting concern that violates SRP in this instance?
I think I'll have to give objectify a try and see how it all fits together. It's a different enough approach to what I'm used to that I probably don't have a reliable intuition as to how it'll turn out in practice.
He echoed the phrase I've seen elsewhere "Ruby just doesn't need that".
I disagreed with him then, and still do. People like to claim that OOP breeds cargo cult programming and overly cumbersome abstractions upon abstractions (and it can), but some times you really do need that kind of approach to make a large scale project testable and maintainable.
I'm glad to see there are Ruby devs who can see the value in using these kind of approaches. I honestly believe it's the kind of thing you think is a major waste of time, until you are shown the benefit first hand, and you experience the change. Then you start to understand that by spending a significant deal of time up front building your infrastructure, you can save a lot of time down the road.
I know that's how it was for me years back.
It's pretty clear, by now, that we (the ruby community) were wrong about how to maintain projects over time without getting mired in complexity.
I personally think that writing reusable libraries using Decorator/Presenter patterns or Modules with ActiveSupport::Concern is simple approach.
Avdi's slides: https://speakerdeck.com/u/avdi/p/making-little-classes-out-o...
I highly recommend Objects on Rails book: http://objectsonrails.com/
>refining for a while now [3]. In this approach, persistence
>objects remain extremely thin, and business logic is
>encapsulated in lots of very simple objects known as
>“services” and “policies”. Not all objects in this
>methodology will fit in to one of those two categories,
>but they are two of the most important concepts.
I've seen this division of labor outlined in this DestroyAllSoftware screencast : https://www.destroyallsoftware.com/screencasts/catalog/fast-...
As can be implied by the title, one of the side effects of keeping your service logic in their own separate plain Ruby classes is that you don't have to load Rails as dependency which means tests can run _really_ fast.
Update: not saying that's a bad thing I used to be a Flex developer and all the frameworks used MVCS (Robotlegs being my favorite). Controllers are all commands and Services are used to retrieve data.
Emphasis mine. I don't think this has to be true. Yes, models get fatter, but the responsibilities will have to be implemented somewhere and to me it's simpler and easier to debug when it's as close to the domain model as possible instead of in some abstract/generic model.
Of course, if you have policies and services that apply across domains, then by all means break them out and recompose as necessary in the the models
Sounds nice, would be fine with it as a default, but not sure it warrants doing a big change for me.
[instance methods] --- [composed subclasses] --- [mixins] --- [the outside world]
But Modules provide more than that. module_function allows for unattached namespaced functions which can be referenced by full name, relative name, or imported into the local scope. Great way to express functions which are decoupled from instance state--much like the services and policies you're advocating for. It's an under-appreciated aspect of the language, I think.
The vast majority of people who scale Rails sites (as far as I can tell) do so by breaking their apps into services. ActiveRecord god-objects do not make that step in an app's life cycle easy, and separating persistence from business models is likely to be your first important step when refactoring for scalability. Fragmenting a User model bloated beyond all hope of sanity is almost a rite of passage at this point.
I often think the only Rails developer who hasn't found ActiveRecord bloat to be an irritation and an obstacle to scaling is DHH, and although that makes his opinion on the subject even more valuable than it would normally be, he doesn't talk about it enough in my opinion. Pretty much everyone else, as far as I can tell, responds to Rails scaling issues by creating services in Sinatra and/or divorcing business modeling from persistence logic.
I want someone to challenge my prejudicial assumption here, most of all because I appear to be saying "Rails can't scale," which is BS, but also because I'm very tempted not to take this discussion seriously at all if it doesn't address this point. It's just the crucial point in my opinion. I like the whole OOP thing but to me the decision to use something like Objectify is all about scalability. That doesn't just mean performance; it also means retaining readability when you have a lot of code. Single Responsibility Principle makes code readable.
- Splitting out logical pieces of behaviour into separate modules, especially the ones less central to the core responsibilities of the class. If you look at Rails itself (it's great Ruby code, I highly recommend really reading it), this is the way it is structured, ActiveRecord::Base provides tons of functionality as a single class, yet it still is very neatly laid out into many well-separated modules. See e. g.:
https://gist.github.com/1014971
http://api.rubyonrails.org/classes/ActiveSupport/Concern.htm...
https://github.com/jakehow/concerned_with
http://blog.waxman.me/extending-your-models-in-rails-3
Deciding what things would fit well into external modules and what modules to create is a new skill to be learned, but I think it can work well once you do learn it.
- Things that aren't part of business logic but just handle some more technical matters should be extracted into plugins and not be directly part of the models (for example: special kinds of validations).
- There is the whole world of OO techniques and patterns that can be applied here just like anywhere else. For example you can extract complicated algorithms into separate classes or use value objects:
http://api.rubyonrails.org/classes/ActiveRecord/Aggregations...
"If you look at Rails itself (it's great Ruby code, I highly recommend really reading it)"
I've worked with a member of Rails core, and frequent committers. I've spoken at 10 Ruby conferences at least, maybe 20. I'm familiar with the Rails code base. I once rewrote a few small pieces of ActiveRecord from memory for a stupid project because my wifi was dead.
I agree about it being worth reading, I just want to slow you down there for a second. Otherwise we're going to end up pulling things out and measuring them. I took the time to check your links, although I'd seen most of them already. I'm familiar with concerns and although I think your links are germane I'd have to disagree with this part:
"Deciding what things would fit well into external modules and what modules to create is a new skill to be learned"
As I think I've learned the skill.
What I'm saying basically is that I think you missed the mark in terms of guessing my background but I believe it was an honest mistake.
Egos aside, I agree that modularity matters. And I must admit I had a painful reminder about that skill recently when I built something around ActiveRecord and then refactored ARec out of the picture for performance reasons (the gains were staggering). It would have been less painful with better modularity, but I don't think the pain relief from using concerns would have been particularly great in this instance. Non-zero, but insignificant.
I don't care that much about OOP theory, but the pragmatic issues Objectify addresses seem to come up for people all the time. A year ago Steve Klabnik and a few other people were talking about this and I noticed it then how it was echoing James Golick's (the OP) posts about limiting ActiveRecord's role from a few years before that.
There basically seem to me to be three trends in discussion of scaling Rails: 1) "Rails can't scale" FUD, 2) DHH saying "just add hardware and/or caching", and 3) this idea of decoupling persistence from modeling, basically, reining in ActiveRecord, which I've heard from people like James Golick, Steve Klabnik, Gary Bernhardt, and I think Xavier Shay. Paul Dix wrote a great book about making Rails apps service-oriented, the Thoughtbot guys touch on it in their antipatterns book as well, and it's not exactly a coincidence that for many Rails apps the only gem more crucial than ActiveRecord is Delayed Job or Resque.
I don't think the problem people seem to have with Objectify is that they see it as a totally invalid solution to a problem--and I agree with your assessment that many people see this problem as very real.
My own take is that I find it disingenuous to call it an Object-Oriented solution to the problem; it's not. That doesn't make it bad, invalid, or wrong. It rightly raises a few eyebrows to talk about effectively calling namespaced procedures as if it were the pinnacle of Object-oriented design.
Anyway, the whole discussion so far was about dealing with the problem of large (in terms of code size) models, so I assumed by "scalability" you mean scalability in terms of being able to handle very large code bases, not performance issues. I do not understand how the approach presented here would result in performance improvements? I've commented elsewhere on the problems associated with it, others did as well in a fairly convincing way I think. I do not deny that problems with scaling both in the performance sense and in the code size sense do exist, I just do not see how Objectify solves any of them and I do see how many problems it introduces.
As for breaking your app down into separate HTTP services, I agree this can be useful in some situations, but it is completely different from the approach outlined in Objectify, for one you still get a chance to do (almost) normal OO modelling and structure the code in a reasonable way, even if the message passing is done via HTTP and not simply via Ruby method calls.
You'll have to decide if he made his case or not, but if that's what he thinks then better he just come right out with it instead of dancing around.
Note that he did not call James ridiculous, he criticized the idea. Which is how it should go (if there's going to be criticism).
You can debate whether his assertion is justified, but some (many, perhaps) ideas really are fucking ridiculous and it's better to just cut to the chase.
As the lead developer of the Objectify project, and someone who makes a living through consulting related to Objectify, I am highly annoyed. I'm also talking to a trademark attorney to find out my options.
If you want to have a respectful debate, I'm completely game. If you'd prefer to continue with your incredibly condescending tone, then I'll bow out here.
I apply it mostly when I'm writing something and am getting the feeling that it might not come across so well.
http://www.rightattitudes.com/2008/02/20/sandwich-feedback-t... (from a quick Google)
Exhibit A: Whatever you think about James's choices in this library, he has clearly thought a lot about OOP. It's just silly to start a comment with "You have to think a bit more about what OO really is about."
I'm beginning to be convinced this is almost never an appropriate thing to say.
http://en.wikipedia.org/wiki/Dataflow_programming
Parts of this approach might be valuable, as I tried to emphasize, but as a whole this doesn't look good for plenty of reasons mentioned in the thread.
> it is not really functional programming either, since there are no direct function calls.
There don't have to be. The classes and instances are being referred to by the developer in the same way as any first-class functions defined elsewhere would be - they're actually resolved and called by the framework.
> Since all the flow of the data is described in the routes file, it isn't OO at all and mostly reassembles dataflow programming as I said
It certainly is OO. There are instances, and they receive and act on messages. Yes, there's a dataflow-like DSL for routing, but that's not to the exclusion of other paradigms.
This is the dependency injection part, not the policy/service/responder core of the framework I am talking about.
There don't have to be. The classes and instances are being referred to by the developer in the same way as any first-class functions defined elsewhere would be - they're actually resolved and called by the framework.
I am not talking about what happens on the level of Ruby, I am talking about what happens logically at the level of the framework. The classes represent functions, sure, but they do not call other functions (services/policies/responders), the only "function-calling" is done by the router. Yes, this too is a form of functional programming, the form known as dataflow programming where the flow of the data is described explicitly.
It certainly is OO. There are instances, and they receive and act on messages. Yes, there's a dataflow-like DSL for routing, but that's not to the exclusion of other paradigms.
It isn't OO at the level of the framework. Yes, Ruby is underneath, there are classes, there are instances, and the thin ORM layer is object oriented, but the objects within the framework do not exchange messages, the only object doing all the message dispatch is the router. This is not OO, just look at the traits listed here: http://en.wikipedia.org/wiki/Object-oriented_programming
The framework basically forces you to abandon messaging (since there is no exchange of messages, just single directional messages flow from the router), abandon encapsulation (since the services have to access the data of the domain objects and hence the domain objects must make their data available to everyone) and abandon polymorphism (since the services are statically dispatched by the router). How can this be object oriented?
Also, those are not really single-method classes, but single-method interfaces. Since you didn't give any arguments, I don't see how those two extra lines to define a class have any disadvantage in comparison to a global method, and having the framework define a interface to program to leaves more flexibility to the framework user than having the framework accept a method name or block instead. The instatiation of the class can be left to the framework user (in general, I don't know if this is the case here in particular) so that additional data can be attached, inheritance can be used (yes, inheritance has its problems, still it also has plenty of valid usage scenarios), methods broken down into smaller private methods etc. Also in Ruby there are no global methods like you would have in Python or C++, you always define a method of some class, even if it's in the top-level scope - files almost always correspond to individual classes so it would be pretty odd to have methods used here instead.
Right.
Which is why hes also recommending the use of DI.
It's hard to strike a balance between a strong design which takes a lot of time to work within, and one that just allows you to get things done, but once your project begins to grow in size and complexity, and testing becomes even more important, these things really do start to matter.
It's difficult to internalize until it's bitten you on the ass, hard.
That said, if you have a large amount of single method classes, you might need to take a look at why you need them and find a better approach (which will vary from project to project).
The whole point of objectify is that it actually makes it pretty reasonable to work this way right off the jump. I would have a hard time believing that it's really any more work to build an objectify app than it is to build a vanilla rails app - at least once you become accustomed to the paradigm.
> That said, if you have a large amount of single method classes, you might need to take a look at why you need them and find a better approach (which will vary from project to project).
I don't buy that. My project has hundreds of single method classes and it's by far the best factored non-trivial application I've ever seen (anecdotal, obviously, but so it goes).
Sure. I wasn't saying otherwise. What I meant was, in a general sense (prior to really seeing the benefits first hand), it can be hard for people to justify the trade off when they feel like they can just "get it done faster" with a more ad-hoc approach to things. I didn't mean to imply Objectify was some how more difficult (I haven't used it, so I wouldn't dare comment on it in such a way).
>I don't buy that. My project has hundreds of single method classes and it's by far the best factored non-trivial application I've ever seen (anecdotal, obviously, but so it goes).
Agree to disagree, with a caveat. I'm not against having a large amount of "Helper" or in the case of what I'm most familiar with, "Extension" classes (godbless you, .NET Extension classes), but I think if you have so many that they comprise most of your code base, maybe you could consolidate some of them (and maybe you couldn't).
Personally, I try to keep 100% of my core business logic inside of "Service" style classes, and then anything that is more of a helper (in that it performs some work, but not "business logic", and does not require or alter internal state) in either a static helper, or an Extension on the type itself.
For example, I usually end up adding a .ToStart/EndOfDay/Week/Month suite of methods to the datetime object of most of my projects. A "DateTime Service" would be overkill, but an extension method (which is just veneer over a static helper) fits the model perfectly, in that it returns a new copy of the existing datetime, leaving the original intact.
But at this point we're getting pretty deep into the comment tree, so feel free to email me from my profile if you want to go more into the issue - I love shooting the shit over this kind of stuff. Regardless of any minor difference of opinions, I think what you're trying to bring to the rails community is to be commended.
Thanks a lot, man.
The services receive messages from the router, then send messages to whatever they need to call to get the job done. Being sent messages by the framework is what makes it a framework and not a library; I don't get your objection here.
> abandon encapsulation (since the services have to access the data of the domain objects and hence the domain objects must make their data available to everyone)
Only if you're conflating domain objects with persistence objects.
> and abandon polymorphism (since the services are statically dispatched by the router).
They're statically dispatched on a constant name, not on a class. If you need polymorphism at that point, it's trivial to define a dynamic service resolver - all you need is something with a #new method assigned to the constant to do whatever late binding you want.
If it was a framework modelled in an object oriented way it would itself be built of classes exchanging messages, but it isn't. It encourages you to also structure your code in a non object oriented way by creating hundreds of little services which are basically procedure calls instead of making your model accept a wider variety of messages or instead of building classes with more than just a "call" method. Most of the code is supposed to go directly into the services, that's the whole point of the framework, remember it is a solution to the "fat model" problem:
One in particular is something I've been thinking about and refining for a while now [3]. In this approach, persistence objects remain extremely thin, and business logic is encapsulated in lots of very simple objects known as “services” and “policies”.
Also, keep in mind that the services do not normally call each other (they could in theory, but that would be even more awkward), so instead of having message exchange between models you have duplicated code in services or have to use inheritance. Ruby is object oriented so sure there are message calls further down, it doesn't make the framework object oriented on the logical level.
Only if you're conflating domain objects with persistence objects.
Must you go as far as to refer to a hypothetical reality just to win an argument ;) ? That's how Rails works, that's what the examples show, I am not referring to what is possible in theory, but to what the framework does and the most reasonable possible way of using it. Honestly one more level of indirection would make this look completely ridiculous.
They're statically dispatched on a constant name, not on a class. If you need polymorphism at that point, it's trivial to define a dynamic service resolver - all you need is something with a #new method assigned to the constant to do whatever late binding you want.
Ehm, no? You cannot just define a "new" method returning whatever you want neither in a module nor in a class in Ruby. You would have to overwrite the initialization methods that are usually hidden from the user. Passing a constant name is almost equivalent to passing a class.
module Foo
def new
5
end
end
puts Foo.new.inspect
# undefined method `new' for Foo:Module (NoMethodError)
class Bar
def new
5
end
end
puts Bar.new.inspect
# #<Bar:0xa146324>Eh? The sentence you quoted explains that putting domain logic in the persistence objects is precisely what objectify avoids. I do't see any examples of domain logic as separate from persistence logic at all in the examples, so I'm not sure where you're getting the idea that the framework encourages it from. If you're that tied to "how Rails works", why are we even discussing this? Objectify exists to change the way Rails works, because the way Rails works is broken.
> Ehm, no?
Ehm, yes.
class MyFunkyResolver
def new
5
end
end
Foo = MyFunkyResolver.new
p Foo.new
# 5I don't understand what you are trying to say. As far as I remember we are discussing here whether Objectify breaks encapsulation. In plain Rails you would have business logic together with persistence in a single class, and the data would be available to the external world only to the extent the business logic allows it. With Objectify all the data in the persistence objects has to be public for the services to be able to access it, and all the business logic is in many small services with only a single "call" method. Do you consider this to be good encapsulation? I am not tied to how Rails works, but I cannot judge all the possible ways of using Objectify, I can only judge what was presented/encouraged in examples and in the blog post and assume that you will use standard Rails practices in the rest of places, since it's in the end a Rails plugin and not a separate framework.
You are right regarding polymorphism.