Search code examples
rubydesign-patternsactiverecordrefactoringstrategy-pattern

Refactor with Strategy Pattern. In Ruby


Heads up! In the below example, using a pattern is probably overkill... however, if I were extending this to count genres, count the members in a given band, count the number of fans, count the number of venues played, count the number of records sold, count the number of downloads for a specific song etc... it seems like there could be a ton of stuff to count.

The Goal:

To create a new function that chooses the correct counting function based on the input.

The Example:



class Genre < ActiveRecord::Base
  has_many :songs
  has_many :artists, through: :songs

  def song_count
    self.songs.length
  end

  def artist_count
    self.artists.length
  end

end

P.S. If you are also a curious about this question, you may find this other question (unfortunately answered in C#) to be helpful as a supplemental context. Strategy or Command pattern? ...


Solution

  • In Ruby you can implement a strategy pattern quite easily using an (optional) block (assuming it's still unused).

    class Genre < ActiveRecord::Base
      has_many :songs
      has_many :artists, through: :songs
    
      def song_count(&strategy)
        count_using_strategy(songs, &strategy)
      end
    
      def artist_count(&strategy)
        count_using_strategy(artists, &strategy)
      end
    
      private
    
      def count_using_strategy(collection, &strategy)
        strategy ||= ->(collection) { collection.size }
        strategy.call(collection)
      end
    end
    

    The above code defaults to using the size strategy. If you ever want to use a specific strategy in a specific scenario you can simply provide the strategy alongside the call.

    genre = Genre.last
    genre.song_count # get the song_count using the default #size strategy
    # or provide a custom stratigy
    genre.song_count { |songs| songs.count } # get the song_count using #count
    genre.song_count { |songs| songs.length } # get the song_count using #length
    

    If you need to re-use some strategies more often you could save them in a constant or variable:

    LENGTH_STRATEGY = ->(collection) { collection.length }
    
    genre.artist_count(&LENGTH_STRATEGY)
    

    Or create a specific class for them if they are more complicated (currently overkill):

    class CollectionStrategy
      def self.to_proc # called when providing the class as a block argument
        ->(collection) { new(collection).call }
      end
    
      attr_reader :collection
    
      def initialize(collection)
        @collection = collection
      end
    end
    
    class LengthStrategy < CollectionStrategy
      def call
        collection.length
      end
    end
    
    genre.artist_count(&LengthStrategy)