Baixe o app para aproveitar ainda mais
Prévia do material em texto
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 maismodular. 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 paraum 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, existemprincí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ácilde 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çosnã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çãodo 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ídoem 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
Compartilhar