15 ms·
Rails – The Missing Parts
- ollysb 13y agoPerhaps because it's written with examples in java but I often feel like no one in the rails community has ever read Eric Evan's Domain Driven Design[1]. It's far and away the best material I've ever seen on how to organise large code bases. It covers pretty much every suggestion that I've seen from the rails community. Sometimes the rails community can feel like the fitness industry, everybody just rebranding things that have been done before. [1] http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215 http://www.amazon.com/Domain-Driven-Design-Tackling-Complexi...
- hiphopyo 13y agoIs http://tech.taskrabbit.com/blog/2014/02/11/rails-4-engines/ http://tech.taskrabbit.com/blog/2014/02/11/rails-4-engines/ another one of those?
- antonydenyer 13y agoprobably because in most cases the domain model doesn't get that complex.
- antonydenyer 13y agoprobably because in most cases the domain model doesn't get that complex.
- tomblomfield 13y agoSure - I don't think anyone in the Rails world is claiming to have invented these principles. The problem is that Rails ships with a very limited set of core architectural concepts, and many inexperienced Rails developers feel like they've got to cram all of their code into a Model, View or Controller. Once your codebase reaches a certain complexity, principles from other programming paradigms are extremely useful.
- pothibo 13y agoProblem with your assumptions is that inexperienced developers do not understand architectural concepts. So you could cram every design pattern known to man and they would still cram everything in their controller. API's are hard to discover and it takes time through trial and errors or being teached the way things are.
- philwelch 13y agoInteractors, domain objects, service objects, etc. are still models; people just don't generally believe that their models are allowed to inherit from things other than ActiveRecord::Base.
- mst 13y agoI keep looking at interactors and thinking "I'd make this a command object and then hide it in the model" - so I'd be doing something like (inside the 'member' class) method confirm_grouper () { MyApp::Action::ConfirmGrouper->new(leader => $self) ->run } and then the controller would simply do - my $result = $member->confirm_grouper;
- ulisesrmzroche 13y agoThey're not models, they're Objects. A model is an object but all objects are not models. The difference is what sort of role they play in your app. A Service Object work, could just echo hello world every 15 seconds and still be called a Service Object, while a model in your application doing the same thing can no longer be called a Model.
- rubiquity 13y ago> "The problem is that Rails ships with a very limited set of core architectural concepts, and many inexperienced Rails developers feel like they've got to cram all of their code into a Model, View or Controller." The problem is that people think Rails is an architecture to begin with. Rails is just a framework that uses the MVC pattern (mangled slightly to fit the realm of HTTP). In the end, MVC is nothing but a directory structure for our files. What you put in those files and directories is up to you. Uncle Bob gave a great keynote on this called Architecture: The Lost Years. Here's a link: http://www.confreaks.com/videos/759-rubymidwest2011-keynote-architecture-the-lost-years http://www.confreaks.com/videos/759-rubymidwest2011-keynote-...
- kohanz 13y agoI would argue that both the Rails community and fitness industry may operate that way, but for good reasons. Any time you have an ongoing an significant influx of beginners, looking to hit the ground running, bad practices will be everywhere. A handful of established "bibles" or textbooks aren't going to be enough to get the message out to the masses. It may not be appealing to those already "in the know", but good practice messages need to be repeated and communicated in new ways. You also see the same dynamic at the gym, of the experienced gym-rat lamenting the things that personal trainers get paid to teach newbies. In that same vein, this may be a repeat message of a concept that is not novel, but as an experienced non-web developer who is fairly new to Rails, I learned something from reading this. I also appreciate you pointing out what sounds like a good development resource (Domain Driven Design).
- rasur 13y agoIn my first Rails job, I was handed a copy of this book to read. I would agree with your comments, although I cannot speak for the Ruby Community as such. Well worth reading.
- ollysb 13y agoThere's also a condensed version for free over at infoq[1](requires registration, but definitely worth it). [1] http://www.infoq.com/minibooks/domain-driven-design-quickly http://www.infoq.com/minibooks/domain-driven-design-quickly
- tuke 13y ago"The nail in the coffin is ActiveRecord callbacks; before_save hooks on one class that modify the state of other objects." before_save can modify the state of other objects. It doesn't have to. One might use it to modify only the state of the current model. You are reaching a bit, blaming the callback, when what you are really concerned with is an awkward and potentially dangerous use of the callback.
- tzaman 13y agoWe tried using DHH's concerns in place for interactors, but ditched them for PORO/service objects because they can be tested outside Rails.
- rubiquity 13y agoI don't like Concerns because usually they are just hiding behaviors in another file (I do like routing concerns though!). You can test Concerns outside of Rails though, all you need is ActiveSupport which actually loads quite fast. require 'active_support/concern' require 'my_cool_concern' class DummyClass include MyCoolConcern end
- Smudge 13y agoThis assumes that MyCoolConcern doesn't depend on too many things in your main model class or in (god forbid) other concerns that you've included in that model. Or, perhaps, to turn this argument around, it's better to say it this way: A Concern is poorly-constructed if it cannot be tested in isolation, so therefore something like MyCoolConcern should be written with its testability (inside of DummyClass) in mind.
- tomblomfield 13y agoYeah - I feel like concerns are a bit of an anti-pattern, especially as they're normally used in Rails. You've got a God class that exposes 500+ public methods, so you split it into concerns. But now you've still got a God class with 500 methods that's split across 10 files. Good luck understanding that. Concerns are useful when they're genuinely sharing functionality between classes - not just for splitting up "Big Bags O' Methods".
- epidemian 13y ago> Concerns are useful when they're genuinely sharing functionality between classes - not just for splitting up "Big Bags O' Methods". Hmmm. I'd say it depends. Of course having concerns (a fancy word for modules IMHO) shared between classes is great. But i've also found cases where separating a god class' behaviour into different concerns helped a lot. The important thing in those cases was recognizing and avoiding dependencies between different concerns, so instead of ending up with a god class split across many files of inter-dependant code (i.e. worse than before), you can end up with a "god" class split into its minimal "core" part and a couple of concerns that only depend on that core part but not between each other. Yes, you could still call that a god class, as it will have a very fat public API, but at least you can reason about what it does in terms of different concerns, so you gain something :)
- casey_lang 13y agoAlong the same line but heavier weight than 'Interactor' is 'ActiveInteraction'[1]. Responds in much the same way active record does, allowing validations, errors, forms built from interaction objects, etc. [1] http://github.com/orgsync/active_interaction http://github.com/orgsync/active_interaction
- pothibo 13y agoOne thing I really enjoy with this post is the conclusion. It doesn't try to tell you it's the only way, just that it's the way they found to fix their problem with the constraints they had, as it should be. Rails is easy to extend, people often forget that. Great post.
- Axsuul 13y agoI've been looking into incorporating this pattern into my larger Rails apps as well. Another benefits of interactions is DRYing up your code for use in APIs. Some of the more popular interactor gems: https://github.com/orgsync/active_interaction https://github.com/orgsync/active_interaction https://github.com/cypriss/mutations https://github.com/cypriss/mutations
- taylorfausak 13y agoThanks for the shout out! I'm one of the developers of ActiveInteraction. Since most of the interactor gems are pretty similar, here's why you might prefer ActiveInteraction over Mutations, Interactor, et al.: - Built-in ActiveModel validations. - Designed to be used with form objects. - Easier and more flexible composition. - Translatable with I18n. - Quacks like an ActiveModel. That being said, Mutations is a great library for PORO interactors and ActiveInteraction wouldn't exist without it.
- rubiquity 13y agoIf you need a gem to implement some of these complementary patterns, you're doing it wrong. Draper, Naught, DisplayCase and all of the other gems of patterns are over abstraction 99% of the time. Implement the pattern yourself. Most of these Presenter and Service object patterns are different variations of the Decorator pattern. Ruby happens to ship with 3 great ways to implement decorators: SimpleDelegator (my personal favorite)[0], Delegator[1] and Forwardable[2]. 0 - http://www.ruby-doc.org/stdlib-1.9.3/libdoc/delegate/rdoc/SimpleDelegator.html http://www.ruby-doc.org/stdlib-1.9.3/libdoc/delegate/rdoc/Si... 1 - http://www.ruby-doc.org/stdlib-1.9.3/libdoc/delegate/rdoc/Delegator.html http://www.ruby-doc.org/stdlib-1.9.3/libdoc/delegate/rdoc/De... 2 - http://www.ruby-doc.org/stdlib-2.0/libdoc/forwardable/rdoc/Forwardable.html http://www.ruby-doc.org/stdlib-2.0/libdoc/forwardable/rdoc/F...
- ryanbrunner 13y agoThe biggest pain points that gems like Draper solve are allowing view helpers in your decorators (which is by no means straightforward when you get into routing helpers), and dealing with some of the strangeness that you get into by wrapping ActiveRecord models (JSON serialization was a biggie for us). It's less about what Ruby gives you, and more about navigating around some of the darker, messier corners of Rails.
- edwinvlieg 13y agoWe've also come across the limitations of Rails as a one-size-fits-all solution for larger codebases. I don't think this is a fault of Rails, but more of the developers using the framework. Software engineering in general has more focus on a wide variety of design patterns and doesn't limit itself to one framework. In my opinion, Rails is the best framework to create wonderful web applications. Modeling business logic requires different kind of architectures than the simple MVC-ish structure Rails provides. At MoneyBird we are using the Mutations gem to represent something like the interactors mentioned in this article. One major advantages of Mutations, is that is also does input filtering. https://github.com/cypriss/mutations https://github.com/cypriss/mutations
- wdewind 13y agoThis is great, and does not only apply to Rails. In many other frameworks it's common to lump business logic in controllers instead of models. Either way you end up with the same thing: eventually you'll need to add a 4th (at least) type on to MVC. > Here at Grouper, we’re long-time users of Ruby on Rails – along with other New York startups like RapGenius, Etsy and Kickstarter. Etsy doesn't use Rails though, it uses PHP.
- tomblomfield 13y agoFixed, thanks
- troels 13y agoOne thing I can't figure out is whether to place my service objects under `/app/models` or under `/lib`. There doesn't seem to be a clear consensus on this? I tend more towards the former, because autoload works better during development. Also, I consider them part of my domain model. What do you do?
- lengarvey 13y agoThere's nothing stopping you from creating extra folders under /app to contain various types of service objects. Personally I have: /app/factories - for classes which create active record objects /app/services - for classes which do something I generally don't care about the return value of. Often perfect for background jobs. /app/decorators - most of these are Draper classes /app/forms - form objects and a few others from various other gems.
- bphogan 13y agoI have app/services and just add that in application.rb to the load path.
- te_chris 13y agoYou don't even need to add that to the load path as of rails 4 - we have app/validators, app/services, app/forms, app/inputs etc etc, all autoloaded with no config.
- Legion 13y ago/app/interactors It's a trivial thing to add new folders like this to autoloading: # add to application.rb config.autoload_paths += %W(#{config.root}/app/interactors)
- nthj 13y agoAs of Rails 3.0, I believe app/anything/ is autoloaded: * https://github.com/rails/rails/blob/3-0-stable/railties/test/application/initializers/load_path_test.rb#L43 https://github.com/rails/rails/blob/3-0-stable/railties/test... * http://hakunin.com/rails3-load-paths http://hakunin.com/rails3-load-paths
- clutchski 13y agoI thought Etsy uses PHP. Could be wrong.
- SpikeGronim 13y agoI can confirm that Etsy.com is primarily PHP. We have no externally facing Rails projects. We have some internal tools on Rails. -- spike@etsy.com
- tomblomfield 13y agoSorry, fixed!
- seancoleman 13y agoA good litmus test of an experienced Rails developer is how large their /lib directories are in relation to project sizes.
- nimblegorilla 13y agoIs a large /lib a sign of experience or inexperience? I try to keep as much as possible out of /lib and moved into gems.
- awj 13y agoInexperience. lib/ quickly turns into a dumping ground unless you are careful to keep it clear of business logic.
- dhh 13y agoBy this I hope you mean that the smaller it is, the better. /lib is intended for non-app specific code. The halfway house before code is extracted into either a Rails PR or a separate gem.
- ryanbrunner 13y agoEven better is realizing that you can put whatever you want into /app, and having /app/presenters, /app/decorators, and /app/services directories in your application.
- avitzurel 13y agoAs always, the question is should be in Rails or not. I think that the answer is clear, this should not be a part of rails since it's not true to all apps. I think that if you want to have something real quick, you don't need an interactor/service class. When you have a bigger app, you definitely need that, or you will get to a point where you code is split into models/observers/callbacks/lib/app/concerns and you can't find anything. Rails generators are pretty easy to extend, this way, when you generate a new model, you can easily create the service class for it. You can also EASILY create new generators that will generate the service classes for you with your defaults and templates and what not. Not sure Rails is missing that, however, it is definitely a best practice that people with bigger apps should use TODAY.
- ryanbrunner 13y agoMailers, concerns, helpers, observers and a lot of other things in Rails aren't universally used by all apps. Even something as simple as an empty directory and some generators in a stock Rails app would go a long way in educating people that these are patterns that you can use / provide a common language for where these sorts of things should go and what we should call them.
- nwienert 13y agoI've found a happy balance developing with rails by adding two strategies: 1) Use presenters for display-only logic to keep controllers concerned with managing requests only. 2) Using service object / custom classes in /lib for actions on models as well as abstracting any common related functionality. Creators, Updaters, Processors, Orchestrators, etc. Keep your models only concerned with data and not data transformation, and your controllers from doing it as well.
- tomblomfield 13y agoYes, definitely. One of the future posts (coming soon!) will be on Presenters / Decorators. I would argue these custom classes should still live in /app, rather than /lib, but I may be wrong.
- jhund 13y agoI have documented my strategy for moving from a simple MVP to a maintainable and complex Rails app. Basically I look at factors like how many objects are affected by an operation, and how complex the additional processing is. TL;DR: Use Rails magic when possible, and Object Oriented best practices when necessary. I created a flowchart that illustrates this strategy: http://rails-recipes.clearcove.ca/pages/how_to_change_objects.html http://rails-recipes.clearcove.ca/pages/how_to_change_object...
- kapilkale 13y agoCan anyone recommend a github repo of a Rails app implementing interactors or any of the similar concepts listed in the comments?
- ahawkins 13y agoI humbly submit my own work: http://hawkins.io/2014/01/rediscovering-the-joy-of-design/ http://hawkins.io/2014/01/rediscovering-the-joy-of-design/
- dhh 13y agoThe proof is always in the pudding. While there are good and reasonable times to introduce "interactors", this particular example is poor. The tests presented are anemic, and the code is absolutely not any clearer by being extracted and wrapped. I would indeed have kept all this in the controller. The key point for an "interactor" extraction is imo when you have multiple models being created in symphony, like a Signup model. Or if you for some reason need to reuse the behavior. But if all your controllers look like this, with one "interactor" model per action, you're doing it wrong. Whatever floats your boat, though. If this is what you prefer, great. But please hold the "beginner's version" crap. Plenty of large apps are built with vanilla Rails. Basecamp is one.
- tomblomfield 13y agoI agree with you - Interactors are particularly useful when you're creating multiple models, and the example could have been better. But they're useful for dealing with side-effects of a particular operation too (sending multiple emails, notifying admins). Much better than ActiveRecord callbacks. If you put this logic in the controller, what happens when you want a separate API controller that does the same thing? Or some admin functionality elsewhere in the codebase? There's zero possibility of code re-use.
- malyk 13y agoI bet dhh is going to say something like "then refactor your controller into a service/interactor/etc., but not before". YAGNI, and all that.
- dhh 13y agoCoulda, woulda, damn shoulda. You're future coding with your "what ifs". Most controller actions are not reused. The time to extract for reuse is when you need to reuse. Not crystal balling about that you probably will be in the future. Because when the reuse case actually arrives, you might find that you need to reuse some, but not all of the action. If you're literally doing the exact same thing for both web and api, why are you using a separate API controller? Just use respond_to with formats. Second, yes, you shouldn't have side-effects like sending email in your models. But you don't need to! Just stick that logic in your controllers. That's what it's there for -- to render the views (of which emails is one of them -- see http://david.heinemeierhansson.com/2012/emails-are-views.html http://david.heinemeierhansson.com/2012/emails-are-views.htm...). The AR callbacks are wonderful for coordinating the domain model. Often times you'll want to create auxiliary objects when something else is created. That's what's it's there for. Or to otherwise keep the integrity of the domain model in place.
- deleted 13y ago[deleted]
- rjspotter 13y agoI agree completely that the Interactor pattern makes for cleaner Models and Controllers in a Rails codebase. I've used both the Interactor gem and PORO (in a directory outside of /app or /lib). Having worked with the Interactor gem for a little while once you break things down into small interactors that can be used by the Organizers, I have two main complaints. 1) inputs are unclear. With calling new or some custom class method you can use descriptive variable names to say what the object expects to be present to do it's job. With the Interactor gem you end up adding in comments describing what keys need be present in the context and what keys will be added to the context so the next programmer can use the interactor you've created without having to go through and grok everything. 2) You end up having to (re)create a failure protocol to communicate with the controller and display to the user. We take the AR errors functionality for granted in our controllers/views with interactors you have to come up with a similar system. 2.5) as a result you end up writing a lot of boilerplate fail! and rollback code 2.5.2) and a non-atomic operation like notifying the payment gateway can break the whole model of rolling back and you have to end up raising so your user doesn't end up in a invalid state or get charged twice.
- tomblomfield 13y agoAgreed, especially on (1). I don't love the lack of explicit inputs. We've got a convention that all Interactors must be commented to specify what inputs they take.
- programminggeek 13y agoI've spent more time thinking about clean architecture and design than most and the conclusion that I've come to is while Ruby gives you all the tools to write clean code and great architecture, it doesn't offer the best tools to do that job. Ruby and Rails feel best as a prototyping platform for getting something out the door fast, proving a concept, and not worrying so much about correctness or maintenance. I don't think if you are doing a lot of TDD and larger systems that piling on more Ruby and Rails is the right answer. I think once you know what you are working with, a well designed language with a compiler is a huge help and would remove a ton of useless tests and stuff that you end up writing in Ruby by hand. This very likely leads to an API driven system with an API written in a strongly typed, compiled language like C#, Java, Scala, Haskell, or Go and writing your front end in whatever makes the most sense for your team. At that point you get the benefits of a nice rapid development platform like Rails for your front end, and a fast, testable, API written in something else using all the clean code architecture you want. The trick is, you do everything ugly in Rails or PHP or whatever in your initial prototype and you might not even write tests. You just ship working code until your prototype has proven a business case. Then, you move it towards something with lower TCO over time. Make the investment in your code when it makes sense to invest in it and use the best tool for the job at each step. You probably never need to leave the standard Rails MVC stuff on most projects unless they are wildly successful and/your long term needs change. Even then, you can probably keep the rails front end stuff and just talk to an API and be very happy.
- reedlaw 13y agoI wrote a blog post along similar lines [1]. The benefits are clear to me: nearly 200 tests run in about a second. For that reason alone I wouldn't consider coupling domain logic with ActiveRecord. 1. http://www.smashingboxes.com/domain-logic-in-rails/ http://www.smashingboxes.com/domain-logic-in-rails/
- dablweb 13y agoI like the interactor gem but don't understand why the method activating it is "perform". Why isn't it "call" so it can be swapped out with a lambda, or method reference without hassle?
- dablweb 13y agoAs in this article. http://blog.arkency.com/2014/02/rails-refactoring-the-aha-moments/ http://blog.arkency.com/2014/02/rails-refactoring-the-aha-mo... Services should be run with call.
- andrzejkrzywda 13y agoIf anyone's interested in a more real-life example, then this post presents a Rails controller refactoring from the Redmine project. http://blog.arkency.com/2014/02/rails-refactoring-the-aha-moments/ http://blog.arkency.com/2014/02/rails-refactoring-the-aha-mo...
- ksec 13y agoIt sounds similar to what Lotus is trying to do. http://lotusrb.org/ http://lotusrb.org/
- jbeja 13y agoHow do you even know about this, so unknown, Thanks :).
- sdegutis 13y agoHow about just "features"? Maybe there are some "user" features (login, logout, change name) and "cart" features (add/remove item in cart). These features would all live horizontally, be able to call each other (design your object graph wisely!) and would never be able to inspect or elaborate on implementation details! These features may or may not talk to some database for its own persistence, but that's up to the functions. And the features are pure functions in your language of choice. They have no knowledge of the web, or requests, they're perfectly transient. They act on a "state" object that you pass around to them, which in tests is just a blob of memory and in production is a real database or wherever else you store state. It's simple, yet so powerful. This is how we do it, and it's scaling very nicely.
- iagooar 13y agoI recommend this article from the guys from Code Climate, it is really mind-opening: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decomp... The author describes different ways of refactoring fat ActiveRecord models, but most of the ideas can be used outside ActiveRecord, as "best practices". I have been using some of the patterns described in it and I am really happy with the results. The code becomes more clearly decoupled, is easy to test and results in clean, slim models.
- ream88 13y agoI would love to take a glimpse on the Basecamp code, dhh ;)
- csense 13y agoThe first missing part: A language with a sane syntax. For some examples of the multitude of ways in which Ruby syntax is awful, see [1]. The second missing part: Ruby is Rails. Trying to learn Ruby at the same time as a modern web framework with all its moving parts? Forget it. The third missing part: Tutorials. Okay, I haven't done a rant on this. Google rails tutorial. The first helpful result I get [2], I scroll down. The first meat is "Setting the application home page". I see this: Blog::Application.routes.draw do get "welcome/index" Huh? "This is your application's routing file which holds entries in a special DSL (domain-specific language) that tells Rails how to connect incoming requests to controllers and actions." So it's not even Ruby or some recognized Web glue language like HTML or CSS; it's some domain-specific language? Okay, I totally don't get it, so as suggested, for more details, I should refer to "Rails Routing from the Outside In" [3]. This is even more confusing. It says you should do something like this: get '/patients/:id', to: 'patients#show' Huh? What's the deal with the colons and octothorpe? I can guess colon is the signifier for an ID in the URL, but why the pound sign for #show? get '/patients/:id', to: 'patients#show', as: 'patient' And what's the deal with the colons? I could go on and on. I'm sure that with a few hours of pain and frustration, I would be able to figure out exactly all the oddities of Ruby syntax, or the template language, or the DSL whatever, and understand this example well enough to extend it. But that's not the point. The point is that, because I have to spend those hours, it means that Ruby/Rails is poorly designed. In Django, by contrast, things are almost always simple and obvious. [1] https://news.ycombinator.com/item?id=5872899 https://news.ycombinator.com/item?id=5872899 [2] http://guides.rubyonrails.org/getting_started.html http://guides.rubyonrails.org/getting_started.html [3] http://guides.rubyonrails.org/routing.html http://guides.rubyonrails.org/routing.html
- dablweb 13y agoRuby has a sane syntax. It probably seems a little foreign if you're new. '#' is the standard notation used throughout the Ruby language for instance methods. Think of it like this: Blog::Application.routes.draw do |implicit_self| implicit_self.get( '/patients/:id', { to: 'patients#show', as: 'patient' } ) end
- GutenYe 13y agoWhat's the different between interactor and wisper? Wisper is better doing the job in your example. interactor: https://github.com/collectiveidea/interactor https://github.com/collectiveidea/interactor wisper: https://github.com/krisleech/wisper https://github.com/krisleech/wisper
- evilgeenius1 13y agoWhen reading DHH's comments it's important to remember that the domain model for Basecamp is extremely simple. It's a brilliant piece of software that's been well thought out, but the underlying domain concepts are extremely basic. I wonder if he's ever worked with an app that has a complicated domain. His comments taken from this perspective make a lot of sense. Rails was written for Basecamp. For large apps with real complexity, the rails way falls short.