简体   繁体   中英

What is the best way to dry out this ruby code?

I would like to DRY out this code. Is the best solution to do it like in rails with before_action method?

class Direction
    attr_accessor :dir

    def initialize(dir)
     @dir = dir
    end

    DIRECTIONS = %w[N E S W]

    def turn_left
     d = DIRECTIONS.find_index(dir)
     @dir = DIRECTIONS.rotate!(d-1).first
    end

    def turn_right
     d = DIRECTIONS.find_index(dir)
     @dir = DIRECTIONS.rotate!(d+1).first
    end
end
# frozen_string_literal: true

class Direction
  DIRECTIONS = %w[N E S W].freeze
  OPERATIONS = { left: :-, right: :+ }.freeze
  private_constant :DIRECTIONS, :OPERATIONS

  def initialize(dir)
    @dir = dir
  end

  OPERATIONS.keys.each do |turn_direction| # turn_left, turn_right 
    define_method "turn_#{turn_direction}" do
      turn(turn_direction)
    end
  end

  private

  attr_reader :dir

  def direction_index
    DIRECTIONS.find_index(dir)
  end

  def turn(operation)
    DIRECTIONS.rotate(direction_index.public_send(OPERATIONS[operation], 1)).first
  end
end

p Direction.new('N').turn_left  # "W"
p Direction.new('E').turn_left  # "N"
p Direction.new('S').turn_left  # "E"
p Direction.new('W').turn_left  # "S"
p Direction.new('N').turn_right # "E"
p Direction.new('E').turn_right # "S"
p Direction.new('S').turn_right # "W"
p Direction.new('W').turn_right # "N"

You can:

  • freeze your constants to avoid modifications.
  • Change the visibility of your constants if they're not being used outside the Direction class.
  • Change the visibility of dir if you're only using it within the class itself.
  • Create an OPERATIONS hash, which defines the direction and the operation it'll use to return the next direction.
  • Iterate over the OPERATIONS keys to dynamically define the methods turn_left and turn_right .
  • Define a direction_index method, which returns the index in DIRECTIONS by using dir .
  • Define a turn method, which receives an operation parameter:
    • Using operation you can get the operation from OPERATIONS which tells how to rotate (positive or negative).
    • Applying the method - or + to the result of direction_index you get the arguments to rotate.
    • After that you invoke rotate on DIRECTIONS and get the first element.

I suggest using hashes, mostly for readability.

class Direction
  NEXT_LEFT  = { 'N'=>'W', 'W'=>'S', 'S'=>'E', 'E'=>'N' }
  NEXT_RIGHT = NEXT_LEFT.invert

  attr_reader :dir

  def initialize(dir)
    @dir = dir
  end

  def turn_left
    turn(NEXT_LEFT)
  end

  def turn_right
    turn(NEXT_RIGHT)
  end

  private

  def turn(nxt)
    @dir = nxt[@dir]
  end
end

d = Direction.new('N')
d.dir
  #=> "N" 
d.turn_left
  #=> "W" 
d.turn_left
  #=> "S" 
d.turn_right
  #=> "W" 

Note:

NEXT_RIGHT
  #=> {"W"=>"N", "S"=>"W", "E"=>"S", "N"=>"E"}

Lots of good answers, but a simple direct solution would be to just factor out the part that is common between the two methods into a turn method and pass in 1 or -1 .

class Direction
  attr_accessor :dir

  def initialize(dir)
    @dir = dir
  end

  DIRECTIONS = %w[N E S W]

  def turn(delta_d)
    d = DIRECTIONS.find_index(dir)
    @dir = DIRECTIONS.rotate!(d + delta_d).first
  end

  def turn_left
    turn(-1)
  end

  def turn_right
    turn(1)
  end

end

You can always implement a direction map independent of the state:

class DirectionMap
  def initialize(*list)
    # Create a Hash mapping table with left and right directions
    # pre-computed. This uses modulo to "wrap" the array around.
    @directions = list.map.with_index do |dir, i|
      [ dir, [ list[(i - 1) % list.length], list[(i + 1) % list.length] ] ]
    end.to_h
  end

  # These methods use dig to avoid blowing up on an invalid direction,
  # instead just returning nil for garbage input.
  def left(dir)
    @directions.dig(dir, 0)
  end

  def right(dir)
    @directions.dig(dir, 1)
  end
end

Where you can now navigate arbitrary compass mappings:

map = DirectionMap.new(*%w[ N E S W ])

map.left('N') # => 'W'
map.left(map.left('N')) # => 'S'

map.right('N') # => 'E'
map.right(map.left('N')) # => 'N'

So you can do %w[ N NE E SE S SW W NW ] as well.

I think you can avoid all the work of creating new array each time you turn (by calling rotate on it). Just store your current direction as an index of its letter in the array. Turning is just modular arithmetic on the index (note that in Ruby -1 % 4 == 3 ). And when you want the letter of the direction, just get it from the array using the index.

class Direction
  DIRECTIONS = %w[N E S W].freeze

  def initialize(dir)
    self.dir = dir
  end

  # dir getter
  def dir
    DIRECTIONS[@dir_index]
  end

  # dir setter
  def dir=(dir)
    @dir_index = DIRECTIONS.index(dir)
  end

  # turning logic
  def turn(delta)
    @dir_index = (@dir_index + delta) % DIRECTIONS.size
    dir
  end

  def turn_left
    turn(-1)
  end

  def turn_right
    turn(1)
  end
end

p Direction.new('N').turn_left   #=> "W"
p Direction.new('E').turn_left   #=> "N"
p Direction.new('S').turn_left   #=> "E"
p Direction.new('W').turn_left   #=> "S"
p Direction.new('N').turn_right  #=> "E"
p Direction.new('E').turn_right  #=> "S"
p Direction.new('S').turn_right  #=> "W"
p Direction.new('W').turn_right  #=> "N"

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