Dicas de Design Orientado a Objetos com Ruby - Parte 1

Publicado por Luiz Cezer Marrone Filho no dia dev

Desenvolvimento de Software

Nos últimos anos o paradigma da Orientação a Objetos vem sendo cada vez mais aplicado e difundindo dentro da comunidade de desenvolvimento de software. Prova disso é a que a grande maioria das novas linguagens que surgiram nos últimos anos obedecem esse paradigma. O desenvolvimento Orientado a Objetos possui diversas vantagens e visa tornar o desenvolvimento mais simples, focado em uma maior abstração e reusabilidade do código.

Dentre as diversas leituras existentes sobre o assunto com foco na linguagem Ruby, que é a principal linguagem da nossa Stack aqui na Resultados Digitais, uma das que mais gostei (e recomendo) foi o livro POODR da autora Sandi Metz, que traz em seu conteúdo diversas técnicas e dicas de como obter um melhor design e uma arquitetura mais limpa em códigos Ruby.

Nessa sequência de posts selecionei algumas das dicas que mais me chamaram atenção e que se aplicadas podem trazer benefícios no código desenvolvido no dia a dia.

Encapsular variáveis de instância

Uma das primeiras dicas que o livro traz é sobre a utilização de variáveis de instância nas classes.

É muito comum durante o desenvolvimento o uso de variáveis de instância dentro das classes Ruby, como no exemplo abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
class Person
  attr_reader :name, :age

  def initialize(name, age)
    @name = name
    @age  = age
  end

  def adult?
    @age > 18
  end
end

Porém o código acima pode ser melhorado aplicando algumas dicas simples de design orientado a objetos, são elas:

  • Trabalhar com comportamento ao invés de uso direto de variáveis de instância.
  • É importante fazer uso de um accessor sobre a variável para evitar o uso direto de seu valor.
  • Se a variável é utilizada apenas dentro da classe e não precisa ser exposta na instância, é possível proteger o atributo com o modificador de visibilidade private.

Então seguindo as dicas acima, a classe poderia ter esse código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person
  attr_reader :name

  def initialize(name, age)
    @name = name
    @age  = age
  end

  def adult?
    age > 18
  end

  private

  attr_reader :age
end

O atributo age da classe Person é apenas utilizado dentro do método adult? para fazer uma verificação, então não há necessidade de ele ser exposto por um accessor público, então foi possível utilizar um accessor privado.

Evitar o uso de estruturas ocultas

Outra dica importante é evitar o uso de estruturas ocultas no código, como por exemplo métodos que utilizam na sua implementação posições de um array, como no exemplo abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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]])

O código da classe acima é ruim, pois a referência cell[0] não é representativa o suficiente e não se sabe o que esperar como retorno para esse valor.

Para resolver esse problema é possível fazer o uso de um Struct e criar uma representação de objeto para as posições do array, como no exemplo abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

Dessa forma o valor de cell[0] é encapsulado e agora é representado por algum objeto, ao invés de apenas referenciar uma determinada posição de um array.

Além do uso de uma Struct é possível fazer o uso de uma constante que represente a coluna a ser utilizada, dessa forma ao invés de referenciar uma posição do array, irá ser referenciado o que aquela coluna representa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

Uma outra alternativa similar ao exemplo acima pode ser utilizada para atribuir um significado a uma variável:

1
2
3
4
5
6
7
8
9
10
  # Ao invés apenas utilizar a expressão regular solta no código...
  if string =~ /\d+/
    # ...
  end

  # Dê um significado a ela, criando uma variável dizendo o que ela representa.
  is_decimal = /\d+/
  if string =~ is_decimal
    # ...
  end

Outras dicas para evitar o uso de estruturas escondidas são:

  • Sempre que possível fazer o uso de Struct ao invés de trabalhar com uma estrutura escondida ou posição de um array.
  • Evitar métodos que dependem de múltiplos hashs ou arrays, pois essas estruturas podem mudar facilmente.
  • Essas implementações não encapsulam comportamento e se expõe diretamente ao código.
  • Evitar o uso de estruturas como array[1] pois não é possível saber o seu real comportamento nem o que essa posição representa.
  • Evitar uso de valores númericos ou expressões regulares soltas pelo código, de preferência por fazer a atribuição desses valores a uma variável e então fazer uso dela.

Aplicando Princípio de Responsabilidade Única (SRP)

O Single Responsibility Principle ou Princípio de Responsabilidade Única é o princípio que prega que toda classe deve ter apenas uma responsabilidade, ou seja, ser responsável por executar apenas uma função dentro do sistema. Mas esse princípio também pode e deve ser aplicado para métodos.

Uma classe onde todos os seus métodos obedecem o SRP se torna mais coesa e traz melhorias de design para a classe como um todo, pois métodos pequenos e focados em apenas uma função facilitam o reuso, além de serem facilmente extraídos para outra classe e reutilizados na aplicação como um todo.

Um exemplo onde um método não obedece o SRP e pode ser melhorado:

1
2
3
4
5
def calculate
  shapes.collect do |shape|
    shape.height + (shape.width * 2)
  end
end

O método acima não obedece SRP pois além de percorrer uma coleção de shapes também é responsável por realizar um determinado cálculo.

Uma simples alteração seria criar um método separado para o cálculo, que seria chamado dentro do loop, conforme o exemplo:

1
2
3
4
5
6
7
def calculate
  shapes.collect { |shape| total_size(shape) }
end

def total_size(shape)
  shape.height + (shape.width * 2)
end

Mas nem sempre é fácil de identificar quando um método quebra o SRP, como no exemplo abaixo:

1
2
3
def calculate
  ratio * (height + (width * 2))
end

Nesse caso o trecho de código responsável por realizer um cálculo, height + (width * 2) poderia ser extraído para um método separado, facilitando a leitura do método calculate e evitando que ele seja responsável por dois cálculos distintos:

1
2
3
4
5
6
7
def calculate
  ratio * total_size
end

def total_size
  height + (width * 2)
end

Isolar responsabilidades extras em outra classe

Uma classe deve ser responsável por lidar com apenas um domínio, se uma classe está precisando lidar com mais de um tipo de comportamento ou então ela tem mais de um motivo para sofrer uma alteração, então ela está fazendo funções demais.

A classe abaixo além de lidar com o domínio Product ainda está sendo responsável por lidar com a lógica de calcular o desconto.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Product
  attr_reader :name, :price

  def initialize(name, price)
    @name  = name
    @price = price
  end

  # Essa lógica pode ser extraída para uma classe separada
  def price_with_discount
    price * 0.90
  end
end

Dessa forma, se um novo comportamento precisar ser adicionado para Product ou então a lógica de desconto mudar, a classe deverá ser altera, ou seja, ela precisa mudar por dois motivos distintos.

Abaixo um exemplo de como essa implementação poderia isolar as duas classes e fazer com que as duas obedece SRP.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  # product.rb
  class Product
    attr_reader :price, :name

    def initialize(name, price)
      @name  = name
      @price = price
    end

    def price_with_discount
      DiscountCalculator.new(price).calculate
    end
  end

  # discount_calculator.rb
  class DiscountCalculator
    DiscountPercentage = 0.9

    def initialize(value)
      @value = value
    end

    def calculate
      value * DiscountPercentage
    end

    private

    attr_reader :value
  end

Considerações finais

  • O ponto de partida para uma boa arquitetura de código, organização, software fácil de manter e de se reaproveitar é procurar fazer com que as classes e métodos sempre tenham apenas uma responsabilidade.
  • Classes/métodos que obedecem SRP permitem mudanças que não causam consequências em pontos distintos, facilitam reuso, refatoração e diminuem duplicações
  • Evitar uso de comentários, pois esses ficam esquecidos e não são atualizados a medida que o código muda.
  • Quando um código possui muitos comentários, há uma boa chance de que esse código está com muitas responsabilidades atribuidas e esse código deve ser separado em outros métodos ou classes, para que cada uma possua apenas uma responsabilidade.

E você, está aplicando as práticas expostas acima em seus códigos ? Tem algo a acrescentar ?

Compartilhe sua opinião nos comentários.

Luiz Cezer Marrone Filho

Luiz Cezer Marrone Filho

Full Stack Developer

Comentários