Desenvolvimento

27 nov, 2015

Storyboards e Injeções de Dependência

Publicidade

Acho a discussão “Storyboards versus UI programaticamente” totalmente válida. É importante entender que existem duas (ou até mais) formas diferentes de resolver os mesmos problemas. Sempre gostei da forma que XIBs e Storyboards facilitam e agilizam o desenvolvimento de views e view controllers. Além disso, acho que são ferramentas insubstituíveis para o aprendizado de alguns conceitos do UIKit.

Assim como em código, você precisa saber o que está fazendo ao usar um Storyboard. O propósito do Storyboard é você simplificar um fluxo de view controllers que estão interconectadas. Como o meu caro amigo Diogo apontou em seu texto, ao usar Storyboards, você acaba acoplando o seu código, pois de uma forma ou outra suas view controllers vão saber da existência umas das outras. Mas será que isso é tão ruim assim quando você está falando de um Storyboard que representa um fluxo atômico no seu app (um fluxo de cadastro ou de tutorial, por exemplo), no qual realmente não existe uma complexidade que justifique esse isolamento entre suas view controllers? Para mim, faz parte do toolset de um bom desenvolvedor iOS saber desenvolver fluxos simples e prototipar utilizando Storyboards.

E existem também os (vários) casos em que Storyboards atrapalham mais do que ajudam. O modo como os Storyboards são implementados no UIKit faz com que muitas vezes ele nos force a usar anti-patterns como God Object (onde um objeto sabe ou faz muito, violando o Princípio de Responsabilidade Única), Magic Strings, ou BaseBean (forçar herança em vez de delegação). Os Storyboards também são (justamente) conhecidos por aumentar o acoplamento do seu código, dificultando injeções de dependências.

O que é Injeção de Dependências mesmo?

O conceito de Injeção de Dependências pode ser difícil de se entender para desenvolvedores com pouca experiência, principalmente quando se está começando a programar (e muitas vezes é esquecido em projetos reais). Uma explicação simples seria mais ou menos assim:

Teoricamente, você quer sempre deixar o seu código com o menor nível de acoplamento possível. Isso significa que cada módulo do seu sistema deve saber o mínimo possível sobre os outros módulos existentes para cumprir a sua funcionalidade. Injetar dependências significa que cada módulo, ao ser criado, terá suas dependências configuradas por outro módulo do sistema, mantendo assim um baixo acoplamento.

Exemplo usando Storyboard

Para ilustrar, temos o app abaixo, no qual há um tela que busca uma imagem na Internet e apresenta para usuário. A idéia é que o FetchAndDisplayImageViewController consiga buscar uma imagem por meio do FetchImageService e mostrar na sua imageView.

storyboard-1

Em uma implementação mais ingênua do FetchAndDisplayImageViewController, teríamos algo assim:

 1 class FetchAndDisplayImageViewController: UIViewController {
 2 
 3     //Image Service to fetch the image to display
 4     var imageService: FetchImageService?
 5     
 6     //Image View utilizada para mostrar a imagem
 7     @IBOutlet weak var imageView: UIImageView!
 8         
 9     override func viewDidLoad() {
10         super.viewDidLoad()
11         imageService = FetchImageService(param1: "Invariante", param2: "Rocks")
12         if let imageService = imageService {
13             imageView.image = imageService.fetchImage()
14         }
15     }
16 }

Acredito que seja fácil de perceber que essa implementação é, no mínimo, questionável, pois o FetchAndDisplayImageViewController sabe como instanciar o FetchImageService. Isso deixa o nosso código mais amarrado e faz com que nossa FetchAndDisplayImageViewController seja praticamente impossível de ser testada. Uma solução melhor é apresentada abaixo. Num artigo futuro, mostrarei por que esse maior acoplamento (e a falta de injeção de dependência) torna o nosso código menos testável.

 1 class FetchAndDisplayImageViewController: UIViewController {
 2 
 3     //Image Service to fetch the image to display
 4     var imageService: FetchImageService?
 5     
 6     //Image View utilizada para mostrar a imagem
 7     @IBOutlet weak var imageView: UIImageView!
 8         
 9     override func viewDidLoad() {
10         super.viewDidLoad()
11         
12         //Esperamos que alguém externo tenha configurado nossa dependência:
13         if let imageService = imageService {
14             imageView.image = imageService.fetchImage()
15         } else {
16             print("Não era para isso acontecer...")
17         }
18     }
19 }

Dessa forma, nós não instanciamos a nossa propriedade imageService, mas, em vez disso, no viewDidLoad esperamos que a propriedade imageService tenha sido configurada por alguém.

Em seu artigo, Diogo também comentou sobre a forma de se passar informações entre o nosso ViewController e o FetchAndDisplayImageViewController quando utilizamos Storyboards. Esta seria a forma correta de setar a propriedade imageService nessa abordagem:

 1 class ViewController: UIViewController {
 2 
 3     override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
 4         if segue.identifier == "ViewControllerToFetchAndDisplayImageViewControllerSegue" {
 5             if let destinationViewController = segue.destinationViewController as? FetchAndDisplayImageViewController {
 6                 let imageService = FetchImageService(param1: "Invariante", param2: "Rocks")
 7                 destinationViewController.imageService = imageService
 8             }
 9         }
10     }
11 }

E é aí que o Storyboard mostra sua fragilidade. Quantos problemas você consegue ver nos dois últimos trechos de código acima? Eu consigo citar alguns:

1) A property imageService é uma var. Gostaríamos muito que ela fosse imutável (let). Para isso, precisaríamos setá-la no init (e, por estarmos utilizando Storyboards, não temos como fazer isso);

2) Essa mesma propriedade precisa ser public ou internal, pois ela é setada por outra classe (ViewController). Idealmente, queremos que ela seja private;

3) Além disso, imageService precisa ser um optional, o que acaba deixando o código mais verboso, pois precisamos sempre fazer o unwrap dessa variável ao usá-la;

4) Por ser possível apresentar a FetchAndDisplayImageViewController sem setar a var imageService, precisamos definir o comportamento da view controller caso isso aconteça.

Que bagunça! E como a gente pode melhorar isso? Simples: não use Storyboards 🙂

Exemplo em código

Veja que, no código abaixo, agora é mandatório que o imageService seja passado na inicialização da nossa classe. Assim, conseguimos fazer com que a nossa propriedade imageService seja privada e imutável (e, claro, não-opcional).

 1 class FetchAndDisplayImageViewController: UIViewController {
 2 
 3     //Image Service to fetch the image to display
 4     //Note que agora nossa propriedade é imutável (let), além de ser privada.
 5     private let imageService: FetchImageService
 6     
 7     //Image View utilizada para mostrar a imagem
 8     weak var imageView: UIImageView!
 9     
10     init(imageService: FetchImageService) {
11         self.imageService = imageService
12         super.init(nibName: nil, bundle: nil)
13     }
14 
15     required init?(coder aDecoder: NSCoder) {
16         fatalError("init(coder:) has not been implemented")
17     }
18     
19     override func loadView() {
20         super.loadView()
21         
22         imageView = UIImageView()
23         //Adicionar constraints manualmente.
24         
25         view.addSubview(imageView)
26     }
27     
28     override func viewDidLoad() {
29         super.viewDidLoad()
30         
31         //Sabemos que nossa dependência foi configurada no `init`
32         imageView.image = imageService.fetchImage()
33     }
34 }

Entendi! Agora eu também odeio Storyboards!

Pode ter certeza de que você não está sozinho, mas infelizmente não compartilho da mesma opinião. Como eu falei, sou fã de Storyboards e acho, sim, que eles têm um papel importantíssimo no desenvolvimento de apps. É importante saber discernir os momentos certos de utilizar ou não Storyboards. Não acredito que as abordagens acima utilizando Storyboards estejam necessariamente erradas. É tudo uma questão de saber o que se está fazendo e escolher a ferramenta certa para cada problema.