60
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

2020_tcc_gjtrbento.pdf - Repositório Institucional UFC

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)

A minha família.

Aos grandes amigos que a vida nos dá.

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.