O Null Object pattern tem como principal objetivo tratar o comportamento do valor Null
, ou em ruby o nil
.
Sabemos que em algum momento de nosso código podemos ter referências nulas, sendo assim, temos que verificar se tal refêrencia é nula ou não para não recebermos uma exception como a seguinte:
NoMethodError: undefined method `name' for nil:NilClass
Vamos a um exemplo: Temos uma classe Game responsável pelas informações de um dado jogo e um classe Report, responsável por imprimir os dados deste tal jogo.
Vamos a classe Game:
1 require 'ostruct'
2
3 class Game < OpenStruct
4 end
Uma simples classe que herda de OpenStruct.
Agora vamos a nossa classe Report:
1 class Report
2
3 def initialize(game)
4 @game = game
5 end
6
7 def show
8 %Q{Game: #{name}
9 Platform: #{platform}
10 Description: #{description}}
11 end
12
13 private
14
15 def name
16 @game.name
17 end
18
19 def platform
20 @game.platform
21 end
22
23 def description
24 @game.description
25 end
26 end
Como podem ver, a nossa classe funciona muito bem para o Game. No entanto, se em algum momento recebermos uma referência nula (por exemplo de um find do ActiveRecord), receberemos a seguinte exception:
1 game = nil
2 report = Report.new(game)
3 puts report.show # undefined method `name' for nil:NilClass (NoMethodError) ...
Vamos resolvê-la utilizando os mais comuns: if
e else
.
1 class Report
2
3 def initialize(game)
4 @game = game
5 end
6
7 def show
8 %Q{Game: #{name}
9 Platform: #{platform}
10 Description: #{description}}
11 end
12
13 private
14
15 def name
16 if @game
17 @game.name
18 else
19 'no name'
20 end
21 end
22
23 def platform
24 if @game
25 @game.platform
26 else
27 'no platform'
28 end
29 end
30
31 def description
32 if @game
33 @game.description
34 else
35 'no description'
36 end
37 end
38 end
Como podem ver, alteramos os métodos da classe Report
, responsável por criar os campos de Game
na exibição do relatório, para tratar quando recebemos um valor nil.
Até funciona, mas como notamos, estamos adicionando mais complexidade a simples métodos que apenas delegam o valor, além de, claramente, estarmos repetindo código. E é neste ponto que o Null Object Pattern vem para nos ajudar. Vamos aos refactories.
1 class NullGame
2
3 def name
4 'no name'
5 end
6
7 def platform
8 'no platform'
9 end
10
11 def description
12 'no description'
13 end
14 end
15
16 class Report
17
18 def initialize(game)
19 @game = game || NullGame.new
20 end
21
22 def show
23 %Q{Game: #{name}
24 Platform: #{platform}
25 Description: #{description}}
26 end
27
28 private
29
30 def name
31 @game.name
32 end
33
34 def platform
35 @game.platform
36 end
37
38 def description
39 @game.description
40 end
41 end
Primeiro criamos uma classe NullGame
responsável por definir os valores quando um Game
for nulo. Em Seguida, alteramos a classe Report
para instanciar um NullGame
(caso o game seja nulo) e assim, podemos alterar nossos métodos que funcionam como delegators para continuarem fazendo apenas isto.
Veja como seria o comportamento de Report
ao receber um nil
:
1 game = nil
2 report = Report.new(game)
3 puts report.show # Game: no name
4 # Platform: no platform
5 # Description: no description
Como podem ver, ao utilizarmos o Null Object Pattern, conseguimos manter o nosso código muito mais Ruby Way utilizando classes coesas, com responsabilidades bem definidas e one line methods.
Para finalizar fica a dica da excelente palestra do Ben Orenstein Refactoring from Good to Great , de onde este post foi inspirado.
Comentários
Included file post/disqus_thread.html not found in _includes directory