DEV Community

Luiz Cezer
Luiz Cezer

Posted on • Updated on

Tips to improve the design of your Ruby code

image

Any fool can write code that a computer can understand. Good programmers write code that humans can understand - Martin Fowler.

Why you should care about the design of your code?

As developers, we sometimes tend to think that being productive is related to deliver your task as fast as possible, but for me, that is not the truth.

Be a productive developer is far from just execute your task and deploy your code, is also related to that code quality, how easy to maintain and evolve that feature in the next releases. Because of this, it's important to treat your code, even the minimal changes with attention.

Imagine that you work for a company that has software with a huge source code, too many features, too many teams, different approaches. Today you're working in a determinate feature, next week you focus is another, two weeks from now you return for the first feature... when you work for a big company, this flow is kind of normal.

Always remember, sometimes, you need to prevent your self from your own legacy old.

Avoid hidden structures

  • The code is hard to understand and hard to maintain
  • You must avoid use index position directly to get some data from an array

An example to avoid:

class ObscuringReferences
  def initialize(data)
    @data = data
  end

  def calculate
    data.collect do |cell|
      cell[0] + (cell[1] * 2)
    end
  end

  private

  attr_reader :data
end

ObscuringReferences.new([[622, 20], [622, 23], [559, 30], [559, 40]])
Enter fullscreen mode Exit fullscreen mode

The main problem of this code is that it is using the indexes positions of the array to get some information to be used inside de calculate method, but we can not be sure of what are these pieces of information because 0 and 1 are not some meaningful.

How to avoid hidden structures?

  • Use constants to assign values
class RevealingReferences
  HEIGTH  = 0
  WIDTH = 1

  def initialize(data)
    @data = data
  end

  def calculate
    data.collect do |value|
      value[HEIGTH] + (value[WIDTH] * 2)
    end
  end

  private

  attr_reader :data
end
Enter fullscreen mode Exit fullscreen mode

Assigning the indexes positions in a constant will help to improve the code because now, the indexes are meaningful.

  • Use a struct to create a class
Shape = Struct.new(:height, :width)

class RevealingReferences
  def initialize(data)
    @data = prepare(data)
  end

  def calculate
    data.collect do |shape|
      shape.height + (shape.width * 2)
    end
  end

  private

  attr_reader :data

  def prepare(data)
    data.collect do |value|
      Shape.new(*value)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Another approach is to use Struct data structure to represent the data.

Avoid comparison with regular expressions

  • Instead of it, assign this regular expression to a meaningful variable name, and use this variable.
  # Instead of using directly the regular expression in the code...
  if string =~ /\d+/
    # ...CODE
  end

  # Assign it to a variable with a meaningful name, and use it.
  is_decimal = /\d+/
  if string =~ is_decimal
    # ...CODE
  end
Enter fullscreen mode Exit fullscreen mode

This simple change could save your time trying to understand what the regular expression is doing.

Avoid directly dependencies

Follow the Demeter's Law

  • Avoid high coupled code
  • Do not know about the dependencies from a dependency
class Customer
  def state
    address.state.name
  end
end
Enter fullscreen mode Exit fullscreen mode

In the example above, the Customer#state method violates the Demeter's Law, because of the fact that, to access the state.name we first need to the customer to get in contact with your relation with address and then the relationship with address will contact with state.

So we can conclude that the class Customer knows too much about your dependencies. Any error inside the State class can break the Customer, even they are not directly related.

An approach to solve this problem is using Delegators.

Tell don' t Ask!

  • Tell the objects what to do, do not ask if the can do
  • Avoid asking an object which is your current state, to perform something based on this answer
class SocialMediaPost
  def send
    SocialMediaPost::API.post(args)
  end

  def connected?
    connected
  end
end

post = SocialMediaPost.new(content: 'Some Message')

if post.connected?
  post.send
end
Enter fullscreen mode Exit fullscreen mode

In the code, above we first ask to the object if it's connected and then based on this we perform an action to post.send.

Now imagine that you need to execute this actions in different points inside your code base and now, you need to add another verification in addition to the current one. Besides, with this code the internal details of the SocialMediaPost are exposed, we know that connected? must be true to perform other methods.

  • Do not expose the details of an object unnecessarily
class SocialMediaPost
  def send
    SocialMediaPost::API.post(args) if connected?
  end

  private

  def connected?
    connected
  end
end

post = SocialMediaPost.new(content: 'Some Message')
post.send
Enter fullscreen mode Exit fullscreen mode

This approach will not expose the internal details of the class and follows that Demeter's Law.

Conclusions

  • Boy scout rule: Always leave your code better than you found it
  • A better design starts on the small changes
  • You need to write code for humans, not for computers

Discussion (0)