ChatGPT解决这个技术问题 Extra ChatGPT

OO Design in Rails: Where to put stuff

I'm really enjoying Rails (even though I'm generally RESTless), and I enjoy Ruby being very OO. Still, the tendency to make huge ActiveRecord subclasses and huge controllers is quite natural (even if you do use a controller per resource). If you were to create deeper object worlds, where would you put the classes (and modules, I suppose)? I'm asking about views (in the Helpers themselves?), controllers and models.

Lib is okay, and I've found some solutions to get it to reload in a dev environment, but I'd like to know if there's a better way to do this stuff. I'm really just concerned about classes growing too large. Also, what about Engines and how do they fit in?


A
Adam Zerner

Because Rails provides structure in terms of MVC, it's natural to end up using only the model, view, and controller containers that are provided for you. The typical idiom for beginners (and even some intermediate programmers) is to cram all logic in the app into the model (database class), controller, or view.

At some point, someone points out the "fat-model, skinny-controller" paradigm, and intermediate developers hastily excise everything from their controllers and throw it into the model, which starts to become a new trash can for application logic.

Skinny controllers are, in fact, a good idea, but the corollary--putting everything in the model, isn't really the best plan.

In Ruby, you have a couple of good options for making things more modular. A fairly popular answer is to just use modules (usually stashed in lib) that hold groups of methods, and then include the modules into the appropriate classes. This helps in cases where you have categories of functionality that you wish to reuse in multiple classes, but where the functionality is still notionally attached to the classes.

Remember, when you include a module into a class, the methods become instance methods of the class, so you still end up with a class containing a ton of methods, they're just organized nicely into multiple files.

This solution can work well in some cases--in other cases, you're going to want to think about using classes in your code that are not models, views or controllers.

A good way to think about it is the "single responsibility principle," which says that a class should be responsible for a single (or small number) of things. Your models are responsible for persisting data from your application to the database. Your controllers are responsible for receiving a request and returning a viable response.

If you have concepts that don't fit neatly into those boxes (persistence, request/response management), you probably want to think about how you would model the idea in question. You can store non-model classes in app/classes, or anywhere else, and add that directory to your load path by doing:

config.load_paths << File.join(Rails.root, "app", "classes")

If you're using passenger or JRuby, you probably also want to add your path to the eager load paths:

config.eager_load_paths << File.join(Rails.root, "app", "classes")

The bottom-line is that once you get to a point in Rails where you find yourself asking this question, it's time to beef up your Ruby chops and start modeling classes that aren't just the MVC classes that Rails gives you by default.

Update: This answer applies to Rails 2.x and higher.


D'oh. Adding a separate directory for non-Models hadn't occurred to me. I can feel a tidy-up coming on...
Yehuda, thanks for that. Great answer. That's exactly what I'm seeing in the apps I inherit (and those I make): everything in controllers, models, views, and the helpers automatically provided for controllers and views. Then come the mixins from lib, but there's never an attempt to do real OO modeling. You're right, though: in "apps/classes, or anywhere else." Just wanted to check if there's some standard answer I'm missing...
With more recent versions, config.autoload_paths defaults to all directories under app. So you don't need to change config.load_paths as described above. I'm not sure about eager_load_paths (yet) though, and need to look into that. Does anyone already know?
Passive aggressive towards Intermediates :P
It would be nice if Rails shipped with this "classes" folder to encourage "single responsibility principle" and to enable developers to create objects that are not database backed. The "Concerns" implementation in Rails 4 (see Simone's answer) seems to have taken care of implementing modules to share logic across models. However, no such tool has been created for plain Ruby classes that aren't database backed. Given that Rails is very opinionated, I am curious the thought process behind NOT including a folder like this?
S
Simone Carletti

Update: The use of Concerns have been confirmed as the new default in Rails 4.

It really depends on the nature of the module itself. I usually place controller/model extensions in a /concerns folder within app.

# concerns/authentication.rb
module Authentication
  ...
end    

# controllers/application_controller.rb
class ApplicationController
  include Authentication
end



# concerns/configurable.rb
module Configurable
  ...
end    

class Model 
  include Indexable
end 

# controllers/foo_controller.rb
class FooController < ApplicationController
  include Indexable
end

# controllers/bar_controller.rb
class BarController < ApplicationController
  include Indexable
end

/lib is my preferred choice for general purpose libraries. I always have a project namespace in lib where I put all application-specific libraries.

/lib/myapp.rb
module MyApp
  VERSION = ...
end

/lib/myapp/CacheKey.rb
/lib/myapp/somecustomlib.rb

Ruby/Rails core extensions usually take place in config initializers so that libraries are only loaded once on Rails boostrap.

/config/initializer/config.rb
/config/initializer/core_ext/string.rb
/config/initializer/core_ext/array.rb

For reusable code fragments, I often create (micro)plugins so that I can reuse them in other projects.

Helper files usually holds helper methods and sometimes classes when the object is intended to be used by helpers (for instance Form Builders).

This is a really general overview. Please provide more details about specific examples if you want to get more customized suggestions. :)


Bizarre thing. I can't get this require_dependency RAILS_ROOT + "/lib/my_module" to work with something out of the lib directory. It definitely executes and complains if the file is not found, but it doesn't reload it.
Ruby's require only ever loads things once. If you want to load something unconditionally, use load.
Also, it seems pretty unusual to me that you'd want to load a file twice during an app instance's lifetime. Are you generating code as you go?
Why do you use require_dependency instead of require? Also note that if you follow naming conventions you don't need to use require at all. If you create MyModule in lib/my_module, you can invoke MyModule without previous require (even if using require should be faster and sometimes more readable). Also note that file in /lib are only loaded once on bootstrap.
Use of concerns is concerning
C
Community

... the tendency to make huge ActiveRecord subclasses and huge controllers is quite natural ...

"huge" is a worrisome word... ;-)

How are your controllers becoming huge? That's something you should look at: ideally, controllers should be thin. Picking a rule-of-thumb out of thin air, I'd suggest that if you regularly have more than, say, 5 or 6 lines of code per controller method (action), then your controllers are probably too fat. Is there duplication that could move into a helper function or a filter? Is there business logic that could be pushed down into the models?

How do your models get to be huge? Should you be looking at ways to reduce the number of responsibilities in each class? Are there any common behaviours you can extract into mixins? Or areas of functionality you can delegate to helper classes?

EDIT: Trying to expand a bit, hopefully not distorting anything too badly...

Helpers: live in app/helpers and are mostly used to make views simpler. They're either controller-specific (also available to all views for that controller) or generally available (module ApplicationHelper in application_helper.rb).

Filters: Say you have the same line of code in several actions (quite often, retrieval of an object using params[:id] or similar). That duplication can be abstracted first to a separate method and then out of the actions entirely by declaring a filter in the class definition, such as before_filter :get_object. See Section 6 in the ActionController Rails Guide Let declarative programming be your friend.

Refactoring models is a bit more of a religious thing. Disciples of Uncle Bob will suggest, for example, that you follow the Five Commandments of SOLID. Joel & Jeff may recommend a more, er, "pragmatic" approach, although they did appear to be a little more reconciled subsequently. Finding one or more methods within a class that operate on a clearly-defined subset of its attributes is one way to try identifying classes that might be refactored out of your ActiveRecord-derived model.

Rails models don't have to be subclasses of ActiveRecord::Base, by the way. Or to put it another way, a model doesn't have to be an analogue of a table, or even related to anything stored at all. Even better, as long as you name your file in app/models according to Rails' conventions (call #underscore on the class name to find out what Rails will look for), Rails will find it without any requires being necessary.


True on all counts, Mike, and thanks for your concern... I've inherited a project in which there were some methods on controllers that were huge. I've broken these down into smaller methods but the controller itself is still "fat." So what I'm looking for are all of my options to offload stuff. Your answers are, "helper functions," "filters," "models," "mixins" and "helper classes." So then, where can I put these things? Can I organize a class hierarchy that gets autoloaded in a dev env?
b
bbozo

Here's an excellent blog post about refactoring the fat models that seem to arise from the "thin controller" philosphy:

http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

Basic message is "Don’t Extract Mixins from Fat Models", use service classes instead, the author provides 7 patterns to do so