Upload
khangminh22
View
2
Download
0
Embed Size (px)
Citation preview
UNIVERSIDADE FEDERAL DO CEARÁ
CAMPUS DE QUIXADÁ
CURSO DE ENGENHARIA DE SOFTWARE
GABRIEL JORGE TAVARES RAMOS BENTO
REFATORAÇÃO DO JOGO BICHO UFC RAMPAGE USANDO SOLID E PADRÕES
DE PROJETO
QUIXADÁ
2020
GABRIEL JORGE TAVARES RAMOS BENTO
REFATORAÇÃO DO JOGO BICHO UFC RAMPAGE USANDO SOLID E PADRÕES DE
PROJETO
Trabalho de Conclusão de Curso apresentada
ao Curso de Engenharia de Software da
Universidade Federal do Ceará, como requisito
parcial para obtenção do título de Bacharel em
Engenharia de Software. Área de concentração:
Engenharia de Software.
Orientadora: Profª. Dra. Paulyne Matthews
Jucá.
QUIXADÁ
2020
GABRIEL JORGE TAVARES RAMOS BENTO
REFATORAÇÃO DO JOGO BICHO UFC RAMPAGE USANDO SOLID E PADRÕES DE
PROJETO
Trabalho de Conclusão de Curso apresentada
ao Curso de Engenharia de Software da
Universidade Federal do Ceará, como requisito
parcial para obtenção do título de Bacharel em
Engenharia de Software. Área de concentração:
Engenharia de Software.
Aprovada em: ___/___/______.
BANCA EXAMINADORA
________________________________________
Profa. Dra. Paulyne Matthews Jucá (Orientador)
Universidade Federal do Ceará (UFC)
_________________________________________
Profa. Me. Antonia Diana Braga Nogueira
Universidade Federal do Ceará (UFC)
_________________________________________
Prof. Me. Diego Andrade de Almeida
Universidade Federal do Ceará (UFC)
AGRADECIMENTOS
Agradeço primeiramente a minha família que por tanto tempo vem me apoiando em
decisões difíceis, sempre com confiança. Também agradeço a minha orientadora que durante
este percurso me guiou e me apoiou sempre em prontidão em dias e horários diversos. E
agradeço os grandes amigos que conheci durante esse percurso que jamais esquecerei e espero
sempre poder reencontrá-los para nos embriagar e conversar. E que para todos os
mencionados aqui, que eu sempre possa ajudá-los e estar sempre ao lado deles apesar das
distâncias.
“O nitrogênio em nosso DNA, o cálcio em
nossos dentes, o ferro em nosso sangue, o
carbono em nossas tortas de maçã… Foram
feitos no interior de estrelas em colapso, agora
mortas há muito tempo. Nós somos poeira das
estrelas.”
(Carl Sagan, Cosmos, 1980)
RESUMO
Cada vez mais o mercado de jogos se torna mais competitivo exigindo que os jogos
desenvolvidos estejam prontos para mudanças rápidas. Para isso, bons projetos usam das
ferramentas e conhecimento provido pela Engenharia de Software. Esses conhecimentos são
usados em seus processos que integram profissionais de diferentes áreas, nas ferramentas que
agilizam o desenvolvimento e no conhecimento prévio adquirido por outros desenvolvedores.
E em se tratando de código, para se construir um design de código bom, bons
desenvolvedores usam de princípios SOLID e padrões de projeto. Esses dois conhecimentos
são fundamentais para a construção de uma estrutura de código boa o suficiente para estar
pronta para as mudanças rápidas que o mercado exige, aumentando as chances de sucesso dos
jogos. É nesse ponto que entra o objetivo deste trabalho que aplica a refatoração do jogo
Bicho UFC Rampage, um jogo desenvolvido por alunos da Universidade Federal do Ceará
(UFC) em Quixadá, Ceará, usando dessas técnicas fundamentais para qualquer bom
desenvolvedor. Este trabalho tem como público-alvo desenvolvedores que desejam aprender
mais sobre refatoração, princípios SOLID e padrões de projeto aplicados a jogos
desenvolvidos com a engine Unity. O processo de execução se dá com a apresentação do
projeto em seu estado inicial, e depois parte para a identificação de maus cheiros de design,
que são indícios de um design de código mau estruturado. Logo depois, aplica os princípios
SOLID e padrões de projeto para amenizar e/ou eliminar esses maus cheiros melhorando a
qualidade do design do código. Depois desses passos de refatoração, uma análise estática de
código é aplicada na versão inicial e final que compara as duas versões mostrando as
diferenças usando a ferramenta NDepend.
Palavras-chave: Refatoração. SOLID. Padrões de projeto.
ABSTRACT
The games market is becoming more and more competitive, demanding that the games
developed are ready for rapid changes. For this, good projects use the tools and knowledge
provided by Software Engineering. This knowledge is used in its processes that integrate
professionals from different areas, in the tools that speed up the development and in the
previous knowledge acquired by other developers. And when it comes to code, to build good
code design, good developers use SOLID principles and design patterns. These two skills are
fundamental to building a code structure good enough to be ready for the rapid changes that
the market requires, increasing the chances of successful games. And at this point comes the
objective of this work that applies the refactoring of the game Bicho UFC Rampage, a game
developed by students from the Federal University of Ceará (UFC) in Quixadá, Ceará, using
these fundamental techniques for any good developer. This work is aimed at developers who
want to learn more about refactoring, SOLID principles and design patterns applied to games
developed with the Unity engine. The execution process takes place with the presentation of
the project in its initial state, and then goes on to identify bad design smells, which are
indications of a badly structured code design. Then, apply the SOLID principles and design
standards to mitigate and/or eliminate these bad smells by improving the quality of the code
design. After these refactoring steps, a static code analysis is applied in the initial and final
versions that compare the two versions showing the differences using the NDepend tool.
Keywords: Refactoring. SOLID. Design patterns.
LISTA DE FIGURAS
Figura 1 ─ Diagrama de classes UML que representa o estado inicial do projeto ................... 24
Figura 2 ─ Classe em desacordo com o princípio SRP ............................................................ 28
Figura 3 ─ Classe em acordo com o princípio SRP ................................................................. 28
Figura 4 ─ Exemplo da implementação de um personagem que usa uma pistola ................... 29
Figura 5 ─ Exemplo do isolamento da classe Player das mudanças que acontecem em relação
as armas .................................................................................................................................... 30
Figura 6 ─ Classe Player responsável por realizar a atualização de pontos e de vida do
jogador quando um item é coletado .......................................................................................... 31
Figura 7 ─ Implementação dos coletáveis mostrando a superclasse e subclasses ................... 31
Figura 8 ─ Trecho da implementação dos coletáveis de acordo com LSP ............................... 32
Figura 9 ─ A Player usa qualquer coletável que seja subtipo de ColectibleBase .................... 33
Figura 10 ─ Estrutura em desacordo com ISP ......................................................................... 34
Figura 11 ─ Serviços para diferentes clientes separados através de interfaces ........................ 34
Figura 12 ─ Estrutura básica do padrão Singleton em código escrito em C# .......................... 36
Figura 13 ─ Estrutura básica do padrão Observer em UML .................................................... 38
Figura 14 ─ Estrutura da separação das responsabilidades da classe ControleDoPersonagem
retirando as responsabilidades de verificar inputs, mover para direita e pular após a aplicação
do Princípio da Responsabilidade única SRP. ......................................................................... 42
Figura 15 ─ Estrutura da separação das responsabilidades da classe ControleDoPersonagem
retirando as responsabilidades de verificar inputs, mover para direita e pular após a aplicação
do Princípio da inversão de dependências DIP ....................................................................... 43
Figura 16 ─ Estrutura do sistema de score e itens implementado com o padrão Observer ..... 44
Figura 17 ─ Implementação do evento que notifica interessados em quando um item é
coletado ..................................................................................................................................... 44
Figura 18 ─ Código do dash acoplado ao código do salto do personagem tornando o design
Viscoso, Rígido, Frágil e Opaco. .............................................................................................. 45
Figura 19 ─ Diagrama de classes UML que mostra a estrutura da funcionalidade dash
implementada............................................................................................................................ 46
Figura 20 ─ Implementação da primeira versão das funcionalidades da câmera e interfaces de
início e fim de jogo na classe ControleDaCâmera ................................................................... 47
Figura 21 ─ Refatoração da estrutura que verifica inputs do jogador para aplicar o padrão
Observer ................................................................................................................................... 48
Figura 22 ─ Implementação da classe CameraController que é uma classe interessada em
saber sobre o evento de primeiro input do jogador .................................................................. 49
Figura 23 ─ Implementação da classe PlayerLife que exibe a interface de fim de jogo quando
o personagem sai do enquadramento da câmera. ..................................................................... 50
Figura 24 ─ Implementação da classe Obstaculo na primeira versão ...................................... 51
Figura 25 ─ Trecho que mostra parte das modificações na classe
PlayerCollisionDetectionAndPenality...................................................................................... 52
Figura 26 ─ Diagrama de classes da estrutura que notifica quando o personagem “morre” para
os ouvintes CameraController e PlayerControls ..................................................................... 53
LISTA DE TABELAS
Tabela 1 ─ Funcionalidades do jogo em sua versão inicial ...................................................... 23
Tabela 2 ─ Linhas de código (LOC) da primeira versão. ......................................................... 53
Tabela 3 ─ Linhas de código (LOC) da versão final. ............................................................... 54
Tabela 4 ─ Complexidade Ciclomática (CC) das classes da primeira versão. ......................... 55
Tabela 5 ─ Complexidade Ciclomática (CC) das classes da versão final. ............................... 55
LISTA DE ABREVIATURAS E SIGLAS
DIP Inversão de Dependência
ISP Princípio da Segregação de Interfaces
LSP Princípio de Substituição de Liskov
OCP Princípio do Aberto/Fechado
PACCE Programa de Aprendizagem Cooperativa em Células Estudantis
SRP Princípio da Responsabilidade Única
UML Unifield Modeling Language
XML Extensible Markup Language
LOC Linhas de Código
CC Complexidade Ciclomática
SUMÁRIO
1 INTRODUÇÃO ................................................................................................................... 15
2 TRABALHOS RELACIONADOS .................................................................................... 16
3 FUNDAMENTAÇÃO TEÓRICA ...................................................................................... 19
3.1 Refatoração ....................................................................................................................... 19
3.3 Sobre o jogo Bicho UFC Rampage .................................................................................. 20
3.3.1 Sobre a primeira versão .................................................................................................. 21
3.3.2 Estado da primeira versão .............................................................................................. 21
3.3.3 Problemas encontrados na primeira versão .................................................................. 21
3.2.1 Maus cheiros de design .................................................................................................. 25
3.2.1.1 Rigidez .......................................................................................................................... 25
3.2.1.2 Fragilidade ................................................................................................................... 26
3.2.1.3 Imobilidade ................................................................................................................... 26
3.2.1.4 Viscosidade ................................................................................................................... 26
3.2.1.5 Complexidade desnecessária ........................................................................................ 26
3.2.1.6 Repetição desnecessária ............................................................................................... 27
3.2.1.7 Opacidade ..................................................................................................................... 27
3.2.2 Princípios SOLID ........................................................................................................... 27
3.2.2.1 Princípio da responsabilidade única (SRP) ................................................................. 27
3.2.2.2 Princípio do aberto/fechado (OCP) ............................................................................. 29
3.2.2.3 Princípio de substituição de Liskov (LSP) ................................................................... 30
3.2.2.4 Princípio da segregação de interfaces (ISP) ................................................................ 33
3.2.2.5 Princípio da inversão de dependência (DIP) ............................................................... 35
3.2.3 Padrões de projeto de software ....................................................................................... 35
3.2.3.1 Singleton ....................................................................................................................... 35
3.2.3.2 Template Method .......................................................................................................... 36
3.2.3.3 Strategy ......................................................................................................................... 36
3.2.2.4 Observer ....................................................................................................................... 37
4 METODOLOGIA ................................................................................................................ 38
4.1 Revisão bibliográfica ........................................................................................................ 38
4.2 Avaliação da primeira versão .......................................................................................... 39
4.3 Refatoração do código do projeto ................................................................................... 39
4.4 Análise estática de código usando NDepend .................................................................. 40
5 DESENVOLVIMENTO ...................................................................................................... 40
5.1 Refatoração ....................................................................................................................... 40
5.1.1 Separando as responsabilidades e invertendo dependências da classe
ControleDoPersonagem .......................................................................................................... 41
5.1.2 Mudando a forma como a contagem de pontos é feita usando o padrão Observer ..... 43
5.1.3 Separação da funcionalidade Dash da classe ControleDoPersonagem, inversão de
dependências e organização em camadas ............................................................................... 45
5.1.4 Dinâmica de movimento da câmera e fim de jogo......................................................... 46
5.1.5 Penalidade de colisão com objetos da cena, bloqueio dos controles do personagem e
parar a câmera depois do fim de jogo ..................................................................................... 50
6 COMPARAÇÃO ENTRE A PEIMEIRA E ÚLTIMA VERSÃO .................................. 53
7 CONCLUSÃO ...................................................................................................................... 56
REFERÊNCIAS ..................................................................................................................... 59
15
1 INTRODUÇÃO
Desenvolver jogos é uma tarefa complexa. Complexidade esta que se dá pelas diversas áreas
envolvidas na produção como programação, design, arte, cinema e música. Segundo
(PARVIAINEN, 2017), da perspectiva de desenvolvedor, é difícil escrever um código de
qualidade que torne fácil a adaptação aos requisitos em constante mudança, sendo
manutenível, extensível, testável e capaz de evoluir durante a produção.
Entretanto, a engenharia de software para o desenvolvimento de produtos tradicionais
já evoluiu bastante na proposição de soluções que melhoram o projeto e a qualidade do
software desenvolvidos. São exemplos dessas iniciativas a definição de padrões de projeto e
princípios de design de código SOLID (MARTIN, R.; MARTIN, M., 2006) e os padrões
classificados pela gangue dos quatro (GAMMA, et al., 1994). Em jogos, algumas dessas boas
práticas da engenharia de software já vêm sendo aplicadas e surgem adaptações dos princípios
SOLID para esse domínio.
Este trabalho tem como principal objetivo realizar a refatoração do jogo Bicho UFC
Rampage aplicando boas práticas da engenharia de software que são, neste caso, o uso dos
padrões de projeto descritos no catálogo de (GAMMA, et al., 1994) e princípios de design de
código SOLID (MARTIN, R.; MARTIN, M., 2006) no ambiente de desenvolvimento de jogos
Unity1. O objetivo é melhorar a qualidade do código para que o projeto se torne mais fácil de
manter. Para realizar essa tarefa, este trabalho utilizará pequenas etapas de refatoração de
código adaptando esses conceitos para o ambiente Unity.
E para comparar a versão inicial e a versão final, foi feita uma análise estática de
código usando a ferramenta NDepend2. As métricas coletadas foram Linhas de código (LOC),
Complexidade Ciclomática (CC) e Dependência. Porém a medida de dependência foi excluída
por conta de falsos positivos. Esses falsos positivos ocorreram por conta que a Unity não
trabalha bem com o uso de interfaces e em diversos pontos ainda são necessárias chamadas a
implementações concretas de classes.
O tema não é novo e trabalhos como os de (PARVIAINEN, 2017) e (FIGUEIREDO e
RAMALHO, 2015) já trataram de aplicar princípios SOLID e padrões de projeto para jogos.
A principal diferença do trabalho apresentado aqui é o jogo e a escolha sobre que padrões
aplicar.
O público alvo deste trabalho são, principalmente, desenvolvedores de jogos que usam
a engine Unity e desejam expandir ou aperfeiçoar seus conhecimentos com boas práticas de
engenharia de software como refatoração, princípios SOLID e padrões de projeto de software.
1 https://unity.com/pt
2 https://www.ndepend.com/
16
Este trabalho está dividido da seguinte forma, a Seção 2 trata de apresentar os
trabalhos com temas semelhantes relacionados. A Seção 3 e suas subseções tratam de
apresentar os conceitos teóricos base deste trabalho que são refatoração, maus cheiros de
design, princípios SOLID, padrões de projeto e sobre o jogo Bicho UFC Rampage. A Seção 3
apresenta os passos de execução deste trabalho apresentando o estado inicial do projeto,
problemas encontrados na versão inicial, cada uma das etapas de refatoração e uma
comparação entre a versão inicial e a final com medidas LOC e CC usando a ferramenta
NDepend.
1.1 Objetivos
Partindo do pressuposto de que é possível se construir uma estrutura de código melhor a partir
de uma estrutura ruim. E usando pequenas modificações no código junto de padrões que
representam o conhecimento prévio de outros desenvolvedores. E usando regras de design de
código. O objetivo principal deste trabalho é a refatoração do jogo Bicho UFC Rampage
aplicando padrões de projeto e princípios SOLID.
A aplicação desse objetivo principal se dá pelos seguintes objetivos secundários:
• Identificar maus cheiros de design na primeira versão do jogo;
• Aplicar princípios SOLID onde existem maus cheiros de design usando refatoração;
• Aplicar padrões de projeto durante a refatoração;
Com isso, é esperado que a estrutura do código do projeto melhore para que novas
modificações possam ser feitas com menos dificuldade.
2 TRABALHOS RELACIONADOS
Nesta seção serão apresentados os principais trabalhos relacionados encontrados durante a
revisão bibliográfica feita buscando outros trabalhos que combinassem os temas de
refatoração, princípios SOLID e padrões de projeto.
2.1 Dependency Injection in Unity3D
Em (PARVIAINEN, 2017), o autor tem como principal objetivo identificar e resolver
problemas técnicos relacionados ao ambiente Unity. Da perspectiva de desenvolvedor, o
trabalho identifica os problemas técnicos que estão relacionados principalmente à gerência de
dependências no desenvolvimento de jogos focando o ambiente Unity.
17
A gerência de dependências na Unity é apresentada como um problema por conta de a
plataforma não oferecer opções eficazes para controlar dependências. Como padrão, o
framework oferece métodos de busca de dependências como GameObject.Find e
Object.FindObjectOfType. Outra forma oferecida é através do Editor da Unity que oferece a
possibilidade de realizar drag and drop de instâncias, mas essa funcionalidade está limitada a
apenas instâncias de objetos da Unity e não é possível usar de abstrações como interfaces
(PARVIAINEN, 2017).
Outro problema apresentado é que a engine não oferece um ponto de entrada único
para a aplicação, o que torna a gerência de dependências mais difícil e dificultando o
desenvolvedor controlar o que é instanciado (PARVIAINEN, 2017).
Como possível solução e melhor abordagem da gerência de dependências na Unity, o
padrão Singleton é apresentado. Dessa forma, não é necessário o uso dos métodos
GameObject.Find e Object.FindObjectOfType (PARVIAINEN, 2017). Porém é um padrão
que deve ser usado com cautela por conta que com ele é difícil controlar estados e usar testes
unitários. A Seção 2.2.3.1 Singleton descreve esse padrão.
Outra forma de gerenciar dependências apresentada, é o uso do framework Zenject3.
Esse framework aplica o padrão Dependeny Injection (DI). Esse padrão é usado para
gerenciar as dependências para que o código se torne mais modular. Dessa forma, objetos não
instanciam suas dependências nem buscam por elas, o framework é o responsável por prover
essas dependências. Vários benefícios são apresentados como a diminuição do acoplamento
entre módulos, facilidade em realizar testes e mocks e late bindings. Também são
apresentadas desvantagens no uso desse padrão. O primeiro é que com o uso de DI, em
projetos grandes, são criados grafos de dependência complexos que são gerenciados
manualmente. E o segundo problema é que não há controle em relação em como as instâncias
de objetos Unity são criadas e não existe um pronto de entrada para a aplicação
(PARVIAINEN, 2017).
O trabalho também apresenta princípios SOLID como forma de criar um bom design
de código. Cada princípio é apresentado partindo de um exemplo que não aplica o princípio
para um exemplo que aplica (PARVIAINEN, 2017).
E por fim, é apresentado um pequeno projeto de teste que usa os conceitos de
fundamentação teórica apresentados. O design da ideia é apresentado junto das tecnologias
3 https://github.com/modesttree/Zenject
18
usadas e detalhes da implementação são apresentados (PARVIAINEN, 2017).
A principal diferença entre (PARVIAINEN, 2017) e este trabalho é que este trabalho
não apresenta o uso do padrão Dependency Injection (DI) com o uso do framework Zenject e
nem cria um projeto de teste. Neste trabalho são apresentados os princípios SOLID e alguns
padrões de projeto comportamentais usados na refatoração do jogo Bicho UFC Rampage.
2.2 Gof design patterns applied to the development of digital games
Outro trabalho semelhante é o (FIGUEIREDO e RAMALHO, 2015), onde são abordados os
usos de padrões GOF para aumentar a capacidade de reuso de componentes, apesar do uso
limitado que essa ferramenta de desenvolvimento tem dentro do desenvolvimento de jogos
com engines.
O trabalho propõe apresentar as melhorias alcançadas com o uso de padrões de projeto
através de um antes e depois da aplicação de padrões GOF. Esses benefícios vêm por conta
que os padrões são uma forma de difusão de conhecimento. Esse conhecimento já foi testado
previamente por outros desenvolvedores em outros problemas com o mesmo contexto. E por
conta disso, sua aplicação por si só é uma forma de documentação. E torna a comunicação do
time mais simples (FIGUEIREDO e RAMALHO, 2015).
No trabalho, são explicados apenas uma pequena gama de padrões por conta da
limitação de páginas. Os padrões apresentados são GOF (GAMMA, et al., 1994) com
adaptações para o desenvolvimento de jogos digitais. Os padrões descritos são Builder,
Prototype, Singleton, Flywheight, Observer e State.
Para demonstrar o impacto do uso de padrões de projeto, um experimento foi
conduzido com estudantes de computação. Os estudantes foram divididos em 6 grupos de três
pessoas cada. Foi aplicado um teste AB comparando os grupos que usaram padrões de projeto
em relação aos grupos que não usaram. Penas três padrões foram usados por conta de
limitações de tempo. Esses padrões foram Singleton, Prototype e Facade. Esse experimento,
foi realizado com o objetivo de verificar se o uso de padrões de projeto reduzia o tempo de
desenvolvimento, diminui a presença de bugs e reduz a quantidade de linhas de código
(FIGUEIREDO e RAMALHO, 2015).
O tempo de desenvolvimento dos grupos que usaram padrões foi de 06:31, enquanto o
tempo dos que não usaram foi de 08:02. Todos os times conseguiram completar a tarefa.
Todos os grupos que não usaram padrões apresentaram bugs, enquanto apenas um dos que
usaram padrões apresentou um bug. Em relação a quantidade de linhas de código, os grupos
19
que usaram padrões tiveram uma contagem de 717, enquanto os que não usaram tiveram uma
contagem de 855 linhas. Em relação a quantidade de classes, os grupos que usaram padrões
tiveram uma contagem de 23 classes, enquanto o grupo que não usou teve uma contagem de 7
classes. Por fim, o ganho de tempo dos grupos que usaram padrões foi de 18,9% e o ganho de
linhas de código foi de 16,14% em relação aos grupos que não usaram (FIGUEIREDO e
RAMALHO, 2015).
As diferenças entre (FIGUEIREDO e RAMALHO, 2015) e este trabalho é que em
(FIGUEIREDO e RAMALHO, 2015) foi abordado um leque maior de padrões GOF, mesmo
essa quantidade sendo limitada por conta da contagem de páginas. E porque foram usados
padrões arquiteturais enquanto este trabalho trata em sua maioria de comportamentais. Outra
diferença foi a forma de validação. Neste trabalho não foram conduzidos experimentos, mas
sim uma pequena análise estática de código usando NDepend.
3 FUNDAMENTAÇÃO TEÓRICA
A execução deste trabalho tem como base refatoração, princípios SOLID e padrões de projeto
para a melhoria do design do código do jogo Bicho UFC Rampage. Os demais tópicos e
subtópicos irão fundamentar o jogo seguido por essas três áreas bases deste trabalho
apresentado exemplos que não fazem parte da solução final, mas que ilustram de forma
simples a aplicação desses conceitos.
3.1 Refatoração
Durante boa parte da história do desenvolvimento de software muitos acreditavam que o
design deveria preceder a implementação. Essa seria uma abordagem em cascata
(SOMMERVILLE, 2011) onde a etapa de design iria preceder e alimentar a etapa seguinte, a
implementação. Porém, existe uma diferença entre modelar e implementar um software. Uma
modelagem é uma maneira abstrata de imaginar o software enquanto a implementação é o
software concreto. Então, à medida que o design fosse implementado, detalhes antes não
planejados seriam identificados e seriam indícios da necessidade de melhorar o design
imaginado no início. À medida que a implementação avança, o código se torna decadente de
maneira que o a implementação vai da engenharia para hacking (FOWLER, 2009).
A abordagem da refatoração segue uma ideia oposta à ideia da degradação que o
software sofreria em uma abordagem cascata. A partir de um design ruim, transformar esse
20
código ruim implementado em um código bem estruturado. Fazendo isso seguindo um passo-
a-passo simples onde, por exemplo alguns dos passos seriam: mover uma propriedade de uma
classe para outra, transformar uma porção de código de um método em um novo método,
deslocar código para cima ou para baixo em uma hierarquia de classes etc. Dessa forma, o
design do código pode melhorar drasticamente. Essa abordagem une as o que antes seriam
duas etapas distintas que antes eram separadas. Essa união faz com que o design e a
implementação passem a se comunicar de forma bidirecional, diferente do canal unidirecional
entre as duas etapas no modo cascata (FOWLER, 2009).
O termo refatoração tem duas interpretações. Uma para a forma substantiva e outra
para a forma verbal. No sentido da primeira, refatorar é “uma alteração feita na estrutura
interna do software para torná-lo mais fácil de ser entendido e menos custoso de ser
modificado sem alterar seu comportamento observável” (FOWLER, 2009, p. 52). No segundo
sentido, a palavra refatorar se refere a uma ação que visa “reestruturar o software aplicando
uma série de refatorações sem alterar seu comportamento observável” (FOWLER, 2009, p.
52). De forma geral, refatorar é modificar a estrutura interna do software sem alterar o que ele
já faz (FOWLER, 2009).
A partir dessas definições, é possível concluir que refatoração não é algo que impacte
nas funcionalidades do software. O impacto acontece em relação a projeto tornando o código
mais fácil de compreender e menos custoso de alterar. Com um código bem estruturado, é
mais fácil realizar modificações.
Neste trabalho, apesar de se tratar da refatoração do código de um projeto, não serão
destacados cada técnica e indícios de necessidade de refatoração. O foco será mantido nas
questões relacionadas a SOLID e padrões de projeto já que essas duas técnicas estão
entrelaçadas e já guiam o desenvolvimento para um design de qualidade, porém, durante o
texto, existem alguns apontamentos que podem remeter a algumas técnicas de refatoração.
3.3 Sobre o jogo Bicho UFC Rampage
O jogo Bicho UFC Rampage foi desenvolvido em 2017 durante a Célula de Desenvolvimento
de jogos do PACCE na UFC no campus de Quixadá, CE. O jogo se trata de um projeto autoral
inspirado em dois jogos antigos que já não se encontram disponíveis, Leo's Red Carpet
Rampage e Super Impeachment Rampage. Nesse jogo o jogador controla um aluno novato na
universidade que precisa realizar suas atividades e fugir dos inimigos que são os alunos
veteranos. E durante essa fuga, ele deve ser rápido e coletar o máximo de itens que puder no
21
trajeto.
3.3.1 Sobre a primeira versão
A primeira versão do jogo foi desenvolvida usando Unity como engine e GIT4 como
ferramenta de versionamento. O GitHub5 foi usado como repositório remoto para o projeto.
Não foi usado nenhum processo de desenvolvimento e cada componente foi implementado
durante encontros da Célula de desenvolvimento de jogos durante 4 meses. Nesse período,
cada encontro acontecia uma vez por semana durante 2 horas. Os responsáveis pelo projeto se
dividiam em tarefas de programação e criação de assets.
Durante o desenvolvimento dessa primeira versão, não foram gerados diagramas.
Apenas um documento de design detalhando o jogo e suas funcionalidades com base no texto
de (CHANDLER, 2009). Nesse documento foi especificado o gancho de jogo, a proposta de
jogos, as mecânicas, condições de vitória e derrota e uma breve história para contextualizar o
jogo.
3.3.2 Estado da primeira versão
O estado inicial do projeto está retratado no diagrama UML da Figura 1 criado usando
engenharia reversa. Para essa criação foram usados duas ferramentas e um plug-in. A principal
ferramenta usada para representar o diagrama UML foi o Astah UML6 com uma licença para
estudante. Então, a segunda ferramenta usada foi o Doxygen7 que é uma ferramenta que
transforma código em XML. Por fim, o plugin da ferramenta Astah UML, o C# Code Reverse
Plug-in8 foi usado para transformar o XML em um diagrama UML de classes.
3.3.3 Problemas encontrados na primeira versão
4 https://git-scm.com/
5 https://github.com/
6 https://astah.net/products/astah-uml/
7 https://www.doxygen.nl/index.html
8 https://astah.net/product-plugins/csharp-reverse/
22
A partir da análise do código e do diagrama da Figura 1, é possível reparar em alguns dados
importantes. O primeiro, existe um total de 3 classes que aplicam o padrão Singleton,
discutido na seção 2.2.3.1 Singleton. Os problemas são a dificuldade de controlar os estados
que uma classe que implementa o padrão está, e a dificuldade de realizar testes unitários com
esse tipo de padrão. Outro dado importante, as duas maiores classes são
ControleDoPersonagem e ControleDaCamera, e são essas classes que carregam as principais
funcionalidades do jogo.
A classe ControleDoPersonagem implementa a maioria das funcionalidades. Essas
responsabilidades são: verificar os inputs do jogador, realizar o movimento para direita
alternando teclas, realizar o salto do personagem, contar a quantidade de itens e realizar o
dash, aplicar a penalidade de colisão. São no total 4 responsabilidades de podem ser
decompostas em mais componentes. Isso evidencia os maus cheiros de Rigidez, pois uma
simples mudança pode causar uma cascata de modificações, Fragilidade, já que o código pode
quebrar em diversos pontos. O código também é Viscoso, porque existem muitas “gambiarras”
e modificações podem gerar mais “gambiarras”. O código também apresenta o mau cheiro de
Opacidade porque o código está muito difícil de compreender.
A classe ControleDeCamera, a segunda maior classe, controla elementos de interface
além do que o próprio nome propõe, a tornado Frágil e Rígida. Ela também é difícil de
compreender e seu design cheira a Opacidade. Essa classe também apresenta Repetição
Desnecessária porque verifica os inputs do jogador assim como outras classes.
A classe ControleDeTempo apresenta Repetição Desnecessária por estar verificando
inputs do jogador assim como outras classes.
Esses são os principais problemas identificados na versão inicial do projeto. O
próximo passo deste trabalho envolve descrever como esses problemas foram resolvidos ou
amenizados através da aplicação dos princípios SOLID e padrões de projeto. Não será
retratado o processo de refatoração de forma detalhada, apenas serão apresentadas as soluções
explicando brevemente como e por que se chegou na solução. Também não serão usados
testes unitários.
23
Tabela 1 ─ Funcionalidades do jogo em sua versão inicial
Funcionalidades
O personagem deve se mover constantemente para direita assim que o primeiro
input do jogador seja emitido. Enquanto esse input não é emitido, o jogo deve
exibir uma tela de início da fase.
O jogador coleta itens ao longo das fases que contam pontos no score total de
pontos do jogador.
A câmera deve se mover constantemente para direita assim que o jogador emitir
seu primeiro input. Caso o personagem do jogador fique fora do enquadramento
da câmera durante a fase, o personagem morre e o jogo deve ser reiniciado.
O tempo que o jogador leva para concluir a fase deve ser contado a partir do
momento que o primeiro input do jogador é emitido e deve parar de contar assim
que o personagem atinge o final da fase.
Após o jogador coletar 5 itens, o dash do personagem deve ser liberado para uso.
Esse dash é um aumento de velocidade que dura por um curto período. Assim que
o dash for usado, o contador deve ser zerado.
O jogador move o personagem para a direita pressionando alternadamente as
teclas “a” e “d”, e para que o personagem pule, a tecla “espaço” deve ser
pressionada. Não deve ser possível que sejam realizados saltos enquanto o
personagem está no ar.
Caso o jogador colida com algum obstáculo, o personagem recebe uma pequena
penalização de 0.7 segundos. Nesse tempo, o personagem tem seus controles
bloqueados e se movo lentamente para esquerda.
A HUD do jogo deve exibir as seguintes informações para o jogador: vida do
personagem, contador de itens para o dash ficar disponível e o total de pontos do
jogador.
Fonte: Autor.
24
Figura 1 ─ Diagrama de classes UML que representa o estado inicial do projeto
Fonte: Autor.
O jogo consiste em controlar o personagem pressionando alternadamente as teclas “a”
e “d” para que o personagem se mova e não saia do enquadramento da câmera. O jogador
também pode pular obstáculos pressionando a tecla “espaço”. Caso o jogador saia do
enquadramento da câmera, o personagem morre e a fase deve ser reiniciada. O jogador deve
coletar os itens espalhados pelas fases para aumentar sua pontuação de escore e deve se mover
o mais rápido possível para que seu tempo durante o percurso da fase seja o menor possível.
Essas funcionalidades estão representadas na Tabela 1 que lista uma descrição simplificada de
cada funcionalidade.
3.2 Princípios SOLID e maus cheiros de design
Em um projeto de software, principalmente em projetos ágeis, a ideia geral do projeto evolui
durante o desenvolvimento. O código que é implementado não é criado para antecipar
features que podem ser necessárias no futuro. Ao contrário, os desenvolvedores focam na
estrutura atual do sistema fazendo-o o melhor que possa ser. Dessa forma o software evolui de
forma incremental até atingir a sua arquitetura e design ideal sem que esforço e custo sejam
desperdiçados com possíveis features que não se sabe se serão realmente necessárias
25
futuramente (MARTIN, R.; MARTIN, M., 2006).
Para que o código do projeto evolua sendo construído da melhor forma possível, os
desenvolvedores precisam de uma base para firmar suas decisões de design. Para isso, existem
princípios que servem como guias para o design do código. Os princípios abordados neste
trabalho são SOLID, um acrônimo que nomeia um conjunto de cinco princípios para criar
estruturas de nível médio que: tolerem mudanças; sejam fáceis de entender e; sejam a base de
componentes que possam ser usados em muitos sistemas de software (MARTIN, 2019).
Entretanto, os princípios não devem ser aplicados sem justificativa, ou complexidade
desnecessária pode ser adicionada ao código do projeto tornando-o difícil de manter. Para isso,
existem certos sintomas de maus cheiros que são usados como indicadores de que o código
está em desacordo com um ou mais princípios. Esses maus cheiros diferem dos maus cheiros
de código por conta de estarem relacionados ao design, e consequentemente estão em um
nível de abstração mais alto (MARTIN, R.; MARTIN, M., 2006).
A seguir, primeiro serão apresentados os maus cheiros que indicam a necessidade de
refatoração do código para que ele fique de acordo com um ou mais princípios. Em seguida
cada um dos princípios será apresentado.
3.2.1 Maus cheiros de design
Usamos princípios para guiar o código a um bom design, entretanto, um bom design não usa
desses princípios de forma descontrolada e impensada. Então, uma boa prática é aplicar os
princípios apenas onde é possível identificar maus cheiros de design. Esses maus cheiros
estão descritos nas subseções abaixo.
A refatoração também abre espaço para a aplicação de Padrões de projeto, tratados na
seção 3.2.3 Padrões de projeto de software. Usar padrões significa usar conhecimento prévio
de outros desenvolvedores que resolveram problemas com contextos semelhantes. E eles
foram criados tentando usar os princípios de orientação a objetos da melhor forma. Além de
melhorar a comunicação entre os desenvolvedores. E estarem de acordo com os princípios
SOLID (FIGUEIREDO, 2015).
3.2.1.1 Rigidez
Rigidez é a tendência para um software de ser difícil de modificar mesmo em modificações
simples. Então se uma simples mudança causa uma cascata de outras modificações em
26
módulos dependentes, o design apresenta o mau cheiro de Rigidez. E quanto mais mudanças
forem necessárias, mais rígido o design é (MARTIN, R.; MARTIN, M., 2006).
3.2.1.2 Fragilidade
Fragilidade é a tendência de o software quebrar em vários lugares quando uma simples
mudança é feita. Frequentemente essas quebras acontecem em módulos que não tem relação
conceitual com o módulo onde foi feita a mudança. Ou seja, módulos que deveriam ser
independentes, são completamente dependentes de forma que uma mudança em um módulo
quebra os demais (MARTIN, R.; MARTIN, M., 2006).
3.2.1.3 Imobilidade
Imobilidade é a incapacidade de reaproveitamento de módulos de software que podem ser
úteis em outros sistemas, mas que seu reuso é impossível por conta dos riscos envolvidos em
separar o dito módulo de seu sistema original (MARTIN, R.; MARTIN, M., 2006).
3.2.1.4 Viscosidade
A viscosidade pode acontecer tanto em software quanto em relação ao ambiente. E acontece
quando uma mudança tem mais de uma maneira de ser feita, e as maneiras que preservam o
design são mais difíceis do que as maneiras que criam “gambiarras”. Dessa forma o design
cheira a Viscosidade. A Viscosidade em relação ao ambiente acontece quando o ambiente de
desenvolvimento é lento e ineficiente. Em ambos os casos a Viscosidade é alta quando o
design é mais difícil do que o uso de “gambiarras” (MARTIN, R.; MARTIN, M., 2006).
3.2.1.5 Complexidade desnecessária
Um design cheira a Complexidade desnecessária quando o código carrega recursos que não
estão sendo usados. Acontece frequentemente quando os desenvolvedores adicionam
facilidades no código visando futuras mudanças. Porém, isso é desperdício de esforço e custo
já que essas facilidades podem não ser necessárias futuramente (MARTIN, R.; MARTIN, M.,
2006).
27
3.2.1.6 Repetição desnecessária
A repetição desnecessária acontece quando o mesmo trecho de código aparece repetidas vezes,
de formas levemente diferentes, em diversas partes do código. Esse é um forte indício que os
desenvolvedores estão fazendo mau uso de abstrações. Dessa forma, o trabalho de realizar
mudanças pode ser muito oneroso por conta que os trechos de código semelhante podem
necessitar de modificação também (MARTIN, R.; MARTIN, M., 2006).
3.2.1.7 Opacidade
O código evolui ao decorrer do tempo e essa evolução torna o código cada vez mais difícil de
entender. Quando um módulo se torna muito difícil de entender, o design cheira a Opacidade
(MARTIN, R..; MARTIN, M., 2006).
3.2.2 Princípios SOLID
SOLID é um acrônimo para um conjunto de princípios para design de código. Esses
princípios são o guia para criar estruturas de código que: tolerem mudanças; sejam fáceis de
entender e; sejam a base de componentes que possam ser usados em muitos sistemas de
software. Entretanto, o uso desses princípios deve ser feito apenas quando modificações são
necessárias e as necessidades dessas modificações evidenciem maus cheiros presentes no
design do código (MARTIN, 2019).
3.2.2.1 Princípio da responsabilidade única (SRP)
O SRP aponta que “uma classe deve ter apenas uma razão para mudar”. Esse princípio se
baseia na ideia de que cada responsabilidade é um único eixo de mudança. Ou seja, quando
um requisito muda, apenas os eixos de responsabilidade atrelados a esse requisito e essa
mudança que devem sofrer alteração (MARTIN, R.; MARTIN, M., 2006).
Então, como exemplo, uma classe Player que carrega duas responsabilidades. Uma de
conectar com o servidor e outra de tratar da comunicação com o servidor, como retrata a
Figura 1. Agora imagine que a lógica como a conexão acontece precisa mudar. Nesse caso, os
métodos Connect e Disconnect, contidos em Player devem ser modificados. Porém,
conceitualmente, é claro que a classe Player não deveria ser modificada por conta de uma
mudança relacionada a conexão, pois como seu próprio nome sugere, a classe deveria tratar
28
apenas de funcionalidades relacionadas ao personagem que o jogador controla. Isso mostra
que a classe tem dois eixos de mudança.
Figura 2 ─ Classe em desacordo com o princípio SRP
Fonte: Autor.
Uma possível solução para este problema seria a separação a responsabilidade de tratar
da conexão para uma classe separada. Isso se trata da extração de dois métodos para uma nova
classe chamada Connection. Dessa forma, qualquer mudança que precise ser feita em como a
conexão acontece deve ser modificado apenas na classe Connection. A solução está
representada na Figura 2.
Figura 3 ─ Classe em acordo com o princípio SRP
Fonte: Autor.
Designs de código que têm classes que carregam mais de uma responsabilidade
cheiram a Fragilidade. Perceba que a modificação pode quebrar tanto em relação as
funcionalidades do personagem, quanto em relação a conexão.
Em Martin (2019) são apresentados conceitos mais concisos a respeito da definição de
SRP. A nova definição é “um módulo deve ser responsável apenas por um, e apenas um, ator”.
Essa “redefinição” deixa claro que um eixo de mudança está mais relacionado com um ator do
que diretamente com um requisito. Isso porque as mudanças nos requisitos são necessárias
quando as necessidades dos stakeholders envolvidos no projeto mudam.
29
3.2.2.2 Princípio do aberto/fechado (OCP)
Segundo a definição de OCP “Um artefato de software deve ser aberto para extensão, mas
fechado para modificações” (MARTIN, 2019, p. 70). Então, os artefatos devem estar prontos
para sofrer mudanças ou extensões em seu comportamento sem que o código antigo seja
modificado (MARTIN, 2019).
Partindo da definição, artefatos que estão de acordo com OCP apresentam duas
características. A primeira, é que são abertos a extensão. Então, o comportamento do artefato
deve ser fácil de estender alterando seu comportamento. A segunda característica é que o
artefato deve ser fechado para modificação, ou seja, estender o comportamento não deve
resultar em modificações ao código fonte, módulo ou binário (MARTIN, 2019).
Artefatos que não estão de acordo com OCP tendem a sofrer uma cascata de mudanças
em módulos dependentes quando uma simples modificação é feita. Essa cascata de
modificações decorrentes de uma simples mudança, indica que o design tem o mau cheiro de
Rigidez (MARTIN, R.; MARTIN, M., 2006).
Para que esse isolamento do que já foi desenvolvido em relação a extensões é
alcançado através do uso de abstrações. Quando os artefatos se isolam de outros artefatos
através de abstrações(contratos), as modificações acontecem na implementação dessas
abstrações sem que o contrato seja desrespeitado. Dessa forma modificações não causam
impactos em dependentes (MARTIN, 2019).
Como exemplo, imagine que temos um jogador que controla um personagem que usa
uma pistola no jogo. A Figura 3 mostra essa funcionalidade com as classes Player e Pistol.
Agora, supondo que o projeto do jogo evoluiu e a possibilidade de o personagem usar uma
pistola mudou para que agora o jogador possa escolher entre duas armas, uma pistola e uma
calibre 12. A classe Pistol precisa mudar e essa mudança impacta diretamente a classe Player.
Isso acontece porque a classe Player depende diretamente de uma implementação de Pistol.
Figura 4 ─ Exemplo da implementação de um personagem que usa uma pistola
Fonte: Autor.
30
Para que a classe Player esteja protegida das mudanças que acontecem e ele usar uma
arma independente de qual seja, a classe Player deve deixar de depender diretamente de uma
classe concreta. Para isso, a dependência de Player muda para depender de uma interface.
Dessa forma Player passa a depender de uma abstração e qualquer arma que implemente a
interface IGun pode satisfazer essa dependência, como mostra a Figura 4. Dessa forma,
Player agora está isolada de modificações relacionadas as armas que usa e ao mesmo tempo
aberta a qualquer extensão que adicione uma nova arma ao jogo.
Figura 5 ─ Exemplo do isolamento da classe Player das mudanças que acontecem em relação
as armas
Fonte: Autor.
A troca da dependência de Player de uma classe concreta pela interface IGun foi a
aplicação de um outro princípio, a Inversão de Dependência (DIP) tratada na seção 2.2.2.5.
Essa inversão de dependência aplicou o padrão de projeto Strategy que é tratado na seção
xxxx.
3.2.2.3 Princípio de substituição de Liskov (LSP)
O LSP é um princípio que guia o bom uso de heranças e até mesmo a implementação de
interfaces. Sua definição é “Subtipos devem ser substituíveis pelos seus tipos bases”
(MARTIN, R.; MARTIN, M., 2006, p. 136).
O mau uso de herança e implementação de abstrações são características que mostram
quando um design está em desacordo com LSP. Esse tipo de artefato apresenta o mau cheiro
de Fragilidade. Isso porque quando subtipos não são substituíveis por seus tipos base, o
código tende a quebrar em diversas partes.
Por exemplo, considere um jogo que tem diferentes tipos de itens coletáveis, um de
pontos de score e outro de vida para o personagem que o jogador controla. A Figura 5 mostra
um trecho de código que mostra como a contagem de pontos acontece. E a Figura 6 mostra
como os tipos e subtipos estão implementados.
31
Figura 6 ─ Classe Player responsável por realizar a atualização de pontos e de vida do
jogador quando um item é coletado
Fonte: Autor.
Figura 7 ─ Implementação dos coletáveis mostrando a superclasse e subclasses
Fonte: Autor.
32
Suponha que o jogo agora tem um novo tipo de coletável para ser adicionado. O
jogador poderá coletar vidas durante as fases do jogo. Essa necessidade de modificação irá
causar impacto na classe Player com a adição de uma nova verificação if para o novo tipo de
coletável. E será necessária a criação de um novo tipo que herda de CollectibleBase e a adição
do novo tipo em CollectibleType. Isso mostra que o exemplo também está em desacordo com
o Princípio de aberto/fechado (OCP) tratado na seção 2.2.2.2 e o design cheira a Rigidez já
que essa modificação pode causar uma cascata de modificações em artefatos dependentes. E
este design não está de acordo com LSP porque nenhum dos tipos derivados de
CollectibleBase são substituíveis pelo seu tipo base. A substituição dos subtipos pelos tipos
base resultaria em “gambiarras” e com isso o design iria cheirar a Fragilidade por conta da
facilidade de o código quebrar em diferentes partes por conta desses hacks.
Uma possível solução seria criar uma abstração que represente qualquer coletável do
jogo. Isso porque qualquer coletável no jogo incrementa ou decrementa um contador próprio.
Para isso, a Figura 7 mostra a modificação feita no tipo CollectibleBase que define um
método geral que qualquer subtipo deve implementar. Nessa modificação, as instâncias de
tipos que controlam os contadores descem na hierarquia saindo da superclasse para as
subclasses e o tipo CollectibleType não mais necessário.
Figura 8 ─ Trecho da implementação dos coletáveis de acordo com LSP
Fonte: Autor.
Agora, como mostra a Figura 8, qualquer subtipo de CollectibleBase é substituível
pelo tipo base. A aplicação de LSP neste exemplo foi feita com a aplicação de um padrão de
33
projeto chamado Template Method que é discutido na seção 3.2.3.2 Template Method.
Figura 9 ─ A Player usa qualquer coletável que seja subtipo de ColectibleBase
Fonte: Autor.
3.2.2.4 Princípio da segregação de interfaces (ISP)
O ISP diz que “Clientes não devem ser forçados a implementar métodos que eles não usam”
(MARTIN, R.; MARTIN, M., 2006, p. 166). Esse princípio lida com as desvantagens de lidar
com interfaces que não são coesivas. Essas interfaces podem ser divididas em grupos de
serviço para cada grupo de clientes tornando-as mais coesivas.
Classes que não tem interfaces coesivas não devem ser apresentadas aos clientes de
forma concreta apresentando serviços que um cliente consome e outro não. Do contrário,
clientes que consomem serviços diferentes podem acabar impactados por mudanças em
serviços que eles não consomem. Para evitar esse problema, classes que apresentam serviços a
diferentes grupos de cliente, devem apresentar apenas os serviços que cada grupo de clientes
precisa. Para isso, cada grupo de clientes deve depender de uma interface da classe de serviço
que expõe apenas o que esse grupo de clientes precisa consumir (MARTIN, R.; MARTIN, M.,
2006).
Como exemplo, imagine um jogo onde o personagem controlado pelo jogador precise
mover para direita, esquerda e pular. E que a implementação dessas funcionalidades foi feita
usando da composição de componentes. O componente Player identifica os inputs do jogador
e delega o que deve ser feito para um outro componente que mantém as funcionalidades e
mover e pular. A Figura 9 mostra o diagrama UML que representa essa estrutura em
34
desacordo com ISP.
Figura 10 ─ Estrutura em desacordo com ISP
Fonte: Autor.
Nesse exemplo, a classe PlayerInputs oferece serviços a vários clientes. E cada um
desses clientes usa serviços diferentes. O problema com esse design é que mudanças em
serviços relacionados a UI podem impactar no serviço do PlayerControls. Para que o exemplo
esteja de acordo com ISP e mudanças não impactem clientes não relacionados, os serviços
prestados a cada grupo de clientes devem ser separados através do uso de interfaces. A Figura
10 mostra a solução.
Figura 11 ─ Serviços para diferentes clientes separados através de interfaces
Fonte: Autor.
Dessa forma os diferentes serviços que cada um dos clientes depende está separado
35
através de interfaces. Assim, o impacto em mudanças em serviços não relacionados menor.
No exemplo da Figura 10, o não uso de ISP pode implicar em “gambiarras” para diminuir o
impacto das mudanças tornando o código Viscoso.
3.2.2.5 Princípio da inversão de dependência (DIP)
Segundo DIP, sistemas flexíveis não tem dependências de código fonte, eles apenas se
referem a abstrações (MARTIN, 2019). Dessa forma os sistemas são flexíveis o suficiente
para que suas implementações possam mudar diminuindo o impacto em dependentes. Isso
pode ser resumido como: não se deve depender de nada que seja concreto (MARTIN, 2019).
Depender de elementos concretos é arriscado. Esse risco decorre do fato que
implementações são menos estáveis do que abstrações. Então, depender de abstrações é mais
seguro (MARTIN, 2019).
Os exemplos das seções 2.2.2.2 Princípio de aberto/fechado (OCP), 2.2.2.3 Princípio
de substituição de Liskov e 2.2.2.4 Princípio da segregação de interfaces todos usam de
inversão de dependência. Apenas com uma pequena diferença no exemplo de aplicação de
LSP em que é usado herança. Entretanto é uma herança onde existe um método abstrato que
as subclasses devem implementar.
3.2.3 Padrões de projeto de software
Um padrão de projeto de software é um conjunto de contexto, problema e uma solução
documentada. Essa solução não é nova, ela é uma solução consolidada que já foi usada e
testada em outros projetos por outros desenvolvedores (GUERRA, 2014). Nos subtópicos a
seguir serão tratados os principais padrões usados neste trabalho.
3.2.3.1 Singleton
O padrão Singleton garante que exista apenas uma instância de objeto. E para garantir que só
exista uma única instância, a classe controla como uma instância é criada. Essa classe garante
que não exista outra instância do mesmo objeto, e que esse objeto seja de fácil acesso
(GAMMA, et al., 1994).
Esse padrão deve ser usado com muito cuidado por conta das dificuldades envolvidas
em controlar estados e usar testes unitários. Também é necessário cuidado em relação a
36
destruição da instância de objetos Singleton. O trecho de código da Figura 11 mostra a
estrutura básica de um Singleton.
Figura 12 ─ Estrutura básica do padrão Singleton em código escrito em C#
Fonte: Autor.
3.2.3.2 Template Method
O padrão Template Method é um padrão comportamental que define um esqueleto básico de
um determinado algoritmo onde certos passos específicos são delegados as subclasses
(GAMMA, et al., 1994). O código genérico que são passos que as subclasses executam de
forma igual, são adicionados a uma superclasse. Os passos específicos de cada subtipo são
implementados através de um método abstrato que é definido na superclasse. Dessa forma o
código que representa os passos gerais é implementado na superclasse e o código é
reaproveitado através da herança, e os passos específicos ficam a cargo das subclasses.
O exemplo da Figura 7, na seção 2.2.2.3 Princípio de substituição de Liskov (LSP),
temos uma aplicação do padrão Template Method onde o método void UpdateValue(int value)
é uma abstração do que qualquer coletável do jogo precisa para incrementar ou decrementar
qualquer contador do jogo. Esse método gancho (GUERRA, 2014) que inicia a execução do
código que é específico da subclasse.
3.2.3.3 Strategy
37
O padrão Strategy também é um padrão comportamental em que é possível definir uma
família de algoritmos, onde cada um é encapsulado e cada um deles é intercambiável de
acordo com os clientes que os usam (GAMMA, et al., 1994). Esse padrão permite que
comportamento seja trocado em tempo de execução de acordo com a instância usada,
contanto que a classe dessa instância seja a implementação de uma interface.
O exemplo da Figura 4, da seção 2.2.2.2 Princípio do aberto/fechado (OCP), é um
exemplo da aplicação do padrão. Com essa aplicação, o jogador alterna entre as armas
disponíveis apenas trocando entre instâncias de classes que implementam IGun. Isso também
é uma clara aplicação do Princípio da inversão de dependência (DIP) descrito na seção
2.2.2.5 porque a classe Player não depende de uma instância concreta de uma classe que
represente uma arma, mas sim de uma interface IGun.
3.2.2.4 Observer
O padrão Observer é um outro padrão comportamental. Esse padrão define uma dependência
de um para muitos entre objetos de forma que quando um objeto observado muda de estado,
os objetos observadores são notificados a respeito da mudança de estado (GAMMA, et al.,
1994). Esse padrão é muito usado em frameworks de várias linguagens como forma de
notificação do acontecimento de interações do usuário com a interface do sistema.
A implementação desse padrão apresenta um objeto que muda de comportamento
chamado de Subject. Os objetos que desejam ser notificados a respeito da mudança de estado
do Subject, são chamados de Observers. Quando um Observer deseja saber a respeito da
mudança de estado de um Subject, ele se inscreve na lista de notificação do Subject. Dessa
forma, quando a mudança de estado acontecer, os Observers são notificados. A estrutura
básica do padrão Observer está representado na Figura 12.
38
Figura 13 ─ Estrutura básica do padrão Observer em UML
Fonte: Gamma, et al. (1994).
4 METODOLOGIA
A metodologia deste trabalho se divide em 4 passos fundamentais. O primeiro foi o passo de
revisão bibliográfica em busca de livros e trabalhos semelhantes aos temas tratados. O
segundo passou foi uma avaliação da estrutura da primeira versão verificando funcionalidades
e estrutura. O terceiro passo foi o processo de refatoração usando os conceitos descritos na
Seção 3 de fundamentação. E o último passo foi a análise estática de código usando o
NDepend. Os subtópicos a seguir detalham a metodologia usada na execução deste trabalho.
4.1 Revisão bibliográfica
A etapa de levantamento bibliográfico, foi feita em busca de trabalhos e livros sobre cada um
dos principais fundamentos abordados neste trabalho. Os temas de busca foram relacionados a
refatoração, princípios SOLID e padrões de projeto. As buscas com relação a padrões de
projeto e princípios de design SOLID, foram realizadas através do Scholar Google, e entre os
anais da SBGames de diversos anos. As principais palavras-chave de busca foram:
• “code quality”;
• “code metrics”;
• “software quality”;
• “qualidade de software”;
• “métricas de código”;
39
• “design smells”;
• “code smells”;
• “design patterns”;
• “padrões de projeto”;
• “game patterns”;
• “game design patterns”;
• “agile design”;
• “solid principles”;
• “princípios solid”;
• “agile principles solid”;
Após as buscas, foi feita uma filtragem de trabalhos e livros que tinham conteúdo
coerente com o tema deste trabalho. Para isso, a introdução e a estrutura de tópicos dos
trabalhos e livros foram analisados. Os que passaram pela primeira filtragem depois foram
lidos por completo ou lidos apenas os tópicos necessários, e somente os que tinham relação
com os temas deste trabalho foram mantidos e usados como referencial teórico.
4.2 Avaliação da primeira versão
A avaliação da primeira versão foi feita para rever os conceitos usados na elaboração da
primeira versão. Para isso, o código foi analisado tanto com leitura quanto com o uso de
diagramas. Os diagramas usados na avaliação inicial foram obtidos através de engenharia
reversa do código do projeto usando a ferramenta Doxygen para criar uma versão do código
em arquivos XML, e o plugin do Astah UML C# Code Reverse Plug-in que usa o projeto em
XML para transformá-lo em diagramas.
A leitura do código e a análise da estrutura por meio do código e dos diagramas, foi
possível perceber os problemas relacionados a maus cheiros de design, dependências,
tamanho das classes etc. Essa etapa foi fundamental para a etapa seguinte de refatoração.
4.3 Refatoração do código do projeto
A terceira etapa foi a aplicação do processo de refatoração. A refatoração foi realizada em
pequenos passos. Em cada passo, uma parte da estrutura ou funcionalidade era analisada em
40
busca de maus cheiros de design e possíveis padrões de projeto que poderiam ser aplicados.
Em seguida, o código era refatorado aplicando os princípios SOLID e padrões de projeto
quando aplicáveis. Durante esse processo, não foram usados testes unitários, os testes eram
apenas de uso verificando se a funcionalidade era mantida em relação a primeira versão.
4.4 Análise estática de código usando NDepend
Para verificar as diferenças entre a versão inicial e a final, foi feita uma análise estática de
código na primeira e na versão final. As medidas realizadas foram em relação as Linhas de
Código (LOC), Complexidade Ciclomática (CC) e Dependências. Mas infelizmente as
medidas de Dependência tiveram de ser desconsideradas pois apresentavam falsos positivos
por conta de como a Unity funciona. Os dados obtidos mostraram diferenças sutis entre as
versões, mas que mesmo assim indicaram que houve uma mudança significativa.
5 DESENVOLVIMENTO
Para o desenvolvimento deste trabalho, primeiro será apresentado o estado inicial do projeto
apresentando os componentes, suas funcionalidades e como esses componentes se relacionam.
Logo depois, serão apresentados os problemas dessa versão inicial em relação ao design desse
código. Serão indicados os maus cheiros de design que mostram a necessidade da refatoração
para os princípios SOLID (MARTIN, R.; MARTIN, M., 2006). E por fim, serão apresentadas
as versões finais dos componentes após a refatoração para aplicar os princípios e os padrões.
Os padrões serão consequência da aplicação dos princípios. Quando não, será justificado sua
aplicação.
5.1 Refatoração
O processo de refatoração não irá contemplar todas as funcionalidades da Tabela 1. Serão
refatoradas as funcionalidades de controles do personagem, inputs do jogador, sistema de
contagem de pontos de escore, sistema de movimento e vida do personagem e penalidade de
colisão com obstáculos na cena. A ordem de refatoração foi feita partindo dos pontos no
código do projeto mais importantes. Classes com maior importância são as classes que
concentram as principais funcionalidades do jogo. Então a sequência de mudanças segue a
ordem de prioridade da classe mais importante até a menos importante.
41
5.1.1 Separando as responsabilidades e invertendo dependências da classe
ControleDoPersonagem
A classe ControleDoPersonagem que é onde ocorre a maior concentração de funcionalidades.
Por conta disso essa classe carrega um total de 4 eixos de mudança. Isso significa que uma
mudança pode gerar uma cascata de outras mudanças e essas responsabilidades devem ser
desacopladas.
As responsabilidades foram separadas em novas classes. Essas classes são:
PlayerInputs, PlayerControls, Mover, Jumpper foram criadas. O diagrama UML representado
na Figura 14 mostra como essa estrutura foi criada. PlayerControls é a classe responsável por
receber as verificações de inputs que vem através da classe PlayerInputs, e executar as ações
do personagem de acordo com esses inputs. As ações de pular e mover para a direita são
executadas através do padrão Template Method, visto na seção 2.2.3.2 Template Method.
Dessa forma, caso uma nova ação do personagem seja necessária, basta criar uma subtipo de
ActionBase e sobrescrever o método void DoAction(). Dessa forma, o design está de acordo
com o princípio SRP, descrito na seção 2.2.2.1 Princípio da responsabilidade única (SRP),
pois as responsabilidades estão bem definidas e cada classe tem apenas um eixo de mudança.
E está de acordo com OCP descrito na seção 2.2.2.2 Princípio de aberto/fechado (OCP).
Entretanto, o design ainda não está de acordo com DIP, descrito na seção 2.2.2.5
Princípio da inversão de dependência (DIP) porque as dependências entre as classes são
todas concretas. Então as o DIP precisa ser aplicado para tornar as dependências concretas em
dependências abstratas.
MoveAction precisa saber se o personagem está em contato com o chão para poder
realizar o movimento para direita. Então, para não depender da classe concreta JumpAction, a
dependência foi invertida para a interface IGroudChecker. Essa dependência é necessária
porque só quando o personagem está em contato com o chão que ele pode mover para direita.
A classe PlayerControls depende apenas da abstração de ActionBase para as ações de mover e
saltar, então já está de acordo com DIP. Entretanto, PlayerControls depende diretamente de
uma instância da classe PlayerInputs. Isso é um problema caso seja necessária a inclusão de
controles para uma nova plataforma, como por exemplo se o jogo precisar ser portado para
dispositivos Android. Nesse caso, a inversão de dependências abre espaço para aplicação do
padrão Strategy, discutido na seção 2.2.3.3 Strategy. A Figura 15 mostra as dependências
citadas invertidas para interfaces entre as classes.
42
Figura 14 ─ Estrutura da separação das responsabilidades da classe ControleDoPersonagem
retirando as responsabilidades de verificar inputs, mover para direita e pular após a aplicação
do Princípio da Responsabilidade única SRP.
Fonte: Autor.
A aplicação da inversão de dependências entre a classe MoveAction e JumpAction é
uma clara aplicação de ISP, descrito na seção 2.2.2.4 Princípio da segregação de interfaces
(ISP) porque MoveAction é um cliente de JumpAction e esse cliente deve ser isolado das
mudanças que podem ocorrer em outras funcionalidades de JumpAction. Com o uso da
inversão de dependências, esse isolamento acontece.
43
Figura 15 ─ Estrutura da separação das responsabilidades da classe ControleDoPersonagem
retirando as responsabilidades de verificar inputs, mover para direita e pular após a aplicação
do Princípio da inversão de dependências DIP
Fonte: Autor.
5.1.2 Mudando a forma como a contagem de pontos é feita usando o padrão Observer
Sempre que um item é coletado pelo jogador, o contador de score deve ser incrementado e o
contador de itens para o dash também deve ser incrementado. Para isso foi implementado o
padrão Observer, discutido na seção 2.2.3.4 Observer. Esse padrão será implementado com o
uso de serialização usando os recursos da classe ScriptableObject. Então uma classe chamada
OnItemCollectedEvent foi criada com a responsabilidade de adicionar, remover e notificar
ouvintes que implementam a interface IOnItemCollectedListener. A figura 16 mostra um
diagrama UML que retratando a estrutura proposta. Em seguida, a Figura 17 mostra o código
que implementa a classe OnItemCollectedEvent estendendo de ScriptableObject.
Classes que estendem de ScriptableObject, são adicionados no projeto como assets.
Esse tipo de objeto é facilmente serializado e todo esse processo fica a cargo da própria Unity.
Então, foi criado um asset no projeto que é a instância do evento de item coletado. Essa
mesma instância é adicionada as classes interessadas em se inscrever no evento e nas que
44
disparam o evento.
Figura 16 ─ Estrutura do sistema de score e itens implementado com o padrão Observer
Fonte: Autor.
Com esse padrão, agora é possível que qualquer ouvinte seja notificado a respeito de itens
coletados durante a fase, contanto que o ouvinte implemente a interface
IItemCollectedListsner e se inscrever na instância de um objeto OnItemCollectedEvent.
Figura 17 ─ Implementação do evento que notifica interessados em quando um item é
coletado
Fonte: Autor.
45
5.1.3 Separação da funcionalidade Dash da classe ControleDoPersonagem, inversão de
dependências e organização em camadas
O dash do personagem foi implementado na classe ControleDoPersonagem. O código está
distribuído em dois métodos como mostra a Figura 16. O código apresenta o mau cheiro de
Opacidade já que é difícil de ser compreendido e qualquer alteração resultaria em uma cascata
de mudanças mostrando que o design cheira a Rigidez. Uma alteração no salto ou no dash
resultaria em falhas, mostrando que o design do código cheira a Fragilidade. E supondo uma
mudança, seria mais fácil realizar “gambiarras” para que uma extensão de funcionalidade
fosse adicionada a base de código existente o que é um sinal do mau cheiro de Viscosidade.
Figura 18 ─ Código do dash acoplado ao código do salto do personagem tornando o design
Viscoso, Rígido, Frágil e Opaco.
Fonte: Autor.
Para remover o dash da classe ControleDoPersonagem, foi necessário decompor a
funcionalidade em outras classes que são: DashAction que é uma subclasse de ActionBase, o
DashCounter que controla a contagem de itens coletados e a DashUI que exibe a quantidade
de itens coletados para o jogador. A Figura 17 mostra um diagrama de classes com a estrutura
46
de solução.
Essa estrutura divide-se em camadas, uma que realiza a ação representada pela classe
DashAction, outra pela contagem e controle de quando o dash deve ser usado ou não que é a
classe DashCouter. E a classe responsável pela exibição da contagem na interface, a classe
DashUI. Sempre que o jogador pressionar a tecla “backspace”, caso cinco itens tenham sido
coletados pelo jogador, o personagem realiza o dash.
Figura 19 ─ Diagrama de classes UML que mostra a estrutura da funcionalidade dash
implementada
Fonte: Autor.
Dessa forma, o design desse módulo está de acordo com SRP, descrito na seção 2.2.2.1
Princípio da responsabilidade única (SRP) pois cada classe tem apenas um eixo de
responsabilidade. O design também está de acordo com 2.2.2.4 Princípio da segregação de
interfaces (ISP) porque as classes servem seus clientes através de abstrações de interface que
expõem apenas o que o cliente usa. O design também está de acordo com DIP o 2.2.2.5
Princípio da inversão de dependência (DIP) porque as dependências das classes
implementadas são direcionadas apenas a abstrações e não implementações concretas.
5.1.4 Dinâmica de movimento da câmera e fim de jogo
47
A câmera do jogo deve iniciar parada enquanto a interface de início da fase é iniciada. Assim
que o jogador dá o primeiro input, a interface some e a câmera começa a se mover para direita
em velocidade constante. Caso o personagem saia do enquadramento da câmera, o fim de
jogo acontece com a câmera parando de se mover e a interface de fim de jogo aparecendo.
O design do código cheira a Fragilidade porque uma pequena modificação pode
resultar na quebra das funcionalidades de mover a câmera, fim de jogo e iniciar a fase. Outro
cheiro desse design é a Viscosidade porque simples modificações são difíceis de manter o
design atual. Isso aumenta as chances de “gambiarras” serem adicionadas ao design na
necessidade de uma modificação. Também apresenta o cheiro de Repetição desnecessária
porque o código de verificação dos inputs iniciais do jogador se repete em outros pontos do
código de outras classes. O design também apresenta o cheiro de Opacidade porque o código
é difícil de entender. A Figura 20 mostra a implementação da classe ControleDaCamera.
Figura 20 ─ Implementação da primeira versão das funcionalidades da câmera e interfaces de
início e fim de jogo na classe ControleDaCâmera
Fonte: Autor.
O primeiro passo foi refatorar o nome da classe ControleDaCamera para
48
CameraController. Depois, o padrão Observer descrito na seção 2.2.3.4 Observer foi
implementado novamente modificando a estrutura da classe PlayerInputs. Agora PlayerInputs
notifica os interessados em saber quando o primeiro input do jogador acontece. Esses
interessados são as classes PlayerControls e CameraController. A decisão dessa modificação e
aplicação desse padrão decorreu da ideia de que existem mais de um interessado no
acontecimento do primeiro input vindo do jogador. Além de que esse padrão diminui o
acoplamento entre classes e módulos e está de acordo com os princípios SOLID. A Figura 21
mostra um diagrama de classe retratando as relações entre CameraController e PlayerInputs.
Primeiro foi necessário realizar as modificações na estrutura que verifica os inputs
vindos do jogador. A Figura 21 mostra essa modificação feita na estrutura. Agora a classe
PlayerControls, que é uma interessada em saber quando o jogador pressiona qualquer botão
do jogo, e para isso ela se inscreve no subject OnPlayerFirstInputEvent implementa a
interface IPlayerFirstInputListener.
Figura 21 ─ Refatoração da estrutura que verifica inputs do jogador para aplicar o padrão
Observer
Fonte: Autor.
49
Em seguida, a classe CameraController foi refatorada para também se inscrever e
implementar no subject OnPlayerFirstInputEvent e IPlayerFirstInputListener. Dessa forma,
assim como retratado na Figura 20, CameraController também é uma classe interessada no
evento de primeiro input do jogador. A Figura 22 mostra a nova implementação da classe
CameraController para aplicar o padrão Observer e estar de acordo com os princípios SOLID.
Dessa forma a Fragilidade diminuiu porque as chances de o código quebrar em outras partes
diminuíram. a Viscosidade também diminuiu porque agora é mais fácil manter o design. A
Repetição desnecessária foi eliminada com a aplicação do padrão Observer e a opacidade
diminuiu, pois, a classe está mais fácil de entender.
Figura 22 ─ Implementação da classe CameraController que é uma classe interessada em
saber sobre o evento de primeiro input do jogador
Fonte: Autor.
50
Por fim, é necessário que o fim de jogo aconteça quando o personagem sai do
enquadramento da câmera. Para isso, a classe PlayerLife verifica quando o personagem sai do
enquadramento da câmera e realiza o fim de jogo. A Figura 23 mostra a implementação dessa
classe.
Figura 23 ─ Implementação da classe PlayerLife que exibe a interface de fim de jogo quando
o personagem sai do enquadramento da câmera.
Fonte: Autor.
5.1.5 Penalidade de colisão com objetos da cena, bloqueio dos controles do personagem e
parar a câmera depois do fim de jogo
Quando o personagem colide com algum obstáculo da cena, o jogador é penalizado com os
movimentos do personagem bloqueados por um curto período enquanto o personagem é
lentamente jogado para esquerda. A Figura 24 mostra a implementação da classe Obstaculo na
primeira versão.
51
O primeiro problema da implementação na Figura 24, é que existe um trecho de
código que está comentado sem explicação nenhuma. No momento da escrita deste trabalho,
não fica claro o porquê de o trecho comentado permanecer na classe. Outro problema é que
existe uma chamada através de mensagens que invoca um método na classe
ControleDoPersonagem. Esse tipo de chamada é suscetível a erros de escrita e é um problema
que pode se tornar difícil de identificar. A responsabilidade da aplicação da penalidade de
colisão está distribuída de forma que não é clara entre as classes Obstaculo e
ControleDoPersonagem e o design cheira a Opacidade.
Figura 24 ─ Implementação da classe Obstaculo na primeira versão
Fonte: Autor.
Essa mudança gerou modificações na classe PlayerControls em que os controles do
personagem devem ser passíveis de bloquei e desbloqueio para que a penalidade de colisão
seja aplicada de forma correta. A classe responsável por verificar e aplicar essas colisões se
chama Obstaculo na primeira versão. O primeiro passo dessa refatoração foi renomear essa
classe para PlayerCollisionDetectionAndPenality. A Figura 25 mostra um trecho de como a
penalidade é aplicada quando uma colisão com um obstáculo acontece.
52
Figura 25 ─ Trecho que mostra parte das modificações na classe
PlayerCollisionDetectionAndPenality
Fonte: Autor.
O segundo passo foi a refatoração da classe CameraController para que a câmera pare
quando o personagem “morre”. Para implementar essa mudança, o padrão Observer da seção
2.2.3.4 Observer foi aplicado novamente. A classe OnPlayerDiedEvent e a interface
IPlayerDeathListener foram criados. Agora a classe se inscreve em uma instância de
OnPlayerDiedEvent e implementa o método IPlayerDeathListener para ser notificada e parar
a câmera assim que o personagem sai do enquadramento da câmera. A Figura 26 mostra um
diagrama com essa estrutura.
53
Figura 26 ─ Diagrama de classes da estrutura que notifica quando o personagem “morre” para
os ouvintes CameraController e PlayerControls
Fonte: Autor.
6 COMPARAÇÃO ENTRE A PEIMEIRA E ÚLTIMA VERSÃO
Como forma de verificar as diferenças entre a versão inicial e a final, a ferramenta NDepend
foi usada para realizar uma análise estática de código dos branches v1 e v2. Foram
comparadas as medidas de Linhas de Código (LOC), Complexidade Ciclomática (CC) e
Acoplamento. A LOC mede a quantidade de linhas de código de cada classe. A CC mede a
quantidade de caminhos de execução possíveis no código de uma classe. E o Acoplamento
mede a quantidade de dependências que uma classe usa em relação a quantidade de classes
externos que usam essa classe. A Tabela 28 mostra a contagem de linhas de cada classe na
primeira versão do projeto. A Tabela 29 mostra a mesma medida em relação a versão final.
Tabela 2 ─ Linhas de código (LOC) da primeira versão.
Nome da classe(tipo) LOC
ControleDoPersonage
m
6
2
ControleDeTempo 1
5
ControleDaCamera 1
54
4
ControleDeScore 1
4
VidaDoPersonagem 9
ControleDeEfeitos 8
FimDaFase 8
Obstaculo 7
Item 6
Fonte: Autor.
A comparação entre as medidas de linhas das duas versões, mostra que a média de
linhas de código por classe mudou de 15,888 na primeira versão para 12,117 na versão final.
Mas o dado mais importante é em comparação a classe ControleDoPersonagem na primeira
versão que tinha 62 linhas e era a maior classe. Em comparação com a maior classe da versão
final, a classe ItemdashCountUI tem 37 linhas. Um valor que chega a ser próximo da metade
da maior classe da primeira versão.
Tabela 3 ─ Linhas de código (LOC) da versão final.
Nome da classe(tipo) LOC
ItemDashCountUI 37
PLayerCOllisionDetectionAndPenality 25
PlayerControls 22
CameraController 18
DashAction 17
JumpAction 17
PlayerInputs 17
DashCounter 10
OnItemCollectedEvent 7
MoveAction 7
OnPlayerFirstinputEvent 7
OnPlayerBecameInvisibleEvent 7
ScoreCounter 6
55
Item 4
PlayerLife 3
ActionBase 1
ScoreUI 1
Fonte: Autor.
Outra medida interessante é a comparação da Complexidade Ciclomática de cada
versão. A Tabela 4 mostra a medida de cada classe na primeira versão, enquanto a Tabela 5
mostra a mesma medida para as classes da versão final.
Tabela 4 ─ Complexidade Ciclomática (CC) das classes da primeira versão.
Nome da classe(tipo) Complexidade Ciclomática (CC)
ControleDoPersonagem 23
ControleDeTempo 14
ControleDaCamera 13
ControleDeScore 8
VidaDoPersonagem 6
ControleDeEfeitos 5
Obstaculo 3
Item 2
Fonte: Autor.
Essa medida mostra que na primeira versão a maior complexidade estava concentrada
na classe ControleDoPersonagem. Isso mostra que havia muitos possíveis caminhos de
execução dentro do código dessa classe, o que significa que era mais difícil mantê-la e testá-la.
O cenário muda bastante na segunda versão e as classes com maior complexidade são
PlayerControls e CameraController que são as duas classes que concentram as
funcionalidades mais importantes do jogo. Cada uma apresenta a medida 15
Tabela 5 ─ Complexidade Ciclomática (CC) das classes da versão final.
Nome da classe(tipo) Complexidade Ciclomática (CC)
PlayerControls 15
56
CameraController 15
PlayerInputs 14
JumpAction 12
PlayerCollisionDetectionAndPenality 9
DashCounter 8
DashItemCountUI 7
DashAction 6
OnItemCollectedEvent 5
ScoreCounter 5
OnPlayerBecameInvisibleEvent 5
OnPlayerFIrstInputEvent 5
MoveAction 4
Item 3
PlayerLife 2
ScoreUI 1
Fonte: Autor.
A medida de Acoplamento mede o acoplamento entre as classes destacando as
dependências que uma classe tem em relação as classes que dependem dela. Infelizmente, por
conta de como a Unity funciona, a medida apresentou falsos positivos. Isso acontece porque a
Unity não incentiva o uso de interfaces como forma de abstração. Então, por mais que o
processo de refatoração tenha definido interfaces e usado de DIP para inverter as
dependências, menções de tipos concretos ainda ocorreram e prejudicaram a medida.
A medida LOC mostra que a distribuição da quantidade de linhas das classes da versão
final ficou mais bem distribuída, mesmo que a quantidade de classes tenha aumentado. Em
relação a medida CC, a complexidade Ciclomática ficou mais bem distribuída entre as classes
em relação a primeira versão, mesmo com a quantidade de classes tendo aumentado também.
7 CONCLUSÃO
O mercado de jogos vem crescendo cada vez mais e os projetos precisam estar prontos para
mudanças rápidas. Para estarem prontos para essas mudanças, os projetos precisam usar do
melhor da Engenharia de Software com processos que integrem de forma eficaz profissionais
57
de áreas como som, programação, cinema, música, arte, animação etc e usar ferramentas para
agilizar o desenvolvimento e técnicas e conhecimentos de programação para criar código bem
estruturado pronto para responder as mudanças.
Por conta disso, bons profissionais precisam conhecer o suficiente dessas ferramentas
e técnicas. Em se tratando de desenvolvedores, não é diferente. Conhecimentos com
princípios SOLID e padrões de projeto são essenciais para desenvolver código com um bom
design.
Este trabalho introduz e mostra um uso prático para os desenvolvedores interessados
em aprender mais sobre refatoração, dos princípios SOLID e padrões de projeto refatorando e
melhorando o design do código do jogo Bicho UFC Rampage. Essa refatoração melhorou o
código desse projeto diminuindo o acoplamento, tornado os módulos mais independentes e
distribuindo melhor responsabilidades. Dessa forma o projeto se tornou mais adaptável e
manutenível.
Durante a execução dos passos de refatoração, principalmente nos passos finais, foi
perceptível que as refatorações feitas foram mais simples de serem feitas e geraram poucos
impactos em outros módulos. Isso mostra que realmente houve uma melhorar do código. O
próprio uso dos princípios SOLID induziram certas partes do projeto a aplicar padrões de
projeto, além de facilitar as modificações que aconteceram. O uso do padrão Observer fez
com que módulos diferentes que precisavam saber de eventos de outros módulos, se
comunicassem sem gerar dependências fortes. O uso do padrão Strategy na implementação da
classe responsável por verificar os inputs do jogador, abriu espaço para implementações para
outras plataformas apenas com a extensão sem modificar código antigo. A aplicação do
Template Method ajudou no reaproveitamento de código das implementações das ações do
jogador.
A análise estática de código usando o NDepend para comparar a versão inicial e final
também destacam melhorias. Com a medida LOC, foi perceptível que a quantidade de linhas
de código de cada classe ficou mais bem distribuída mesmo com a quantidade de classes
tendo aumentado. E com a medida CC, a medida mostrou também uma melhor distribuição da
Complexidade Ciclomática nas classes mesmo com a quantidade de classes tendo aumentado.
Infelizmente, a medida de Acoplamento não pode ser usada por conta de indicar medidas
imprecisas devido as características da própria Unity.
Como possíveis trabalhos futuros, poderia ser feita uma análise estática com o
NDepend ou outra ferramenta para identificar code smells e realizar etapas de refatoração para
eliminar esses code smells. Outro trabalho poderia usar de um experimento AB com grupos
58
usando princípios SOLID e padrões em comparação com outro grupo desenvolvendo sem
usar nenhum dos dois para verificar pontos fortes em relação a comunicação do time,
velocidade de desenvolvimento, quantidade de bugs e linhas de código. E um trabalho onde
fossem feitas seções de audição do código do projeto por profissionais mais experientes para
verificar pontos que precisam melhorar.
59
REFERÊNCIAS
FIGUEIREDO, Roberto Tenório; RAMALHO, Geber Lisboa. GOF design patterns applied
to the development of digital games. Proceedings of SBGames. 2015. E-book. Disponível
em: http://www.sbgames.org/sbgames2015/anaispdf/computacao-full/146712.pdf. Acesso em:
26 out. 2020.
PARVIAINEN, Niko. Dependency Injection in Unity3D. 2017. E-book. Disponível em:
https://www.theseus.fi/bitstream/handle/10024/125683/Parviainen_Niko.pdf?sequence=1.
Acesso em: 26 out. 2020.
SOMMERVILLE, I. Engenharia de Software. 9. ed. Londres: Pearson, 2012.
MARTIN, R. C.; MARTIN, M. Agile principles, patterns, and practices in C#. 1. ed. Nova
Jersey: Prentice Hall PTR, 2006.
MARTIN, R. C. Arquitetura Limpa: O Guia do Artesão para Estrutura e Design de Software.
1. ed. Rio de Janeiro: Alta Books Editora, 2019.
GAMMA, E. et al. Design Patterns: Elements of Reusable Object-Oriented Software. 1. ed.
Boston: Addison-Wesley Professiona, 1994.
GUERRA, E. Design Patterns com Java: Projeto orientado a objetos guiado por padrões. 1.
ed. [S. l.]: Editora Casa do Código, 2014.