简体   繁体   中英

How to refactor a large Ruby class?

I have a controller class that is large enough to be a bad practice (Rubocop throws this warning: Class definition is too long).

The class has many private methods, all of them calculate paths to redirect to, so it's a controller matter.

How could it be refactored?

Example code:

class PostController
  def new
    @post = Post.new
  end

  def create
    @post = Post.new(post_params)
    # ...
  end

  # more resources methods

  private

  def post_params
    # ...
  end

  def post_url(post)
    if params['submit-save'] || params['submit-publish']
      url_for [:edit, post]
    else
      url_for [:review, post]
    end
  end

  def next_post_url(post)
    next_post = post.find_next

    if next_post
      url_for [:edit, post]
    else
      some_path(next_post)
    end
  end

  # some more methods
end

From the discussion on your question, my thought would be to identify if the various private methods that are calculating paths for redirection could belong in a special purpose class that abstracts the various aspects of the path calculation logic or possibly just a module that the controller mixes in. The right answer will depend greatly on exactly what your logic looks like in the various private methods. A simple module is not likely the right approach since you're just moving the code somewhere else to meet an arbitrary metric. Identifying the reason for all the private methods and coming up with a better abstraction is probably the right approach, but there are different patterns that may come into play.

Many of the same patterns discussed in this article will apply to overweight controllers as well as models: 7 Patterns to Refactor Fat ActiveRecord Models

So, extracting conclusions from the comments, I liked these 2 ideas:

  • Common sense. Only you as the developer know how the code should be structured, so tools can only suggest, but you have the final word. Rubocop can be configured through comments in the code to disable some checks.
  • In case of refactor, private methods in a controller could be extracted into a helper.

I agree that the last word is going to be always from the developer, but if you add a tool like rubocop is to encourage you to write a better code so It makes no sense to me adding rubocop and then disabling some checking.
I would'n use helpers methods either, the reason behind is that a class should have only one responsibility, so it should have only one behaviour. People tend to put methods of all types inside helpers, methods with completely different behaviours which give to that class many different responsibilities. I would just use a helper, if all of its methods have just one and only one purpose.
In your particular case I would be more tempted to extract knowledge into plain classes. Try to identify different behaviours and encapsulate them into classes.
In that case it looks like you have some kind of url builder so a posible solution could be something like :

class PostController
  def new
    @post = Post.new
  end

  def create
    @post = Post.new(url_builder.post_url)
    # ...
  end

  # more resources methods

  private

  def url_builder
    UrlBuilder.new(self, params)
  end
end

class UrlBuilder
  def initialize(self, params)
    @self = self
    @params = params
  end

  def post_url(post)
    if params['submit-save'] || params['submit-publish']
      self.url_for [:edit, post]
    else
      self.url_for [:review, post]
    end
  end

  def next_post_url(post)
    next_post = post.find_next

    if next_post
      self.url_for [:edit, post]
    else
      some_path(next_post)
    end
  end
  private
  attr_reader :self, :params
end  

This is not a fully working example as I don't know the entire implementation of your controller, is just a posible solution which will need some adjustments. A couple of things to say about that solution are:

  • I would move the class into a completely different file.
  • I don't like the idea of passing self between objects, I don't thinks is a good idea to pass the context of an object into another object, it creates coupling between the two of them because one need to know about the other one context, but in this particular case is the only way I can see, if you want to use the url_for helper methods available in the controller otherwise you will have to make your own url builder. If anyone have others ideas please share it with us.
  • If you discover that there are more than just one behaviour then I would split then into more than one class, as many as behaviours you find.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM