Prévia do material em texto
Autor: Prof. Tarcísio de Souza Peres Colaboradores: Prof. Angel Antonio Gonzalez Martinez Profa. Larissa Rodrigues Damiani Programação Orientada a Objetos II Professor conteudista: Tarcísio de Souza Peres É formado em Engenharia da Computação e mestre pela Unicamp. Atuou como pesquisador no Human Cancer Genome Project, onde aplicou algoritmos e programação orientada a objetos. É MBA em Gestão de TI pela Fiap, formado em Gestão de Projetos pela FIA/USP e cursou Gestão Estratégica e Competitividade pela Fundação Dom Cabral. Com perfil multidisciplinar, tem experiência em tecnologia, saúde, governança, novos negócios e inovação. Trabalhou em multinacionais e órgãos públicos. É certificado pelo PMI, Cobit e autor de livro sobre mercado financeiro. Atua como empreendedor (startups de inovação em tecnologia), professor na UNIP, cFGV e no Centro Paula Souza. © Todos os direitos reservados. Nenhuma parte desta obra pode ser reproduzida ou transmitida por qualquer forma e/ou quaisquer meios (eletrônico, incluindo fotocópia e gravação) ou arquivada em qualquer sistema ou banco de dados sem permissão escrita da Universidade Paulista. Dados Internacionais de Catalogação na Publicação (CIP) U520.13 – 24 Dados Internacionais de Catalogação na Publicação (CIP) P437p Peres, Tarcísio de Souza. Programação Orientada a Objetos II / Tarcísio de Souza Peres. – São Paulo: Editora Sol, 2024. 298 p., il. Nota: este volume está publicado nos Cadernos de Estudos e Pesquisas da UNIP, Série Didática, ISSN 1517-9230. 1. Elementos gráficos. 2. C#. 3. Desenvolvimento. I. Título. CDU 681.3.062 Profa. Sandra Miessa Reitora Profa. Dra. Marilia Ancona Lopez Vice-Reitora de Graduação Profa. Dra. Marina Ancona Lopez Soligo Vice-Reitora de Pós-Graduação e Pesquisa Profa. Dra. Claudia Meucci Andreatini Vice-Reitora de Administração e Finanças Prof. Dr. Paschoal Laercio Armonia Vice-Reitor de Extensão Prof. Fábio Romeu de Carvalho Vice-Reitor de Planejamento Profa. Melânia Dalla Torre Vice-Reitora das Unidades Universitárias Profa. Silvia Gomes Miessa Vice-Reitora de Recursos Humanos e de Pessoal Profa. Laura Ancona Lee Vice-Reitora de Relações Internacionais Prof. Marcus Vinícius Mathias Vice-Reitor de Assuntos da Comunidade Universitária UNIP EaD Profa. Elisabete Brihy Profa. M. Isabel Cristina Satie Yoshida Tonetto Prof. M. Ivan Daliberto Frugoli Prof. Dr. Luiz Felipe Scabar Material Didático Comissão editorial: Profa. Dra. Christiane Mazur Doi Profa. Dra. Ronilda Ribeiro Apoio: Profa. Cláudia Regina Baptista Profa. M. Deise Alcantara Carreiro Profa. Ana Paula Tôrres de Novaes Menezes Projeto gráfico: Revisão: Prof. Alexandre Ponzetto Caio Ramalho Vitor Andrade Programação Orientada a Objetos II APRESENTAÇÃO ......................................................................................................................................................7 INTRODUÇÃO ...........................................................................................................................................................8 Unidade I 1 REVISÃO DE CONCEITOS DE POO E INTRODUÇÃO AO AMBIENTE C# ........................................ 11 1.1 Revisando conceitos de POO: exercícios práticos (console) ............................................... 12 1.1.1 Considerações sobre o código-fonte .............................................................................................. 69 1.2 Apresentação geral dos recursos e elementos de interface no ambiente de programação C# .................................................................................................................................... 80 1.3 Ambiente de desenvolvimento integrado (IDE) ....................................................................... 83 1.4 Introdução ao .NET Framework e .NET Core .............................................................................. 95 2 DESENVOLVIMENTO DA PRIMEIRA INTERFACE GRÁFICA .............................................................100 2.1 Windows Forms ...................................................................................................................................101 2.2 Formulários e caixas de mensagem ............................................................................................103 2.3 Elementos gráficos básicos: rótulos, caixas de texto e botões ........................................109 2.4 Elementos gráficos de apoio: painéis, grupos, rótulos com vínculo e dicas de ferramenta ..............................................................................................................................................111 Unidade II 3 ELEMENTOS GRÁFICOS DE ESCOLHA E ORGANIZAÇÃO ................................................................122 3.1 Elementos gráficos de escolha: caixas de opção, de seleção e de combinação .......122 3.2 Elementos gráficos de escolha: calendário mensal e seletor de data ..........................125 3.3 Elementos gráficos de organização: menus e caixa de listagens ...................................128 3.4 Elementos gráficos de organização: controle de abas, visualização em árvore e em lista .........................................................................................................................................131 4 MANIPULAÇÃO DE EVENTOS E JANELA MDI ......................................................................................137 4.1 Manipulando eventos de mouse e teclado ..............................................................................138 4.2 Interface de documentos múltiplos (multiple document interface – MDI) ...............142 4.3 Projeto básico: juntando todas as partes com interface humano-computador (IHC) e user experience (UX) ....................................................................146 Unidade III 5 ARQUITETURA DE SOFTWARE E PADRÕES DE DESIGN ..................................................................162 5.1 Desenvolvimento em camadas e MVC ......................................................................................163 5.2 Entendendo os componentes do Windows Presentation Foundation (WPF) ............165 Sumário 5.3 MVVM e sua integração com XAML ...........................................................................................167 5.4 Aprimoramento: projeto de interface desktop com XAML ...............................................168 6 ACESSO A DADOS, ADO.NET E LINQ .......................................................................................................205 6.1 String de conexão: SQLClient e comentários sobre outros provedores de dados do .NET Framework (OLE DB e OBDC) ............................................................................206 6.2 Conexões locais e servidores remotos no contexto da computação em nuvem .....................................................................................................................................................219 6.3 Arquitetura do ADO.NET: DataSet, DataTable, DataView, DataRow e DataColumn .............................................................................................................................................224 6.4 LINQ e ADO.NET ...................................................................................................................................229 6.5 Projeto intermediário: interface desktop com acesso aos dados do banco ...............230 Unidade IV 7 TÉCNICAS AVANÇADAS E PRÁTICAS SEGURAS EM C# .................................................................236 7.1 Técnicas de programação assíncrona em C# (async e await) ..........................................236 7.2 Princípios e práticas de domain-driven design (DDD) ........................................................241 7.2.1 Contexto delimitado 1: gestão de produtos ............................................................................. 245 7.2.2 Contexto delimitado 2: processamentoconsiderando que cada carta encapsula sua própria lógica de efeito; isso facilita a leitura do código, pois a lógica relacionada a um efeito específico da carta está contida dentro dela. Terceiro, simplifica o processo, uma vez que se evita criar uma grande estrutura condicional e se dispensa herança ou polimorfismo para lidar com diferentes tipos de efeito de carta. Para finalizar, facilita a POO, na qual os efeitos das cartas podem ser vistos como respostas a eventos (neste caso, o evento de uma carta ser jogada). Na prática isso significa que, ao criar uma nova CartaDeEvento, você pode fornecer diferentes lógicas de efeito simplesmente passando diferentes métodos para o construtor. Por exemplo, uma carta pode ter um efeito que aumenta a fortuna do jogador, enquanto outra pode pausar o jogador no próximo turno – este é um exemplo de polimorfismo, porque diferentes implementações de ação podem ser usadas de forma intercambiável. Veremos em breve a classe Baralho, que contém uma coleção de CartaDeEvento, mecanismo pelo qual os eventos são introduzidos no jogo. Ao chamar SortearCarta na classe Baralho, uma carta é selecionada aleatoriamente, e seu efeito é aplicado ao jogador relevante. Esse aspecto aleatório é fundamental para criar uma experiência de jogo dinâmica e imprevisível, na qual os participantes devem constantemente adaptar suas estratégias a eventos inesperados. Do ponto de vista técnico, a abordagem usada para implementar a classe CartaDeEvento é elegante e eficiente. Na programação C#, um código considerado elegante é o que harmoniza vários aspectos importantes do desenvolvimento de software. Legibilidade é essencial: um código elegante é fácil de ler e entender, o que se consegue através de uma boa estrutura, formatação adequada e nomes significativos para variáveis e métodos. A clareza na expressão das intenções do programador facilita tanto o trabalho em equipe quanto a manutenção futura do código. Manutenibilidade é outro aspecto importante: código elegante é o que pode ser facilmente modificado e estendido. Isso é frequentemente alcançado adotando bons princípios de design (como os princípios SOLID) e boa modularização do código, facilitando a atualização e a adição de novas funcionalidades, além de tornar o código mais compreensível para outros desenvolvedores. Utilizando delegados e expressões lambda, dispensa-se uma hierarquia extensa de classes ou um switch-case longo para lidar com os diferentes efeitos das cartas. Além disso, essa abordagem torna o código mais legível e fácil de manter, já que a lógica de cada carta está contida dentro de sua própria instância, em vez de espalhada em várias partes do código. 35 PROGRAMAÇÃO ORIENTADA A OBJETOS II A figura 11 ilustra como a simplicidade da CartaDeEvento se manifesta na interação da classe Baralho. 1. class Baralho 2. { 3. private List cartas; 4. private Random random; 5. 6. public Baralho(Random random) 7. { 8. this.random = random; 9. cartas = new List() { 10. new CartaDeEvento(“Desafio do Cardeal: Você foi pego em uma emboscada pelos guardas de Richelieu. Perca uma rodada enquanto escapa.”, jogador => jogador.PausarProximoTurno()), 11. new CartaDeEvento(“O Olhar de Constance: Constance Bonacieux, por quem você se apaixona, lhe deu um olhar encorajador. Avance duas casas com a alegria do amor.”, jogador => jogador.Posicao += 2), 12. new CartaDeEvento(“Intriga da Rainha: A rainha Ana da Áustria confiou a você uma missão secreta para recuperar diamantes. Avance três casas e ganhe uma ficha de Honra.”, jogador => { jogador.Posicao += 3; jogador.FichasDeHonra += 1; }), 13. new CartaDeEvento(“Duelo com um Mosqueteiro: Um desentendimento levou a um duelo com um de seus camaradas. Volte uma casa e perca uma ficha de Honra.”, jogador => { jogador.Posicao = Math.Max(1, jogador.Posicao – 1); jogador.FichasDeHonra = Math.Max(0, jogador.FichasDeHonra – 1); }), 14. new CartaDeEvento(“Trama de Milady: A sedutora e astuta Milady de Winter tramou contra você. Volte duas casas enquanto desvenda sua trama.”, jogador => jogador.Posicao = Math.Max(1, jogador.Posicao - 2)), 15. new CartaDeEvento(“Auxílio de Buckingham: O Duque de Buckingham, aliado da rainha, oferece ajuda em sua jornada. Avance duas casas com o apoio dele.”, jogador => jogador.Posicao += 2), 16. new CartaDeEvento(“Festa em Paris: Uma festa extravagante é realizada em Paris, e você é o convidado de honra. Perca uma rodada enquanto celebra.”, jogador => jogador.PausarProximoTurno()), 17. new CartaDeEvento(“Missão Secreta: Uma carta selada da rainha lhe é entregue, pedindo uma missão sigilosa. Avance três casas com sua nova responsabilidade.”, jogador => jogador.Posicao += 3), 18. new CartaDeEvento(“Emboscada na Estrada: Enquanto viaja, você é emboscado por inimigos desconhecidos. Volte duas casas enquanto escapa do perigo.”, jogador => jogador.Posicao = Math.Max(1, jogador.Posicao – 2)), 36 Unidade I 19. new CartaDeEvento(“Defesa do Rei: O Rei Luís XIII está sob ameaça e você o defendeu bravamente. Avance duas casas e ganhe duas fichas de Honra.”, jogador => { jogador.Posicao += 2; jogador.FichasDeHonra += 2; }), 20. new CartaDeEvento(“Convite do Cardeal: Richelieu, em um movimento inesperado, convida você para um banquete. Perca uma rodada devido à cautela, mas ganhe uma ficha de Fortuna pelas oportunidades que surgem.”, jogador => { jogador.PausarProximoTurno(); jogador.FichasDeFortuna += 1; }), 21. new CartaDeEvento(“Refúgio em um Mosteiro: Escapando de perseguidores, você se refugia em um mosteiro. Perca uma rodada, mas ganhe uma ficha de Honra pela proteção oferecida.”, jogador => { jogador.PausarProximoTurno(); jogador.FichasDeHonra += 1; }), 22. new CartaDeEvento(“Treinamento Intensivo: Você treina duro com seus companheiros mosqueteiros. Avance uma casa e ganhe uma ficha de Fortuna.”, jogador => { jogador.Posicao += 1; jogador.FichasDeFortuna += 1; }), 23. new CartaDeEvento(“Intriga na Corte: Rumores sobre você circulam na corte. Volte uma casa, mas ganhe uma ficha de Fortuna pelo aprendizado com a situação.”, jogador => { jogador.Posicao = Math.Max(1, jogador.Posicao – 1); jogador.FichasDeFortuna += 1; }), 24. new CartaDeEvento(“Mensagem de Socorro: Constance lhe envia uma mensagem urgente. Avance duas casas enquanto se apressa em ajudá-la e ganhe uma ficha de Honra.”, jogador => { jogador.Posicao += 2; jogador.FichasDeHonra += 1; }), 25. new CartaDeEvento(“Dádiva do Duque: O Duque de Buckingham presenteia você com ricas vestimentas. Ganhe duas fichas de Fortuna.”, jogador => jogador.FichasDeFortuna += 2), 26. new CartaDeEvento(“Perseguição nas Ruas: Envolvido em uma perseguição pelas ruas de Paris, você escapa por pouco. Perca uma ficha de de Honra, mas ganhe uma ficha de Fortuna pela experiência.”, jogador => { jogador.FichasDeHonra = Math.Max(0, jogador.FichasDeHonra – 1); jogador.FichasDeFortuna += 1; }), 27. new CartaDeEvento(“Rivalidade na Guarda: Um rival na Guarda do Rei desafia você. Volte duas casas enquanto lida com a situação, mas ganhe uma ficha de Fortuna pelo aprendizado.”, jogador => { jogador.Posicao = Math.Max(1, jogador.Posicao – 2); jogador.FichasDeFortuna += 1; }), 28. new CartaDeEvento(“Conspiração Desvendada: Você descobre uma conspiração contra os mosqueteiros. Avance duas casas e ganhe uma ficha de Honra.”, jogador => { jogador.Posicao += 2; jogador.FichasDeHonra += 1; }), 29. new CartaDeEvento(“Perdido em uma Floresta: Durante uma missão, você se perde em uma floresta densa. Volte duas casas enquanto encontra seu caminho.”, jogador => jogador.Posicao = Math.Max(1, jogador.Posicao – 2)), 37 PROGRAMAÇÃO ORIENTADA A OBJETOS II 30. new CartaDeEvento(“Presente da Rainha: A Rainha Ana da Áustria lhe dá umpresente por sua lealdade. Ganhe três fichas de Fortuna.”, jogador => jogador.FichasDeFortuna += 3) 31. }; 32. } 33. 34. public CartaDeEvento SortearCarta() 35. { 36. int indice = random.Next(cartas.Count); 37. return cartas[indice]; 38. } 39. } Figura 11 – Classe Baralho A classe Baralho é fundamental pois adiciona elementos de aleatoriedade e variedade ao gameplay. Essa classe encapsula a lógica e a gestão das CartaDeEvento, cada uma representando um evento específico que afeta os jogadores de maneiras diversas. Ao explorar a estrutura e o funcionamento dessa classe, fica claro como ela contribui significativamente para a dinâmica do jogo. Na linha 3 vemos a lista genérica List, escolha que não apenas oferece segurança de tipo – assegurando que apenas objetos do tipo CartaDeEvento sejam adicionados à lista –, mas também uma série de métodos úteis para manipular a coleção de cartas. Diferentes de arrays (de tamanho fixo), as listas são dinâmicas e podem crescer ou diminuir em tempo de execução, proporcionando grande flexibilidade e permitindo adicionar ou remover elementos da lista sem se preocupar com gerenciamento do tamanho da coleção. Isso oferece uma maneira prática de gerenciar as cartas do baralho, pois podemos facilmente adicionar novas cartas, removê-las ou percorrer a lista para encontrar uma carta específica. Por exemplo, o método SortearCarta() da linha 34 combina a geração de um índice aleatório, fornecido pela classe Random, com o acesso indexado oferecido pela lista. Essa abordagem permite selecionar um elemento aleatório da lista de maneira eficiente e direta, aproveitando as vantagens de ambas as classes (o que seria mais complicado com outras estruturas de dados). Essa característica fundamental da lista a diferencia de outras coleções que não permitem acesso direto por índice, como HashSet ou LinkedList. Para a dinâmica do jogo, o método SortearCarta() é particularmente interessante: utilizando o objeto Random, ele seleciona uma carta aleatória da lista, e isso não só garante que o jogo permaneça imprevisível e emocionante, mas também demonstra uma aplicação prática do conceito de randomização em programação. 38 Unidade I No construtor da classe Baralho (linha 6), observamos a injeção de uma dependência, um objeto Random – escolha de design inteligente, pois permite que o baralho controle sua própria fonte de aleatoriedade, característica essencial em jogos de tabuleiro. Injeção de dependência é um conceito utilizado na POO e se refere ao processo de fornecer a um objeto as dependências externas de que ele precisa. Em outras palavras, em vez de um objeto criar suas próprias dependências ou utilizar instâncias globais, as dependências são passadas para ele, normalmente através do construtor ou de métodos específicos. Esse conceito é vital para criar um código mais modular, testável e fácil de manter. No contexto do jogo, injeção de uma dependência refere-se a como a classe Baralho recebe um objeto Random através do seu construtor. Isso significa que o Baralho não gera nem controla sua própria instância de Random, mas utiliza uma fornecida de fora. Essa abordagem tem várias vantagens significativas: primeiro, aumenta a flexibilidade do código. Ao injetar a dependência, podemos facilmente substituir a instância de Random por outra, talvez uma versão mock ou stub para fins de teste, sem alterar o código interno da classe Baralho. Isso torna o código mais adaptável e fácil de testar. Observação Mock e stub são termos usados no contexto de testes de software, especialmente em testes automatizados e no desenvolvimento orientado a testes (test-driven development – TDD), e se referem a objetos que simulam o comportamento de objetos reais de maneira controlada, usados principalmente para isolar o código testado de suas dependências externas, permitindo que desenvolvedores verifiquem a assertividade do código de maneira mais eficiente e isolada. Mock é a imitação de um objeto real usada para simular o comportamento de uma dependência externa num teste. Seu propósito principal é verificar interações entre o objeto sob teste e sua dependência. Stub, por outro lado, é mais simples que um mock; também imita um objeto real, mas apenas fornece respostas predefinidas para chamadas de métodos, independentemente da entrada. A principal diferença entre mocks e stubs são suas intenções e capacidades. Mocks são mais sofisticados e usados para afirmar como o código interage com suas dependências (por exemplo, verificar se um método específico foi chamado). Já as stubs são usadas sobretudo para fornecer respostas predeterminadas, sem qualquer verificação adicional sobre como são usadas pelo código sob teste. Além disso, injeção de dependência ajuda a reduzir o acoplamento entre classes. No jogo, a classe Baralho não precisa saber como criar ou obter uma instância de Random; ela apenas precisa saber que pode usá-la – isso separa as preocupações entre diferentes partes do código, tornando-o mais 39 PROGRAMAÇÃO ORIENTADA A OBJETOS II organizado e manutenível. Por fim, ao utilizar a injeção de dependência, podemos ter um controle mais preciso sobre o comportamento do programa. Por exemplo, ao compartilhar uma única instância de Random entre diferentes partes do jogo, garantimos uma melhor consistência na geração de números aleatórios. Isso pode ser importante em jogos, nos quais a randomização precisa ser justa e previsível em certa medida. A inicialização da lista cartas dentro do construtor – no qual cada carta é criada com uma descrição e um efeito específico (conforme o quadro 4) – é exemplo claro do princípio da responsabilidade única (single responsibility). Cada CartaDeEvento é uma entidade autônoma com um propósito claro: modificar o estado do jogo de alguma forma quando sorteada. Em termos de encapsulamento, a classe Baralho exemplifica bem o conceito. Ela esconde seus detalhes internos, como a lista de cartas e a instância de Random, expondo apenas o que é necessário para outras partes do jogo interagirem com ela – no caso, o método SortearCarta(). Esse nível de abstração é vital em POO, pois permite que partes do código sejam modificadas independentemente, sem afetar outras partes. Dentro do construtor da classe Baralho, diversas cartas de eventos são criadas e adicionadas à lista cartas. Cada CartaDeEvento é inicializada com uma descrição e um efeito, que é onde as expressões lambda e Action entram em jogo. Usar Action para definir os efeitos das cartas é uma decisão de design sofisticada, como descrevemos ao analisar a classe CartaDeEvento. Ao usá-lo, a classe Baralho ganha flexibilidade e poder, pois pode atribuir diferentes ações a diferentes cartas de forma muito limpa e organizada. Essa técnica é um ótimo exemplo de como os delegados podem ser usados para criar código mais modular e reutilizável. Uma expressão lambda em C# é basicamente uma função anônima, ou seja, uma função sem nome que pode ser definida no local onde é utilizada. Na figura 11 cada expressão lambda define o efeito de uma carta de evento quando for sorteada e aplicada a um jogador. Por exemplo, uma das cartas tem o efeito de fazer o jogador avançar duas casas, o que é expresso pela lambda jogador => jogador.Posicao += 2. Aqui, jogador é o parâmetro da lambda, representando o jogador que será afetado pelo efeito da carta. A expressão jogador.Posicao += 2 é o corpo da lambda e descreve a ação de mover o jogador duas casas para frente no tabuleiro. Essas expressões lambda são associadas a um Action, um tipo de delegado fornecido pelo C#. Lembrete Delegado Action é um tipo de referência que pode encapsular um método que não retorna um valor e que aceita parâmetros do tipo especificado – no caso, um Jogador. Ao criar cada CartaDeEvento, associamos uma dessas expressões lambda ao delegado Efeito, que é do tipo Action. Quando uma carta é sorteadadurante o jogo e seu efeito é invocado, executa-se essa expressão lambda associada. 40 Unidade I No código da figura 11, as linhas de 10 a 30 contêm expressões específicas, conforme a necessidade do jogo. As expressões lambda e Action conferem ao código uma grande flexibilidade: se no futuro decidirmos adicionar novas cartas ao jogo com efeitos diferentes, podemos simplesmente criar novas expressões lambda para esses efeitos e associá-las a novas instâncias de CartaDeEvento. Outro aspecto relevante para nossa revisão é a função Math.Max, exemplo do uso prático e eficiente de uma das bibliotecas fundamentais em C#: a System.Math. É uma coleção de métodos estáticos que fornecem funcionalidades matemáticas essenciais, incluindo operações básicas – máximo, mínimo, raiz quadrada, além de constantes matemáticas, como PI. O uso de Math.Max na classe Baralho (linhas 13, 14, 18, 23 e 29) visa garantir que o valor de uma expressão não fique abaixo de determinado limite. No contexto do jogo, isso é crucial para manter sua lógica e regras intactas. Por exemplo: ao calcular a nova posição de um jogador após ele tirar uma carta que o fez recuar no tabuleiro, Math.Max assegura que a nova posição não seja menor que o valor que representa a posição inicial no tabuleiro. Isso evita que a posição se torne negativa, o que não faria sentido nesse contexto e poderia causar erros ou comportamento inesperado. Esse uso de Math.Max é um excelente exemplo de como funções matemáticas podem ser aplicadas em contextos além de simples cálculos, servindo como ferramentas para impor regras lógicas e garantir que os dados do programa permaneçam válidos e consistentes. Além disso, a biblioteca System.Math é parte integral do .NET Framework e disponibiliza uma gama de funções matemáticas essenciais para diversos tipos de aplicativo, desde jogos (como neste caso) até aplicações científicas e financeiras. A vantagem de bibliotecas-padrão como essa é que elas são otimizadas, bem testadas e confiáveis, o que reduz a chance de erros e inconsistências se comparadas a funções implementadas manualmente. O método jogador.PausarProximoTurno() (linha 20) é uma representação clara e eficiente de como as regras e mecânicas do jogo são integradas na programação. Pertencente à classe Jogador, encapsula a lógica de alterar o estado de um jogador para indicar que ele deve pausar seu próximo turno. Quando um jogador tira uma carta específica do baralho, pode ser que ele precise pular seu próximo turno – comportamento modelado no código através da chamada jogador.PausarProximoTurno. Essa chamada de método é um exemplo de como os efeitos das cartas interagem diretamente com os objetos Jogador, modificando seu estado de acordo com as regras do jogo. É um exemplo da abstração em POO: ações complexas são encapsuladas em métodos que podem ser facilmente chamados e gerenciados. Ao usar PausarProximoTurno, a classe Baralho não precisa conhecer os detalhes internos de como um jogador pausa seu turno; ela simplesmente sabe que, ao chamar o método, o jogador afetado terá seu próximo turno pausado de acordo com as regras. Neste tópico já detalhamos o desenvolvimento das classes Programa, RoletaDeDuelo, Casa, Tabuleiro, CartaDeEvento e Baralho da figura 3. Também criamos duas enumerações: ResultadoDuelo e TipoCasa. Vamos detalhar agora a classe Jogador, já usada em CartaDeEvento. Ela representa um jogador que mantém informações sobre seu estado e fornece métodos para manipular esse estado de acordo com as regras estipuladas. 41 PROGRAMAÇÃO ORIENTADA A OBJETOS II 1. class Jogador 2. { 3. public string Nome { get; set; } 4. public int Posicao { get; set; } 5. public int FichasDeFortuna { get; set; } 6. public int FichasDeHonra { get; set; } 7. public bool DevePausar { get; set; } // Indica se o jogador deve pausar no próximo turno 8. public bool NoReconhecimentoDaCorte { get; set; } = false; 9. public bool Eliminado { get; set; } = false; 10. private Random aleatorio; 11. 12. 13. // Construtor do Jogador 14. public Jogador(string nome, Random random) 15. { 16. aleatorio = random; 17. Nome = nome; 18. Posicao = 0; // Todos começam na posição inicial 19. FichasDeFortuna = 5; // Valor inicial de fichas de fortuna 20. FichasDeHonra = 5; // Valor inicial de fichas de honra 21. } 22. 23. // Método para pausar o próximo turno do jogador 24. public void PausarProximoTurno() 25. { 26. DevePausar = true; 27. } 28. 29. // Método para atualizar o estado do jogador a cada turno 30. public void AtualizarTurno(int numeroTotalDeCasas) 31. { 42 Unidade I 32. if (DevePausar) 33. { 34. Console.WriteLine($”{Nome} está pausando este turno.”); 35. DevePausar = false; 36. return; // Adiciona um retorno aqui para evitar movimento adicional neste turno 37. } 38. 39. int movimento = RolarDado(); 40. Posicao += movimento; 41. 42. // Garante que a posição não ultrapasse o número de casas no tabuleiro 43. if (Posicao > numeroTotalDeCasas) 44. { 45. Posicao = numeroTotalDeCasas; // Ajusta para a última casa do tabuleiro 46. } 47. 48. Console.WriteLine($”{Nome} avançou {movimento} casas até a posição {Posicao}.”); 49. 50. } 51. 52. 53. 54. private int RolarDado() 55. { 56. return aleatorio.Next(1, 7); // Dado de 6 faces 57. } 58. 59. } Figura 12 – Classe Jogador 43 PROGRAMAÇÃO ORIENTADA A OBJETOS II Comecemos pela estrutura da classe Jogador, que tem várias propriedades para armazenar informações essenciais sobre ele, como Nome, Posicao, FichasDeFortuna e FichasDeHonra (linhas 3, 4, 5 e 6, respectivamente), acessíveis publicamente, permitindo que outras partes do programa (como a classe Jogo) interajam com elas. Além disso, existem propriedades booleanas, como DevePausar, NoReconhecimentoDaCorte e Eliminado (linhas 7, 8 e 9, respectivamente), usadas para controlar o fluxo do jogo baseado no estado do jogador. Observe que NoReconhecimentoDaCorte está inicializado como false, uma vez que só será true quando o jogador chegar à casa 60. Similarmente, Eliminado também foi inicializado como false, pois só será true após o jogador chegar à casa 60, escolher a Roleta do Julgamento Real, a roleta ser girada e ele ter o revés de ser eliminado. Esse registro é importante se considerarmos que um eliminado não pode mais participar de duelos nem das demais etapas do jogo. A propriedade Nome é um simples armazenamento de string que identifica o jogador. Posicao é um inteiro que rastreia a posição do jogador no tabuleiro, fundamental para a mecânica do jogo. As FichasDeFortuna e FichasDeHonra são contadores numéricos que podem influenciar o resultado e representam recursos que o jogador acumula durante a jornada. O construtor da classe Jogador inicializa essas propriedades de acordo com as especificações dadas (vide quadro 1), recebe o nome de um dos quatro mosqueteiros e uma instância de Random que pode ser usada para ações que requerem aleatoriedade. A posição inicial do jogador é zero, ou seja, todos os jogadores começam do mesmo ponto no tabuleiro. Um aspecto importante da classe Jogador é como ela gerencia o estado e as regras através de seus métodos. O método PausarProximoTurno (linha 24), por exemplo, define a propriedade DevePausar como verdadeira, indicando que o jogador deve pular seu próximo turno. Isso é um exemplo claro de como a lógica do jogo é incorporada diretamente nos objetos que representam os participantes. O método AtualizarTurno (linha 30) encapsula a lógica do que acontece com um jogador a cada turno (ou rodada). Se ele deve pausar seu turno (DevePausar é verdadeiro), o método redefine essa variável para falso e retorna, indicando o fim do turno do jogador. Caso contrário, o método simula o movimento do jogador no tabuleiro usando a função RolarDado, que é uma chamada ao gerador de números aleatórios para determinar quantas casas ele deve mover. Isso ilustraa integração da classe Jogador com outros componentes, como o dado virtual. Além disso, o método AtualizarTurno verifica se a posição do jogador após o movimento excede o número de casas disponíveis. Se isso acontecer, ajusta sua posição para ser igual ao número máximo de casas, garantindo que ele não ultrapasse o limite do tabuleiro (note que aqui não foi necessário usar exceções, pois arbitrariamente resolvemos a questão). É importante utilizar exceções de forma judiciosa, apenas em situações verdadeiramente excepcionais, uma vez que o processo de lidar com elas pode ser custoso em termos de desempenho. Esse cuidado com as condições de limite é um exemplo de como a classe Jogador mantém a integridade do estado do jogo. 44 Unidade I Nas linhas 34 e 48 a interpolação de strings cria mensagens informativas e personalizadas com base no estado atual do jogador. Trata-se de um recurso de muitas linguagens de programação modernas, incluindo C#, que facilita a inserção de valores de variáveis dentro de strings. Essencialmente, permite a incorporação direta de variáveis e expressões dentro de uma string literal sem precisar de concatenação explícita nem usar métodos de formatação de strings. Isso é feito usando o símbolo $ antes da string e colocando as variáveis ou expressões entre chaves dentro dela (marcadas por { }). Por exemplo: a linha 34 usa interpolação para inserir o valor da propriedade Nome do jogador diretamente na mensagem. Isso torna o código mais legível e direto, pois você pode ver claramente o layout da mensagem final e onde cada variável será inserida. Da mesma forma, a linha 48 combina a interpolação de múltiplas variáveis – Nome, movimento e Posicao – para formar uma mensagem detalhada sobre o movimento dele no tabuleiro. Além desses exemplos, a interpolação de strings poderia ser usada de várias outras maneiras. Por exemplo: se quisermos informar os participantes sobre o número de fichas de fortuna ou honra de um jogador, podemos usar uma linha como Console.WriteLine($“{Nome} tem {FichasDeFortuna} fichas de fortuna e {FichasDeHonra} fichas de honra.”). Isso forneceria uma atualização instantânea e clara sobre o status do jogador em termos de recursos no jogo. Outro detalhe na classe Jogador é a implementação do método RolarDado, que encapsula a ideia de rolar um dado de seis faces – operação comum em jogos de tabuleiro. Ao encapsular essa lógica num método separado, o código fica mais organizado e reutilizável, pois qualquer ação no jogo que requeira rolar um dado pode simplesmente chamar esse método. Finalmente, vamos completar o código-fonte analisando a classe Jogo. Optamos por deixá-la para o final por dois motivos: é uma classe mais extensa (com mais linhas de código) do que as anteriores, e seu entendimento é facilitado agora, considerando que ela se integra com as demais classes e que todas as classes do jogo já foram devidamente esmiuçadas. A classe Jogo serve como espinha dorsal de um tabuleiro virtual; ela encapsula sua lógica central, gerenciando o estado e a interação do jogo entre diferentes componentes. A classe Jogo é responsável por iniciá-lo, executar cada turno e determinar quando ele termina, seguindo regras específicas definidas em seu corpo; é um exemplo de como a orientação a objetos modela um sistema complexo. A classe tem atributos privados, como tabuleiro, jogadores, baralhoDeEventos, aleatorio e roletaDeDuelo, cada um representando um aspecto distinto. Esses atributos são instâncias de outras classes, demonstrando o conceito de composição, onde um objeto é composto de outros objetos. A classe também exemplifica o uso do encapsulamento: detalhes internos do funcionamento do jogo são escondidos do mundo exterior, e a interação com a classe é feita via métodos públicos bem definidos. Isso é evidenciado pelo método IniciarJogo(), que serve como ponto de entrada, e ExecutarJogo(), que contém a lógica principal do seu loop. 45 PROGRAMAÇÃO ORIENTADA A OBJETOS II 1. class Jogo 2. { 3. private Tabuleiro tabuleiro; 4. private List jogadores; 5. private Baralho baralhoDeEventos; 6. private Random aleatorio; 7. private RoletaDeDuelo roletaDeDuelo; 8. // Construtor do Jogo 9. public Jogo(Random random) 10. { 11. aleatorio = random; 12. tabuleiro = new Tabuleiro(60); 13. jogadores = new List() { 14. new Jogador(“D’Artagnan”, random), 15. new Jogador(“Athos”, random), 16. new Jogador(“Porthos”, random), 17. new Jogador(“Aramis”, random) 18. }; 19. baralhoDeEventos = new Baralho(random); 20. roletaDeDuelo = new RoletaDeDuelo(random); 21. } 22. // Demais métodos 23. } Figura 13 – Classe Jogo: primeira parte – atributos e construtor A classe Jogo no código-fonte da figura 13 contém uma série de atributos privados fundamentais para sua funcionalidade e para entender como se aplica a POO, incluindo tabuleiro, jogadores, baralhoDeEventos, aleatorio e roletaDeDuelo. Cada um representa um componente essencial do jogo, e todos juntos formam a estrutura sobre a qual ele é construído e executado: 46 Unidade I • tabuleiro: instância da classe Tabuleiro que representa o espaço físico onde os jogadores movem as peças. Esse uso demonstra a composição em POO, onde um objeto (no caso, Jogo) contém outro objeto (tabuleiro) como parte de sua estrutura. • jogadores: lista de instâncias da classe Jogador. Cada objeto Jogador armazena informações sobre um jogador individual, como nome, posição no tabuleiro e pontuação. A lista genérica (List) exemplifica o uso de coleções em C# para gerenciar múltiplas entidades do mesmo tipo. • baralhoDeEventos: representa o conjunto de cartas de evento, sendo uma instância da classe Baralho, atributo crucial para introduzir elementos de aleatoriedade e eventos dinâmicos no jogo, mostrando como a classe Jogo integra diferentes componentes para poder criar uma experiência diversificada. • aleatorio: objeto da classe Random que gera números aleatórios. Esse atributo é essencial para as mecânicas de aleatoriedade do jogo, como lançamento de dados ou sorteio de cartas, ilustrando como a aleatoriedade é implementada em programas. • roletaDeDuelo: instância da classe RoletaDeDuelo que resolve duelos. Assim como o baralho de eventos, essa classe adiciona mais um elemento de incerteza e estratégia ao jogo. Lembrete O construtor da classe Jogo (linha 11) recebe um objeto Random como parâmetro. Como vimos, essa abordagem exemplifica um importante princípio de design de software conhecido como injeção de dependência. Em vez de a classe Jogo criar sua própria instância de Random, ela recebe uma de fora. Isso aumenta a flexibilidade e a testabilidade da classe, permitindo, por exemplo, que um objeto Random com uma semente específica seja fornecido para testes ou simulações. Dentro do construtor, vários componentes do jogo são inicializados: • O tabuleiro é criado com um número específico de casas (linha 12). • Uma lista de jogadores é inicializada, e cada jogador recebe um nome e o mesmo objeto Random para manter a consistência na geração de números aleatórios em todo o jogo (linhas 15 a 20). • O baralho de eventos e a roleta de duelos são também inicializados com o mesmo objeto Random, garantindo que todos os elementos aleatórios sejam sincronizados (linhas 19 e 20, respectivamente). Essa inicialização demonstra como os construtores em C# configuram um objeto num estado inicial válido, garantindo que todos os atributos necessários sejam configurados antes que o objeto seja usado. 47 PROGRAMAÇÃO ORIENTADA A OBJETOS II Além disso, a passagem do mesmo objeto Random para diferentes componentes do jogo é um exemplo de reutilização de objetos e manutenção da consistência. O código da classe termina na linha 23; na linha 22 inseriremos as demais partes que virão a seguir neste tópico. 1. // Método para rodar o jogo (linha 22 da Figura 13) 2. public void ExecutarJogo() 3. { 4. bool jogoTerminado= false; 5. 6. while (!jogoTerminado) 7. { 8. foreach (Jogador jogador in jogadores) 9. { 10. if (jogador.Eliminado) 11. { 12. continue; // Ignora jogadores eliminados 13. } 14. Console.WriteLine($”\nÉ a vez de {jogador.Nome}. Casa= {jogador.Posicao}, Fortuna= {jogador.FichasDeFortuna}, Honra= {jogador.FichasDeHonra}”); 15. Console.WriteLine(“Pressione qualquer tecla para jogar.”); 16. Console.ReadKey(); // Aguarda o jogador pressionar uma tecla 17. 18. JogarTurno(jogador); 19. 20. // Verifique o fim do jogo após cada turno 21. if (TodosJogadoresNoReconhecimentoOuDesprezo()) 22. { 23. AnunciarVencedor(); 48 Unidade I 24. jogoTerminado = true; 25. break; 26. } 27. } 28. } 29. 30. Console.WriteLine(“Pressione qualquer tecla para sair.”); 31. Console.ReadKey(); // Aguarda o usuário pressionar uma tecla antes de fechar 32. } 33. // Demais métodos Figura 14 – Classe Jogo: segunda parte – método ExecutarJogo O método ExecutarJogo desempenha papel central e encapsula a lógica principal do jogo, ilustrando conceitos fundamentais da programação C#, como loops, condicionais e interação com o usuário, além de revelar o fluxo do jogo e como ele é controlado, sendo executado dentro de um loop while (linha 6), que continua até a condição de término do jogo (jogoTerminado) ser verdadeira. Esse uso de um loop while é típico nos jogos em que uma série de ações se repete a cada rodada até se atingir uma condição de parada. Dentro desse loop há um loop foreach (linha 8) iterando a lista de jogadores. Esse segundo loop é exemplo do uso de estruturas de repetição para processar coleções (no caso, a coleção de jogadores participantes). A cada iteração, o método verifica se o jogador atual foi eliminado (jogador.Eliminado), o que demonstra o uso de condicionais (if) para controlar o fluxo do jogo com base no estado dos objetos (linha 10). Participantes eliminados são ignorados no turno atual (linha 12), mostrando como a lógica do jogo é adaptada para diferentes situações. Em seguida, informações sobre o jogador atual são exibidas usando interpolação de strings (linha 14), e a entrada do usuário é solicitada pelo Console.ReadKey (linha 16) – aspecto que ilustra a interação simples com o usuário, comum em aplicações de console. O método JogarTurno(jogador) é chamado para cada jogador e encapsula a lógica de um único turno (linha 18). Aqui a classe Jogo delega parte de sua responsabilidade para outro método, demonstrando o princípio de responsabilidade única e como um programa pode ser organizado em partes menores e mais gerenciáveis. Após cada turno, o método ExecutarJogo verifica se ele terminou, utilizando TodosJogadoresNoReconhecimentoOuDesprezo. Esse método exemplifica como os estados dos objetos são usados para tomar decisões, integrando estados individuais de todos os participantes para determinar o estado geral do jogo. Finalmente, se o jogo terminar, AnunciarVencedor é chamado e o loop é encerrado, o que mostra como um programa pode sair de um loop e proceder para a próxima etapa do 49 PROGRAMAÇÃO ORIENTADA A OBJETOS II fluxo de execução. O método ExecutarJogo termina solicitando que o usuário pressione qualquer tecla para sair, concluindo a execução. Agora, confira este código: 1. // Método para jogar um turno 2. private void JogarTurno(Jogador jogador) 3. { 4. // Verifica se o jogador está no Reconhecimento da Corte e, se sim, pula o resto do turno 5. if (jogador.NoReconhecimentoDaCorte) 6. { 7. Console.WriteLine($”{jogador.Nome} está no Reconhecimento da Corte e aguarda os demais jogadores.”); 8. return; 9. } 10. 11. if (jogador.DevePausar) 12. { 13. jogador.AtualizarTurno(tabuleiro.NumeroDeCasas); // Isso cuidará da lógica de pausa 14. } 15. else 16. { 17. // Rolar o dado para movimento 18. int movimento = RolarDado(); 19. Console.WriteLine($”{jogador.Nome} rolou {movimento} no dado.”); 20. jogador.Posicao += movimento; 21. 22. if (jogador.Posicao > tabuleiro.NumeroDeCasas) 23. { 24. jogador.Posicao = tabuleiro.NumeroDeCasas; // Garante que não ultrapasse a última casa 25. } 50 Unidade I 26. 27. // Verificar a casa no tabuleiro 28. Casa casaAtual = tabuleiro.ObterCasa(jogador.Posicao); 29. ProcessarCasa(casaAtual, jogador); 30. } 31. } 32. // Demais métodos Figura 15 – Classe Jogo: terceira parte – método JogarTurno Analisaremos agora o método JogarTurno, que dita o fluxo do turno de cada jogador. No início de cada um, primeiro verifica-se se o jogador atual está no estado “reconhecimento da corte” (conforme condição da linha 5 na figura 15). Se estiver, ele não executa mais nenhuma ação nesse turno, refletindo uma pausa estratégica no jogo (lembre-se: o jogador na casa 60 deverá esperar os demais caso não tenha sido eliminado). Se não estiver no reconhecimento da corte, o método então verifica se ele deve pausar seu turno (linha 11), condição que pode ter sido imposta por eventos anteriores (cartas de evento). Essa lógica ilustra como o estado do participante pode ser afetado por ações anteriores, aumentando a complexidade e a imprevisibilidade do jogo. Caso o jogador esteja ativo, o método prossegue com RolarDado (linha 18), que gera um número aleatório e simula a rolagem de um dado físico, introduzindo o elemento de sorte. Essa rolagem determina quantas casas o jogador avança – mecânica central em muitos jogos de tabuleiro; após o movimento, sua posição é cuidadosamente ajustada (linhas 20 a 25) para garantir que não ultrapasse os limites do tabuleiro, mostrando atenção aos detalhes na programação. Em seguida, o jogo executa ações com base na casa onde o participante caiu. Se ele pousar numa casa especial (como um duelo ou evento), ações correspondentes são executadas, cada uma com suas próprias regras e consequências – como duelos, eventos aleatórios e encontros especiais –, que trazem uma rica variedade e garantem que nenhum turno seja igual ao outro. Isso não apenas mantém os jogadores engajados, mas também reflete a temática dos mosqueteiros, onde aventura e incerteza são elementos constantes. Métodos auxiliares – como RolarDado, ExecutarEvento e ExecutarDuelo – são componentes vitais que enriquecem a experiência do jogo, incorporando elementos de aleatoriedade, estratégia e interatividade. Na linha 2 da figura 16, o método RolarDado é exemplo clássico de introdução de aleatoriedade em jogos de tabuleiro. Lembrete Esse método simples gera um número aleatório entre um e seis, simulando a rolagem de um dado de seis faces. 51 PROGRAMAÇÃO ORIENTADA A OBJETOS II 1. // Método para rolar o dado 2. private int RolarDado() 3. { 4. return aleatorio.Next(1, 7); // Dado de 6 faces 5. } 6. 7. // Método para executar um evento 8. private void ExecutarEvento(Jogador jogador) 9. { 10. CartaDeEvento carta = baralhoDeEventos.SortearCarta(); 11. Console.WriteLine($”Evento: {carta.Descricao}”); 12. carta.Efeito(jogador); 13. } 14. 15. private void ExecutarDuelo(Jogador jogador) 16. { 17. Console.WriteLine($”{jogador.Nome} está em um duelo!”); 18. ResultadoDuelo resultado = roletaDeDuelo.Girar(); 19. 20. switch (resultado) 21. { 22. case ResultadoDuelo.Vitoria: 23. Console.WriteLine(“Você venceu o duelo! Avance 3 casas e ganhe 2 fichas de Honra.”); 24. jogador.Posicao = Math.Min(jogador.Posicao + 3, tabuleiro.NumeroDeCasas); 25. jogador.FichasDeHonra += 2; 26. break; 27. case ResultadoDuelo.Empate: 28. Console.WriteLine(“O duelo terminou em empate. Fique onde está e ganhe 1 ficha de Honra.”); 29. jogador.FichasDeHonra += 1; 30. break; 52 Unidade I 31. case ResultadoDuelo.Derrota: 32. Console.WriteLine(“Você foi derrotado. Volte 2 casas e perca 1 ficha de Honra.”); 33. jogador.Posicao = Math.Max(jogador.Posicao – 2, 1); 34. jogador.FichasDeHonra = Math.Max(jogador.FichasDeHonra– 1, 0); 35. break; 36. } 37. } 38. // Demais métodos Figura 16 – Classe Jogo: quarta parte – métodos auxiliares ExecutarEvento (linha 8) é outro método fundamental que ativa os eventos aleatórios durante o jogo. Ao chamá-lo, uma carta de evento é selecionada aleatoriamente do baralho de eventos, e o efeito descrito é aplicado ao jogador. Cada carta de evento, com seu efeito único, garante que nenhum jogo seja igual ao outro. Já o método ExecutarDuelo (linha 15) traz à tona a temática de duelos de esgrima, recorrente na era dos mosqueteiros, sendo chamado quando um jogador cai numa casa de duelo, e a roleta é girada para determinar o resultado: vitória, empate ou derrota (lembre-se que criamos uma enumeração para representar esses desfechos). Cada resultado tem diferentes implicações para a posição do jogador no tabuleiro e seus recursos, como fichas de honra ou fortuna. Esse método não só reforça a temática do jogo, mas também cria momentos de tensão e excitação. Quando um jogador vence um duelo, a regra indica que ele deve avançar três casas, no entanto deve-se respeitar uma condição fundamental: não ultrapassar o número total de casas disponíveis no tabuleiro; e aqui a função Math.Min entra em ação como ferramenta de segurança. Ela compara dois valores: posição potencial do jogador após avançar três casas e número total de casas no tabuleiro. O menor dos dois valores é escolhido como sua nova posição. O método pertence à biblioteca System.Math, e a utilizamos (assim como Math.Max) para lidar com limites de valor, garantindo que os dados permaneçam dentro de um intervalo válido e prevenindo erros que poderiam surgir se ele ultrapassasse o fim do tabuleiro. Esses métodos auxiliares exemplificam como diferentes partes de um sistema de jogo podem interagir e criar uma experiência coesa, sendo chamados em momentos apropriados durante o jogo, cada um contribuindo para progredir de maneira significativa. Na figura 17, o método ExecutarDueloDeHonra começa com a determinação dos oponentes possíveis para um desafiante específico, utilizando a palavra-chave var e uma expressão lambda (linha 4). Var é uma prática comum em C# para declarar variáveis quando o tipo é óbvio ou redundante. Na declaração da linha 4, var oponentesPossiveis é uma lista de jogadores elegíveis para um duelo, gerada pelo método Where da biblioteca LINQ, que filtra a lista de jogadores baseando-se em critérios específicos definidos na expressão lambda j => j!= desafiante && !j.NoReconhecimentoDaCorte && !j.Eliminado. 53 PROGRAMAÇÃO ORIENTADA A OBJETOS II 1. private void ExecutarDueloDeHonra(Jogador desafiante) 2. { 3. 4. var oponentesPossiveis = jogadores.Where(j => j != desafiante && !j.NoReconhecimentoDaCorte && !j.Eliminado).ToList(); 5. 6. if (oponentesPossiveis.Count == 0) 7. { 8. Console.WriteLine(“Não há oponentes disponíveis para o duelo.”); 9. return; 10. } 11. 12. Jogador oponente; 13. if (oponentesPossiveis.Count == 1) 14. { 15. // Se houver apenas um oponente possível, escolha-o automaticamente 16. oponente = oponentesPossiveis[0]; 17. } 18. else 19. { 20. // Se houver mais de um oponente possível, permita que o jogador escolha 21. oponente = EscolherOponente(desafiante, oponentesPossiveis); 22. } 23. 24. Console.WriteLine($”{desafiante.Nome} desafiou outro jogador para um Duelo de Honra!”); 25. 26. ResultadoDuelo resultado = roletaDeDuelo.Girar(); 27. 28. switch (resultado) 29. { 30. case ResultadoDuelo.Vitoria: 54 Unidade I 31. Console.WriteLine($”{desafiante.Nome} venceu o duelo e ganhou 2 fichas de Honra do oponente.”); 32. TransferirFichasDeHonra(oponente, desafiante, 2); 33. break; 34. case ResultadoDuelo.Empate: 35. Console.WriteLine(“O duelo terminou em empate. Ninguém perde ou ganha fichas.”); 36. break; 37. case ResultadoDuelo.Derrota: 38. Console.WriteLine($”{desafiante.Nome} foi derrotado e perdeu 2 fichas de Honra para o oponente.”); 39. TransferirFichasDeHonra(desafiante, oponente, 2); 40. break; 41. } 42. } 43. // Demais métodos Figura 17 – Classe Jogo: quinta parte – método auxiliar ExecutarDueloDeHonra Expressão lambda é uma função anônima concisa que especifica que um jogador é um oponente possível se ele não for o desafiante, não estiver no reconhecimento da corte e não tiver sido eliminado; é um exemplo eficiente de como critérios múltiplos podem ser aplicados para filtrar dados em uma coleção. Aplicado o filtro, o método ToList() converte o resultado da expressão lambda, que é um IEnumerable, em uma lista (List). Isso é feito por várias razões, sendo a principal a necessidade de materializar os resultados do filtro em uma coleção concreta que possa ser utilizada posteriormente. Conversão para uma lista também permite operações adicionais, como contar o número de elementos ou acessar elementos específicos por índice – o que seria mais complicado com um IEnumerable. Com a lista de oponentes possíveis na mão, o código verifica se há jogadores disponíveis para duelar usando o método Count. Se não houver nenhum (linha 6), o método retorna e o duelo não sucede; caso contrário, se houver apenas um oponente possível (linha 13), este é automaticamente escolhido. Se houver mais de um, a escolha é delegada ao método EscolherOponente, que permite uma seleção mais interativa. A parte seguinte do método envolve a execução do duelo propriamente dita, utilizando a RoletaDeDuelo para determinar o resultado, que trará diferentes ações a tomar, como ganhar ou perder fichas de honra, que se concretizam por chamadas de métodos específicos – como TransferirFichasDeHonra –, que manipulam o estado dos jogadores de acordo com as regras. O método AudienciaComORei representa uma etapa crítica no jogo, na qual um jogador interage com um cenário-chave – a audiência com o rei –, incorporando lógica de decisão, aleatoriedade e consequências significativas para seu andamento. 55 PROGRAMAÇÃO ORIENTADA A OBJETOS II 1. private void AudienciaComORei(Jogador jogador) 2. { 3. Console.WriteLine($”{jogador.Nome} chegou à Audiência com o Rei!”); 4. 5. bool escolhaValida = false; 6. while (!escolhaValida) 7. { 8. Console.WriteLine(“Escolha [1] para Reconhecimento da Corte ou [2] para Roleta do Julgamento Real:”); 9. string escolha = Console.ReadLine(); 10. 11. if (escolha == “1”) 12. { 13. jogador.NoReconhecimentoDaCorte = true; 14. Console.WriteLine($”{jogador.Nome} escolheu o Reconhecimento da Corte.”); 15. escolhaValida = true; 16. } 17. else if (escolha == “2”) 18. { 19. bool vitoria = GirarRoletaDoJulgamentoReal(jogador); 20. if (vitoria) 21. { 22. Console.WriteLine($”{jogador.Nome} foi condecorado pelo Rei e venceu o jogo!”); 23. AnunciarVencedorImediato(jogador); 24. Environment.Exit(0); // Encerra o programa 25. } 26. else 27. { 28. Console.WriteLine($”{jogador.Nome} recebeu o Desprezo da Corte e foi eliminado do jogo.”); 29. jogador.Eliminado = true; 56 Unidade I 30. } 31. escolhaValida = true; 32. } 33. else 34. { 35. Console.WriteLine(“Escolha inválida, tente novamente.”); 36. } 37. } 38. } 39. // Demais métodos Figura 18 – Classe Jogo: sexta parte – método auxiliar AudienciaComORei Inicialmente, o método informa que o jogador chegou à audiência com o rei, um momento potencialmente decisivo. Interpola-se a string na linha 3 para personalizar a mensagem com o nome do jogador, aumentando a imersão e a personalização da experiência. Em seguida, entra-se em um loop while que continua até o jogador fazer uma escolha válida. Esse loop é uma técnica comum, que garante ao usuário fornecer uma entrada esperada e válida, evitando erros ou comportamentos inesperados. Dentro do loop, o jogador enfrenta um dilema crítico: escolher o reconhecimento da corte ou enfrentara roleta do julgamento real (conforme os requisitos do quadro 1). Se o jogador escolher o reconhecimento da corte (escolha == “1”), a variável booleana NoReconhecimentoDaCorte do objeto jogador é definida como verdadeira. Essa escolha é então anunciada, e a variável escolhaValida é definida como verdadeira, terminando o loop e prosseguindo o jogo. Caso ele opte pela Roleta do Julgamento Real (escolha == “2”), o método GirarRoletaDoJulgamentoReal é chamado. O método retorna um valor booleano que determina se ele vence ou perde nessa roleta. Se o resultado for vitória, ele é declarado vencedor, e a função AnunciarVencedorImediato é chamada para lidar com a situação. É um método muito simples (figura 19) e apenas gera um retorno na tela do console a respeito do vencedor. 1. private void AnunciarVencedorImediato(Jogador vencedor) 2. { 3. Console.WriteLine($”\nO vencedor é {vencedor.Nome}!”); 4. Console.ReadKey(); 5. } 6. // Demais métodos Figura 19 – Classe Jogo: sétima parte – método auxiliar AnunciarVencedorImediato 57 PROGRAMAÇÃO ORIENTADA A OBJETOS II Na sequência, o método Environment.Exit é usado em seguida para encerrar o programa, indicando uma conclusão bem-sucedida. Disponível no namespace System, essa função encerra imediatamente um processo em execução e pode ser muito útil em diversas situações. Ao ser chamada, Environment.Exit requer um código de saída inteiro, usado pelo sistema operacional para entender o status de término do processo. Ele é especialmente importante se o programa fizer parte de um pipeline maior ou se for chamado por outros softwares, pois permite comunicar se o processo foi concluído com sucesso. Por outro lado, se o resultado for uma derrota (linha 26 da figura 18), o jogador é eliminado, o que é refletido pela definição da propriedade Eliminado do objeto jogador como verdadeira. Caso ele não escolha nem 1 nem 2, uma mensagem de erro é exibida, e o loop continua, solicitando uma nova entrada (linha 35); essa parte do código garante que ele não prossiga sem fazer uma escolha válida, mantendo a integridade do fluxo do jogo. Vamos analisar agora outros quatro métodos auxiliares: • TodosJogadoresNoReconhecimentoOuDesprezo • TransferirFichasDeHonra • GirarRoletaDoJulgamentoReal • TodosJogadoresTerminaram 1. private bool TodosJogadoresNoReconhecimentoOuDesprezo() 2. { 3. return jogadores.All(j => j.NoReconhecimentoDaCorte || j.Posicao == tabuleiro.NumeroDeCasas); 4. } 5. 6. 7. private bool GirarRoletaDoJulgamentoReal(Jogador jogador) 8. { 9. // Implementação simplificada. Pode ser substituída por uma mecânica mais complexa se necessário. 10. return roletaDeDuelo.Girar() == ResultadoDuelo.Vitoria; 11. } 12. 13. private bool TodosJogadoresTerminaram() 14. { 58 Unidade I 15. // Implementação do método para verificar se todos os jogadores terminaram o jogo 16. foreach (var jogador in jogadores) 17. { 18. if (jogador.Posicao , e o corpo da função, que retorna um valor booleano (true ou false), está à direita. 59 PROGRAMAÇÃO ORIENTADA A OBJETOS II No contexto do método All, se a expressão lambda retornar true para todos os elementos da coleção, então o All como um todo retorna true. Se pelo menos um elemento não satisfizer a condição (ou seja, a expressão lambda retornar false para esse elemento), All retornará false. Por exemplo, se tivermos uma lista de números e quisermos verificar se todos são positivos, podemos usar o All com expressão lambda assim: numeros.All(n => n > 0). Aqui, n => n > 0 é a expressão lambda, onde n é o parâmetro que representa cada elemento da lista à medida que All itera sobre ela. Para cada elemento, a expressão verifica se n é maior que zero. Se todos os elementos da lista forem positivos, o resultado será true; caso contrário, será false. O método GirarRoletaDoJulgamentoReal é outra parte crucial. Ele é invocado quando um jogador chega à última casa do tabuleiro e opta por girar a roleta do julgamento real. Através de um sorteio, decide-se seu destino – se ele vence (e potencialmente ganha o jogo) ou é eliminado. Na linha 10 da figura 20, o método retorna o resultado da comparação: roletaDeDuelo.Girar() == ResultadoDuelo. Vitoria. Aqui, roletaDeDuelo.Girar chama o método Girar da classe RoletaDeDuelo, que retorna um valor do tipo ResultadoDuelo (que pode ser Vitoria, Empate ou Derrota). O valor retornado é então comparado com ResultadoDuelo.Vitoria. Se o resultado do método Girar for Vitoria, a expressão será verdadeira (true), caso contrário será falsa (false). É uma implementação direta e simples que decide o resultado baseado em uma única rotação da roleta. Portanto, se cada resultado tiver a mesma probabilidade de ser escolhido e existem três resultados possíveis, a probabilidade de qualquer um deles (incluindo Vitoria) é de um terço, ou aproximadamente 33%. Essa probabilidade é importante pois afeta diretamente a experiência do jogador e o equilíbrio da partida. A taxa de sucesso de um terço pode ser considerada justa em muitos jogos por oferecer uma mistura equilibrada de incerteza e possibilidade de sucesso. No entanto, conforme nos lembra o comentário da linha 9, se quiséssemos alterar a dificuldade ou a dinâmica do jogo, poderíamos ajustar essas probabilidades modificando a lógica de geração aleatória ou introduzindo outros elementos que afetam as chances de cada resultado. Para complexificar essa mecânica, poderíamos introduzir fatores adicionais que influenciariam o resultado. Por exemplo: • Probabilidades variáveis: em vez de ter chances iguais para Vitoria, Empate e Derrota, poderíamos ajustar as probabilidades com base em certas condições ou atributos do jogador. Por exemplo, se ele tiver um alto número de fichas de honra, as chances de vitória podem aumentar. • Múltiplas rodadas: em vez de decidir o resultado com uma única rotação, poderíamos exigir que o jogador ganhasse várias rodadas consecutivas para alcançar a vitória. Por exemplo, ele precisa conseguir Vitoria duas vezes em três rotações para realmente vencer. 60 Unidade I • Efeitos acumulativos: o resultado das rotações anteriores poderia afetar as rotações subsequentes. Por exemplo, se um jogador perde na primeira rodada, isso poderia diminuir suas chances de vitória nas rodadas seguintes, ou talvez ele precise de mais vitórias para compensar a derrota inicial. • Condições externas: o resultado poderia ser influenciado por eventos externos ou ações de outros jogadores. Por exemplo, se um jogador recentemente venceu um duelo, isso poderia aumentar suas chances na roleta dojulgamento real. • Elementos de estratégia: os jogadores poderiam ter a opção de apostar fichas de honra ou fortuna para aumentar as chances de vencer, adicionando um elemento de risco e recompensa. São apenas algumas ideias para adicionar complexidade ao método GirarRoletaDoJulgamentoReal. A chave aqui é: ao introduzir mais variáveis e decisões, o jogo se torna mais estratégico e menos dependente do acaso, o que pode aumentar o envolvimento e a diversidade da experiência. O terceiro método, TodosJogadoresTerminaram, verifica se todos os jogadores alcançaram a última casa do tabuleiro – exemplo prático de como iterar uma lista de objetos (jogadores) e verificar a propriedade (Posicao) de cada um deles. Já o foreach – estrutura de loop amplamente utilizada para percorrer coleções como listas, arrays ou quaisquer outros tipos que implementam a interface IEnumerable – proporciona uma maneira simples e eficiente de acessar cada elemento de uma coleção, sem precisar gerenciar manualmente um contador ou índice. No contexto do foreach, a palavra-chave var declara uma variável que representa o elemento atual da coleção iterada (linha 16 da figura 20). Var em C# é uma forma de declarar uma variável implícita, cujo compilador infere automaticamente o tipo da variável com base no valor que ela recebe. No caso, como jogadores é uma coleção de objetos Jogador, o compilador infere que a variável jogador é do tipo Jogador. A principal vantagem do var é que ele torna o código mais limpo e fácil de ler, especialmente se o tipo da variável for óbvio ou muito longo para ser digitado. Por outro lado, a palavra in no foreach especifica a coleção que estiver sendo percorrida. No exemplo, in jogadores indica que o loop foreach deve iterar cada elemento dentro da coleção jogadores. Em cada iteração do loop, a variável jogador recebe um elemento da coleção jogadores, permitindo que o código dentro do loop acesse propriedades e métodos do objeto Jogador atual. Por fim, na linha 27 da figura 20 temos o método TransferirFichasDeHonra, utilizado em duelos, que permite transferir fichas de honra de um jogador para outro, demonstrando como se manipulam atributos de objetos em C#. Ele encapsula a lógica de transferência e garante que a quantidade de fichas transferidas não exceda a quantidade disponível do jogador que está transferindo (note o uso de Math.Min na linha 29). Agora vamos analisar em detalhe a lógica e os aspectos técnicos de três outros métodos: EscolherOponente, TributoAoCardeal e VerificarFimDeJogo, conforme o código-fonte da figura 21: 61 PROGRAMAÇÃO ORIENTADA A OBJETOS II 1. // Método para escolher oponente para o duelo 2. private Jogador EscolherOponente(Jogador desafiante, List possiveisOponentes) 3. { 4. int indiceEscolhido = -1; 5. 6. Console.WriteLine(“Escolha um oponente para o Duelo de Honra:”); 7. for (int i = 0; i possiveisOponentes.Count) 18. { 19. Console.WriteLine(“Escolha inválida, tente novamente.”); 20. } 21. else 22. { 23. indiceEscolhido = escolha – 1; // Ajuste para índice baseado em zero 24. } 25. } 26. 27. return possiveisOponentes[indiceEscolhido]; 28.} 29. 30.// Método para o tributo ao Cardeal 62 Unidade I 31.private void TributoAoCardeal(Jogador jogador) 32.{ 33. if (jogador.FichasDeFortuna >= 2) 34. { 35. jogador.FichasDeFortuna -= 2; 36. Console.WriteLine($”{jogador.Nome} pagou 2 fichas de Fortuna como tributo ao Cardeal.”); 37. } 38. else 39. { 40. jogador.FichasDeHonra -= 1; 41. Console.WriteLine($”{jogador.Nome} não tinha fichas de Fortuna suficientes e perdeu 1 ficha de Honra.”); 42. } 43.} 44. 45.// Método para verificar se o jogo terminou 46.private bool VerificarFimDeJogo(Jogador jogador) 47.{ 48. // Checa se o jogador alcançou a última casa 49. return jogador.Posicao == tabuleiro.NumeroDeCasas; 50.} 51. // Demais métodos Figura 21 – Classe Jogo: nona parte – métodos auxiliares EscolherOponente, TributoAoCardeal e VerificarFimDeJogo Na linha 1 o método EscolherOponente permite que um jogador (identificado como desafiante) escolha o oponente de uma lista de jogadores possíveis (possiveisOponentes) para um duelo de honra. O processo começa listando na tela todos os jogadores elegíveis para o duelo, excluindo o próprio desafiante e quaisquer participantes no reconhecimento da corte ou que já tenham sido eliminados. 63 PROGRAMAÇÃO ORIENTADA A OBJETOS II Lembrete É o método ExecutarDueloDeHonra que faz a seleção inicial dos possíveis oponentes, e não diretamente dentro do método EscolherOponente (vide linha 4 da figura 17). A lista filtrada é passada para o método EscolherOponente, que trabalha apenas com a lista de jogadores já filtrados no método ExecutarDueloDeHonra. Essa separação de papéis é importante: ExecutarDueloDeHonra prepara a lista de oponentes possíveis, enquanto EscolherOponente lida com a interação do usuário para escolher um dos oponentes pré-filtrados. A implementação usa um loop while (linha 12 da figura 21) para garantir que a entrada do usuário seja válida e, uma vez que um índice válido é fornecido, ele é ajustado para corresponder à indexação baseada em zero da lista possiveisOponentes. O método então retorna o Jogador escolhido como oponente. A técnica de ajuste do índice (linha 23) é crucial para alinhar a escolha do usuário com a indexação da lista em C#, que começa em zero. Além disso, o método exemplifica o uso de validação de entrada do usuário num cenário de jogo interativo, garantindo a integridade das ações. Convém esclarecer que o método int.TryParse (linha 17) é uma forma segura de converter uma string em número inteiro. Diferentemente do método int.Parse, o TryParse não lança uma exceção se a conversão falhar; em vez disso, ele retorna um valor booleano que indica o sucesso ou fracasso da conversão. TryParse é uma escolha prudente quando a entrada do usuário pode ser imprevisível e potencialmente não numérica. A palavra-chave out declara uma variável local (escolha) no mesmo momento em que ela é passada como argumento, e o valor convertido é atribuído a essa variável se a conversão for bem-sucedida. Observe também que a segunda parte da condição (escolha possiveisOponentes.Count) garante que o número escolhido pelo usuário não exceda o número de oponentes disponíveis. É uma medida importante para prevenir erros de índice fora dos limites que ocorreriam se o usuário escolhesse um número maior que o de possíveis oponentes. Na linha 31 da figura 21 temos o método TributoAoCardeal, acionado quando um jogador cai numa casa específica do tabuleiro que exige um tributo ao cardeal. A lógica do método é simples e eficaz em termos de mecânica de jogo: verifica-se se o jogador tem o mínimo de duas fichas de fortuna (linha 33). Se tiver, paga duas como tributo; caso contrário, perde uma ficha de honra. O método ilustra a manipulação de atributos do objeto Jogador, alterando seu estado com base nas regras. A decisão 64 Unidade I entre duas diferentes moedas (fichas de fortuna e de honra) adiciona uma camada de complexidade estratégica, influenciando as decisões tomadas e o andamento do jogo. Já o método VerificarFimDeJogo na linha 46 tem um propósito claro: determinar se um jogadorespecífico alcançou a condição de término – no caso, chegar à última casa do tabuleiro. A lógica é direta: o método retorna um valor booleano (linha 49) que indica se a posição do jogador é igual ao número total de casas no tabuleiro (tabuleiro.NumeroDeCasas). O método exemplifica uma verificação de condição comum em jogos de tabuleiro, em que alcançar determinado ponto ou posição sinaliza seu fim. Além disso, ao encapsular essa lógica num método, o código ganha em clareza e reusabilidade, permitindo que a mesma lógica de verificação seja aplicada consistentemente em várias partes do jogo. Estamos quase finalizando os métodos da classe Jogo. A figura 22 tem os detalhes dos métodos AnunciarVencedor (linha 2) e IniciarJogo (linha 28). O primeiro é chamado quando o jogo identifica que um ou mais jogadores alcançaram um estado que permite determinar um vencedor. Isso ocorre quando todos os jogadores estão no estado de reconhecimento da corte ou quando atingem a última casa do tabuleiro. 1. // Método para anunciar o vencedor 2. private void AnunciarVencedor() 3. { 4. var pontuacoes = new Dictionary(); 5. foreach (var jogador in jogadores.Where(j => j.NoReconhecimentoDaCorte)) 6. { 7. int pontuacao = jogador.FichasDeFortuna + 2 * jogador.FichasDeHonra; 8. pontuacoes.Add(jogador, pontuacao); 9. } 10. 11. int maiorPontuacao = pontuacoes.Values.Max(); 12. var vencedores = pontuacoes.Where(p => p.Value == maiorPontuacao).Select(p => p.Key).ToList(); 13. 14. if (vencedores.Count == 1) 15. { 16. Console.WriteLine($”\nO vencedor é {vencedores[0].Nome} com {maiorPontuacao} pontos!”); 17. } 18. else 19. { 20. string nomesDosVencedores = string.Join(“, “, vencedores.Select(v => v.Nome)); 65 PROGRAMAÇÃO ORIENTADA A OBJETOS II 21. Console.WriteLine($”\nHouve um empate entre {nomesDosVencedores}, com {maiorPontuacao} pontos cada um.”); 22. } 23. 24. Console.ReadKey(); 25.} 26. 27.// Método para iniciar o jogo 28.public void IniciarJogo() 29.{ 30. Console.WriteLine(“\nBem-vindo ao Jogo dos Mosqueteiros!\n”); 31. // Inicializações do jogo, se necessário 32. 33. Console.WriteLine(“Pressione qualquer tecla para começar.”); 34. Console.ReadKey(); // Aguarda o usuário pressionar uma tecla 35. 36. ExecutarJogo(); 37.} 38.// Demais métodos Figura 22 – Classe Jogo: décima parte – métodos auxiliares AnunciarVencedor e IniciarJogo O método AnunciarVencedor implementa uma solução para determinar o vencedor num jogo de tabuleiro, projetada para lidar não apenas com a identificação de um vencedor único, mas também com a possibilidade de empate entre dois ou mais jogadores. A escolha de um Dictionary para armazenar pontuações é central para a lógica implementada, pois em C# é uma coleção que armazena pares de chave-valor; nesse caso cada objeto Jogador é usado como chave, e a respectiva pontuação é armazenada como valor. A principal vantagem de usar um Dictionary aqui é sua eficiência em associar chaves (jogadores) a valores (pontuações) e na recuperação desses valores – estrutura de dados que permite calcular, armazenar e acessar pontuações de maneira rápida e direta, facilitando o processo de determinar o vencedor ou identificar um empate. No início do método é criado um Dictionary vazio (linha 4) para armazenar pontuações. O método então itera (linha 5) a lista de jogadores (note novamente o uso do Where e da expressão lambda), calculando a pontuação de cada um que está no reconhecimento da corte. 66 Unidade I Lembrete Pontuação é a soma das fichas de fortuna mais o dobro das fichas de honra. Esse cálculo foi definido nos requisitos do quadro 1 e reflete a importância dual da fortuna e da honra no jogo, atribuindo um peso maior à honra, conforme o fator de multiplicação. Essas pontuações são então adicionadas ao Dictionary (linha 8). Calculadas as pontuações, o método identifica a maior pontuação usando o Max do LINQ (linha 11), uma maneira eficiente e concisa de encontrar o maior valor numa coleção. Em seguida, utiliza-se uma consulta LINQ para selecionar todos os jogadores cuja pontuação corresponda a esse valor máximo (linha 12). Ao utilizar o método Where, aplicamos um filtro sobre essa coleção; esse filtro percorre cada par chave-valor no Dictionary e seleciona apenas aqueles cujo valor (a pontuação do jogador) se iguale à variável maiorPontuacao, previamente determinada como maior pontuação entre todos os jogadores. Na linha 12, a expressão lambda p => p.Value == maiorPontuacao serve como critério para esse filtro, onde p representa cada par chave-valor e p.Value acessa a pontuação associada a esse par; aplicado o filtro, o método Select transforma os elementos filtrados. Já o Select(p => p.Key) converte cada par chave-valor filtrado em sua chave, que é o objeto Jogador. Em outras palavras, a transformação extrai o jogador de cada par chave-valor que passou pelo filtro do Where. Finalmente, a chamada ao método ToList converte a sequência resultante da filtragem e transformação numa lista concreta de objetos Jogador, composta pelos jogadores cujas pontuações são iguais à maior pontuação encontrada. Já a lógica para anunciar o vencedor ou um empate depende do número de jogadores que alcançaram a maior pontuação. Se apenas um jogador tiver a maior pontuação, ele é declarado vencedor (linha 14 à 17); caso contrário, o método reconhece um empate e lista o nome desses jogadores (linha 19 à 22). A expressão da linha 20 começa com a chamada do método Join da classe string – método conhecido por sua habilidade em concatenar elementos de uma coleção inserindo um separador específico entre cada elemento. No caso presente, o separador escolhido é uma vírgula seguida de um espaço (“, ”), escolha que reflete o objetivo de criar uma lista legível e bem formatada do nome dos jogadores. O primeiro argumento do método Join é o separador, enquanto o segundo é a coleção de strings a concatenar. Para obtê-la, aplicamos o método Select sobre a lista vencedores, que contém os objetos Jogador que empataram ou venceram. A expressão lambda v => v.Nome passada para o Select especifica que, para cada objeto Jogador na lista vencedores, o método deve extrair a propriedade Nome do jogador. O resultado é uma coleção de strings, cada uma representando o nome de um jogador vencedor ou empatado. Ao combinar string.Join com a expressão LINQ vencedores.Select(v => v.Nome), conseguimos gerar de maneira sucinta uma única string que lista esses nomes, separados por vírgula – abordagem que não 67 PROGRAMAÇÃO ORIENTADA A OBJETOS II apenas simplifica o código, mas também o torna mais legível e fácil de manter, evitando loops manuais e concatenações complexas de strings. A figura 22 também apresenta o método IniciarJogo na linha 28. No início uma mensagem de boas-vindas é exibida para o jogador utilizando Console.WriteLine; a ação é simples mas essencial para uma introdução amigável. A interação imediata com o usuário é feita pelo Console.ReadKey, que aguarda uma entrada de teclado para prosseguir – mecanismo frequentemente utilizado em aplicações de console para controlar o fluxo do programa, pausando a execução até o usuário fazer uma ação específica; no caso, pressionar qualquer tecla. Após essa interação inicial, o método ExecutarJogo é chamado; e este é o núcleo do ciclo de jogo, no qual a lógica principal é executada. 1.// Método para processar a casa em que o jogador caiu 2. private void ProcessarCasa(Casa casa, Jogador jogador) 3. { 4. Console.WriteLine($”Jogador {jogador.Nome} caiu na casa {jogador.Posicao}.”); 5. switch (casa.Tipo) 6. { 7. case TipoCasa.DueloDeHonra: 8. ExecutarDueloDeHonra(jogador); 9. break; 10. case TipoCasa.Duelo: 11. ExecutarDuelo(jogador); 12. break; 13. case TipoCasa.AudienciaComORei: 14. AudienciaComORei(jogador); 15. break; 16. case TipoCasa.Evento: 17. ExecutarEvento(jogador); 18. break; 19.case TipoCasa.EscolaDeEsgrima: 20. jogador.FichasDeHonra++; 21. Console.WriteLine($”{jogador.Nome} treinou na Escola de Esgrima e ganhou 1 ficha de Honra.”); 68 Unidade I 22. break; 23. case TipoCasa.Estalagem: 24. jogador.FichasDeFortuna = Math.Max(0, jogador.FichasDeFortuna – 1); 25. Console.WriteLine($”{jogador.Nome} pagou pela estadia e perdeu 1 ficha de Fortuna.”); 26. break; 27. case TipoCasa.TributoAoCardeal: 28. TributoAoCardeal(jogador); 29. break; 30. case TipoCasa.Retorno: 31. if (casa.CasaDeRetorno.HasValue) 32. { 33. jogador.Posicao = casa.CasaDeRetorno.Value; 34. Console.WriteLine($”{jogador.Nome} retornou para a casa {jogador.Posicao}.”); 35. } 36. break; 37. default: 38. Console.WriteLine(“Nenhuma ação especial nesta casa.”); 39. break; 40. } 41. } Figura 23 – Classe Jogo: última parte – método auxiliar ProcessarCasa Finalmente, o último método da classe Jogo. O ProcessarCasa na figura 23 define as ações que ocorrem quando um jogador cai em determinada casa no tabuleiro, exemplificando vários conceitos de programação em C#, como estrutura de controle switch, uso de enums e manipulação de objetos. Ele recebe dois parâmetros: um objeto Casa, que representa a casa na qual o jogador caiu; e um objeto Jogador, que representa o jogador atual (linha 2). A lógica do método é baseada no tipo da casa, determinado pela enumeração TipoCasa. Dentro do método, um switch (linha 5) é usado para tratar cada possível tipo de casa. A estrutura de controle é uma maneira eficiente de direcionar o fluxo do programa com base no valor de uma variável – no caso, o tipo da casa. Para cada tipo, uma série de ações é executada, afetando o estado do jogador de diferentes maneiras. 69 PROGRAMAÇÃO ORIENTADA A OBJETOS II Por exemplo, se a casa for do tipo DueloDeHonra, o método ExecutarDueloDeHonra é chamado para o jogador atual; se for do tipo Evento, o método ExecutarEvento é chamado. A propriedade HasValue na linha 31 é um exemplo interessante da manipulação de tipos que podem ou não ter valor definido (característica comum em C#); no caso, verifica se o objeto CasaDeRetorno dentro da classe Casa tem valor atribuído. CasaDeRetorno é um exemplo de tipo Nullable, funcionalidade da linguagem que permite aos tipos de valor – como inteiros e datas – serem nulos. Em C#, tipos de valor como int, float e double não podem ser nulos por padrão. Isso significa que sempre devem conter um valor como 0, 1,5 etc. Contudo, um valor nulo (ou ausência de um valor) pode ser condição necessária. Para acomodar isso, C# introduziu os tipos Nullable, representados por um ponto de interrogação após o tipo (por exemplo, int?). Quando um tipo Nullable é declarado, ele pode conter um valor ou ser nulo; aqui a propriedade CasaDeRetorno é do tipo int?, ou seja, pode conter um número inteiro ou ser nula. Isso é útil no contexto do jogo, pois nem todas as casas precisam ter uma casa de retorno associada. Para algumas casas esse conceito não se aplica, portanto seu valor pode ser nulo. HasValue é uma forma de verificar se um objeto Nullable tem valor atribuído ou não. Se retorna true, significa que o objeto contém um valor; se retorna false, o objeto é nulo. Portanto, no trecho if (casa.CasaDeRetorno.HasValue), o código está verificando se a casa atual no tabuleiro tem uma casa de retorno associada; se houver, o código subsequente (que depende desse valor) pode ser executado com segurança, sabendo que existe um valor válido para usar. 1.1.1 Considerações sobre o código‑fonte Chegamos ao final do código-fonte do jogo Os desafios dos mosqueteiros: duelos & destinos. Ao todo são aproximadamente 660 linhas (a quantidade pode variar um pouco, a depender das linhas em branco entre métodos e classes); você pode compilá-lo e executá-lo para ver o jogo funcionar. Observação Se tiver dificuldade nessa tarefa, leia as seções 1.2, 1.3 e 1.4. Podem ser úteis. Mas não chegamos ao final da revisão proposta para este tópico. Após compilar e executar o código, analise-o atentamente e verifique que alguns conceitos básicos de POO e C# ainda não estão presentes nesta implantação. Começaremos por herança e polimorfismo (embora este estivesse presente na discussão sobre o delegate Action). Podemos criar uma hierarquia de classes para diferentes tipos de casa no tabuleiro. Atualmente, elas são diferenciadas apenas pelo enum TipoCasa, mas podemos torná-las mais versáteis e extensíveis com herança. Vamos criar uma classe-base Casa e subclasses específicas para cada tipo de casa, como 70 Unidade I CasaEvento, CasaDuelo etc. Cada subclasse pode ter propriedades e métodos específicos para suas ações; por exemplo, a classe CasaEvento pode ter um método para lidar com eventos. Alterações necessárias: • modificar a classe Casa para ser uma classe-base abstrata (figura 24); • criar subclasses para cada tipo de casa (figura 25); • modificar o método InicializarCasas na classe Tabuleiro para usar as novas subclasses (figura 26); • atualizar o método ProcessarCasa na classe Jogo para utilizar o novo método ProcessarAcao (figura 27). 1. abstract class Casa 2. { 3. public TipoCasa Tipo { get; protected set; } 4. public int? CasaDeRetorno { get; protected set; } 5. 6. protected Casa(TipoCasa tipo, int? casaDeRetorno = null) 7. { 8. Tipo = tipo; 9. CasaDeRetorno = casaDeRetorno; 10. } 11. 12. // Método abstrato para processar a ação da casa 13. public abstract void ProcessarAcao(Jogador jogador); 14. } Figura 24 – Herança: classe abstrata Casa Uma classe abstrata em POO não pode ser instanciada diretamente; ela serve como base para outras classes. A classe abstrata Casa na figura 24 serve como esqueleto ou modelo para outras classes e não pode ser instanciada diretamente, ou seja, não se pode criar um objeto diretamente dessa classe; em vez disso, ela é projetada para ser a base de outras classes, chamadas subclasses. 71 PROGRAMAÇÃO ORIENTADA A OBJETOS II A escolha de tornar Casa uma classe abstrata é uma aplicação prática do princípio da abstração, que é diferente de uma classe abstrata (abstração, em contexto mais amplo, é o processo de esconder a complexidade e mostrar apenas o necessário). No código, a classe Casa abstrai a ideia de uma casa no tabuleiro, fornecendo uma estrutura comum para todos os tipos de casa, como CasaEvento, CasaDuelo, entre outras. Cada uma dessas subclasses pode ter características e comportamentos específicos, mas compartilham certos traços definidos na classe Casa. As propriedades na classe Casa são definidas como protected (linhas 3 e 4) para permitir que somente a própria classe Casa e suas subclasses tenham acesso a elas. Isso é útil para garantir que as propriedades essenciais da Casa, como Tipo e CasaDeRetorno, sejam facilmente acessíveis e modificáveis pelas subclasses, mas não pelo mundo externo. Essa abordagem encapsula e protege dados, mantendo-os seguros e expostos apenas onde necessários. ProcessarAcao é declarado na linha 13 como método abstrato dentro da classe abstrata Casa. Método abstrato, por definição, não tem implementação na classe em que é declarado, deixando para as subclasses a responsabilidade de fornecer uma implementação concreta. Isso é um exemplo de polimorfismo, ou seja, diferentes classes derivadas de uma mesma superclasse implementam um método de maneiras diferentes. A decisão de tornar ProcessarAcao um método abstrato se baseia na ideia de que cada tipo de casa no tabuleiro terá uma maneira única de processar a ação quando um jogador cair nela. Assim, cada subclasse de Casa precisa definir como essa ação é processada, o que é coerente com a natureza diversificada das ações no jogo. Na figura 25 temos a subclasse CasaEvento, que exemplifica essa natureza diversificada. A palavra-chave base será usada no construtor das subclasses de Casa, como em base(TipoCasa. Evento), para chamar o construtor dade pedidos .............................................................. 245 7.2.3 Contexto delimitado 3: administração de clientes ................................................................ 246 7.3 Práticas de programação segura em C# ...................................................................................246 7.4 Autenticação e autorização ...........................................................................................................257 8 DESENVOLVIMENTO CROSS-PLATFORM E MOBILIDADE ...............................................................263 8.1 Desenvolvimento cross-platform com Xamarin ou MAUI ................................................264 8.2 Desenvolvimento de aplicativos móveis ...................................................................................265 8.3 Gráficos e visualização de dados .................................................................................................276 8.4 Microsserviços .....................................................................................................................................282 8.5 Arquiteturas baseadas em eventos .............................................................................................286 7 APRESENTAÇÃO O objetivo geral deste livro-texto é proporcionar ao aluno uma continuidade da disciplina anterior, aprofundando conceitos e desenvolvendo novas habilidades, em especial o acesso a banco de dados, com exercícios práticos. O aluno também entenderá o desenvolvimento de aplicações com interfaces gráficas para desktop que observem e implementem os conceitos de interação humano-computador (IHC). Os objetivos específicos são, entre outros: • aprofundar os princípios da programação orientada a objetos (POO) e apresentar técnicas de persistência de dados utilizando conexões com banco de dados locais ou remotos, considerando aspectos da computação em nuvem; • desenvolver interfaces com C# para desktop, adotando as melhores práticas de experiência do usuário; • capacitar os alunos a implementar aplicações em camadas utilizando o paradigma de POO em C#. A disciplina foi estruturada para oferecer uma abordagem mais detalhada e aprofundada dos conceitos e práticas associadas à POO, com ênfase em áreas de desenvolvimento avançado, como criação de interfaces gráficas e gerenciamento de bancos de dados. A era digital evoluiu, e a programação e o desenvolvimento de software se tornaram a espinha dorsal de muitas operações e serviços. Dentro desse vasto domínio, esta disciplina não é apenas uma extensão, mas uma exploração mais profunda dos intrincados caminhos da POO, focada principalmente na construção de interfaces gráficas e interatividade com bancos de dados. Apresentaremos o universo das interfaces gráficas, começando com o Windows Forms. Numa época em que a IHC é mais relevante do que nunca, é imperativo que os futuros programadores compreendam suas nuances e as de user experience (UX). Em qualquer aplicação, a maneira como os dados são apresentados e organizados pode determinar a facilidade de uso e a eficácia da interface. Assim, a disciplina se aprofunda em técnicas para criar layouts tanto esteticamente agradáveis quanto funcionais. Outro aspecto vital é o foco na arquitetura e no design. O desenvolvimento em camadas e o padrão model-view-controller (MVC) são aprofundados, oferecendo uma visão clara de como os aplicativos de grande escala são estruturados e organizados. Esse tema não apenas solidifica o entendimento da estrutura de software, como também prepara futuros programadores para desafios reais. Integração com bancos de dados é outro tópico primordial. Abordaremos a criação de conexões com bancos de dados, sejam eles locais, sejam eles hospedados em ambientes de computação em nuvem. A relevância de se conectar a bases de dados num cenário de computação em nuvem é de extrema importância, dada a tendência crescente da digitalização de serviços e do armazenamento em nuvem; 8 além disso, a utilização de strings de conexão, especialmente pelo SQLClient, é discutida em detalhes. Language Integrated Query (LINQ) é uma inovação que permite aos desenvolvedores interagir com bancos de dados de maneira mais intuitiva e declarativa. Também serão explorados o Windows Presentation Foundation (WPF) e a Extensible Application Markup Language (XAML), componentes fundamentais para desenvolver interfaces gráficas ricas em aplicações .NET. O primeiro permite aos desenvolvedores criar interfaces avançadas que podem aproveitar ao máximo recursos do hardware; a segunda oferece uma maneira declarativa de definir a aparência e o comportamento da interface gráfica. Essas ferramentas são vitais para desenvolver interfaces avançadas e personalizadas para desktop, dado que ambas permitem criar interfaces gráficas avançadas e personalizáveis, tornando-se um conhecimento valioso para qualquer desenvolvedor focado em aplicações desktop. Na contemporaneidade do desenvolvimento de software, dominar técnicas avançadas, manter padrões de segurança robustos e compreender os paradigmas do desenvolvimento cross-platform são competências indispensáveis. O alcance desses temas abrange desde práticas específicas de linguagens de programação até conceitos arquiteturais complexos, alinhando-os às necessidades atuais e futuras da indústria. Ao longo da disciplina, o aluno não apenas ganha conhecimento teórico, mas também habilidades práticas. Os assuntos serão complementados por exercícios que garantem a aplicabilidade do conhecimento adquirido. INTRODUÇÃO Uma vez que a POO é o paradigma utilizado em C#, começaremos, já na unidade I, a rever seus conceitos básicos com exercícios práticos no console. Não são apenas aquecimentos, mas fundamentos sólidos sobre os quais se constroem os edifícios complexos do conhecimento. Cada exercício reforça o aprendizado anterior e pavimenta o caminho para conceitos avançados em seguida. A transição dos conceitos básicos para aplicações mais sofisticadas é guiada pela introdução ao ambiente de C#, como veremos no tópico 1. Aplicativos modernos contêm interfaces gráficas ricas, por isso o desenvolvedor precisa saber desenvolvê-las adequadamente; e neste livro-texto abordaremos dois aspectos: a programação em si e o design de interfaces. Sobre programação, veremos no tópico 2 detalhes sobre Windows Forms, um recurso poderoso para criar aplicativos para desktop, explorando elementos gráficos desde os básicos até os mais avançados: rótulos, caixas de texto, botões, painéis, grupos e muitos outros componentes. Cada elemento será analisado não apenas em termos de sua funcionalidade, mas também de sua relevância e aplicabilidade em cenários do mundo real. Deve-se entender o peso da escolha certa de componentes para oferecer a melhor experiência de usuário possível. Também não podemos negligenciar a complexidade e a variedade que vêm com os elementos gráficos, por isso o tópico 3 apresenta e discute caixas de opção e de seleção, calendários, seletores de data e muito mais; cada um desses componentes desempenha papel único 9 na criação de interfaces intuitivas e eficientes. À medida que avançamos no aprendizado desses elementos, somos confrontados com o desafio de organizar dados e componentes; introduziremos, então, menus, caixas de listagem, controles de abas e outras ferramentas. No mundo da programação, interatividade é chave. Isto posto, manipular eventos – como ações de mouse e teclado – é habilidade essencial. O tópico 4 oferece uma exploração abrangente dos eventos, permitindo que o aluno compreenda e analise as várias interações possíveis dentro de uma interface gráfica. A evolução do aprendizado não se limita a componentes individuais; também integra vários componentes em uma interface coesa. Aqui entramos no domínio da multiple document interface (MDI), característica central em muitos aplicativos modernos. Ao final do tópico 4 teremos um projeto prático para os conceitosclasse-base (linha 3 da figura 25); isso é essencial para garantir que a inicialização definida na classe-base seja executada, além de qualquer inicialização específica da subclasse. 1. class CasaEvento : Casa 2. { 3. public CasaEvento() : base(TipoCasa.Evento) { } 4. 5. public override void ProcessarAcao(Jogador jogador) 6. { 7. // Lógica específica para casa de evento 8. } 9. } 10. 11. // Semelhante para CasaDuelo, CasaEscolaDeEsgrima, etc. Figura 25 – Herança e polimorfismo: exemplo de subclasse CasaEvento 72 Unidade I No exemplo dado isso significa que, ao criar uma instância de CasaEvento, a classe não apenas executa sua própria inicialização, mas também a inicialização definida na classe Casa, garantindo que todas as propriedades e comportamentos necessários sejam devidamente configurados. Note também que o método ProcessarAcao na linha 5 da figura 25 sucede a palavra-chave override, evidenciando claramente o conceito polimórfico. 1. private void InicializarCasas() 2. { 3. //... código anterior... 4. switch (i) 5. { 6. case 5: 7. case 20: 8. //... outros casos... 9. casas.Add(new CasaEvento()); 10. break; 11. //... outros casos... 12. } 13. } Figura 26 – Herança: atualização do método InicializarCasas na classe Tabuleiro para usar as novas subclasses 1. private void ProcessarCasa(Casa casa, Jogador jogador) 2. { 3. Console.WriteLine($”Jogador {jogador.Nome} caiu na casa {jogador.Posicao}.”); 4. casa.ProcessarAcao(jogador); 5. } Figura 27 – Herança: atualização do método ProcessarCasa na classe Jogo para utilizar o novo método ProcessarAcao Essas mudanças introduzem herança e polimorfismo no código, permitindo que cada tipo de casa tenha comportamentos específicos e facilite a adição de novos tipos de casa no futuro. 73 PROGRAMAÇÃO ORIENTADA A OBJETOS II Observação Os códigos das figuras 25 e 26, bem como os próximos códigos-fonte deste tópico, são apenas exemplos ilustrativos e não exaustivos. Para funcionarem adequadamente, é necessário completar alterações, conforme os próprios comentários dos referidos códigos. 1. interface IAcaoCasa 2. { 3. void ExecutarAcao(Jogador jogador); 4. } Figura 28 – Polimorfismo de interface: IAcaoCasa Podemos também adaptar o código do jogo usando polimorfismo de interface (figura 28). Uma abordagem seria criar uma interface para as diferentes ações que os jogadores podem realizar nas casas do tabuleiro, incluindo eventos, duelos, audiências com o rei, entre outras. No caso, precisamos criar uma interface chamada IAcaoCasa, que definirá um método ExecutarAcao a ser implementado por diferentes classes de ação. A seguir, precisamos criar classes específicas para cada tipo de ação possível numa casa, implementando a interface IAcaoCasa. A figura 29 mostra um exemplo para uma ação de duelo; o mesmo deve ser feito em outros tipos de ação, como eventos, audiências com o rei etc. 1. class AcaoDuelo : IAcaoCasa 2. { 3. public void ExecutarAcao(Jogador jogador) 4. { 5. // Lógica do duelo aqui 6. } 7. } Figura 29 – Polimorfismo de interface: AcaoDuelo Também seria necessário modificar a classe Casa para usar a interface IAcaoCasa, adicionando um campo para armazenar referência à ação associada à casa. Finalmente, o método ProcessarCasa da classe Jogo deve ser ajustado para usar polimorfismo; em vez de um grande switch-case, podemos simplesmente chamar o método ExecutarAcao da ação associada à casa. O código original do jogo também não apresenta polimorfismo ad hoc, que ocorre quando um único símbolo ou operação se comporta de maneira diferente com base nos argumentos com que é chamado ou no contexto em que é utilizado. Há dois tipos principais de polimorfismo ad hoc: sobrecarga (overloading) e coerção (coercion). 74 Unidade I Para introduzir esse conceito no código do jogo dos mosqueteiros, uma abordagem comum é utilizar a sobrecarga de métodos (overloading), pois permite que vários métodos com o mesmo nome existam, mas com diferentes assinaturas de parâmetros (tipos ou números de argumento). Isso é útil quando desejamos que um método realize operações similares, mas com diferentes tipos de dados ou número de argumentos. Vamos considerar a classe Jogador e introduzir sobrecarga no método AtualizarTurno. Inicialmente, ele atualiza o turno do jogador baseado no número total de casas no tabuleiro (linha 30 da figura 12). Podemos sobrecarregá-lo para também poder atualizar o turno baseado em outros fatores, como um evento específico ou uma ação de outro jogador (figura 30). 1. public void AtualizarTurno(int numeroTotalDeCasas) 2. { 3. // Lógica original... 4. } 5. 6. // Sobrecarga do método para aceitar um evento 7. public void AtualizarTurno(CartaDeEvento evento) 8. { 9. // Lógica específica para quando um evento ocorre 10. } 11. 12. // Sobrecarga do método para aceitar a ação de outro jogador 13. public void AtualizarTurno(Jogador jogadorAcao) 14. { 15. // Lógica específica para quando outro jogador influencia o turno 16. } Figura 30 – Polimorfismo ad hoc: método AtualizarTurno Com essas alterações, o método agora pode lidar com diferentes cenários: um é o cenário-padrão, onde o turno é atualizado com base no número de casas no tabuleiro (linha 1); o segundo é quando um evento específico influencia o turno do jogador (linha 7); e o terceiro é quando o turno é afetado pela ação de outro jogador (linha 13). Essa abordagem exemplifica polimorfismo ad hoc, pois o mesmo nome de método (AtualizarTurno) é usado em diferentes contextos com diferentes tipos de argumento. Cada versão tem uma implementação específica e mais adequada ao tipo de dado fornecido como argumento. Apesar de a revisão deste tópico ter discutido um pouco os métodos delegados (como na figura 10), vale a pena exercitá-los aqui de forma independente. Para adicionar um exemplo de eventos e delegados no código do jogo, podemos criar um sistema de notificação para quando eventos importantes acontecem, como quando um participante ganha uma rodada, perde fichas ou é eliminado. 75 PROGRAMAÇÃO ORIENTADA A OBJETOS II Vamos definir um delegado e um evento na classe Jogador e dispará-lo em situações específicas (a figura 31 ilustra esse cenário, cuja linha 4 define um delegado chamado JogadorEventHandler). Delegado é um tipo que representa referências a métodos com uma lista de parâmetros e tipos de retorno específicos. No caso, o delegado pode apontar para qualquer método que aceite um objeto Jogador e uma string como parâmetros e que não retorne nada (void). 1. class Jogador 2. { 3. // Definição do delegado 4. public delegate void JogadorEventHandler(Jogador jogador, string mensagem); 5. 6. // Evento baseado no delegado 7. public event JogadorEventHandler JogadorMudou; 8. 9. // Restante da classe... 10. 11. // Método para disparar o evento 12. protected virtual void OnJogadorMudou(string mensagem) 13. { 14. JogadorMudou?.Invoke(this, mensagem); 15. } 16. 17. // Exemplo de uso do evento dentro da classe 18. public void GanharFichaDeHonra() 19. { 20. FichasDeHonra++; 21. OnJogadorMudou($”ganhou uma ficha de honra. Total agora: {FichasDeHonra}”); 22. } 23. 24. //... Outros métodos da classe... 25. } Figura 31 – Delegado e Evento na classe Jogador 76 Unidade I Na linha 7 é declarado um evento público chamado JogadorMudou, baseado no tipo de delegado JogadorEventHandler. Em C#, eventos são um tipo especial de delegado que pode ser invocado apenas de dentro da classe que o declara, proporcionando um mecanismo para comunicação entre classes. Na linha 12 criamos um método para disparar o evento, denominado OnJogadorMudou, e a palavra-chave virtual permite que o método seja sobrescrito em uma subclasse. O operador ?. é uma verificação de nulidade segura – o evento só será invocado se houver inscritos (não nulo). Por fim, no método GanharFichaDeHonra (linha 18), o evento JogadorMudou é disparadoapós a atualização de um atributo fictício FichasDeHonra. Isso ilustra como eventos podem notificar outras partes do código sobre mudanças ou ações ocorridas dentro de uma classe. Para que a alteração tenha efeito é necessário modificar a classe Jogo para se inscrever e reagir ao evento. A figura 32 explicita essas duas atividades (linhas 10 e 15, respectivamente). Por exemplo, você pode exibir uma mensagem sempre que o estado de um jogador mudar. Dentro do construtor da classe Jogo, existe um loop que se inscreve no evento JogadorMudou de cada objeto Jogador na coleção jogadores (linha 10). 1. class Jogo 2. { 3. public Jogo(Random random) 4. { 5. //... [Restante do construtor não modificado]... 6. 7. // Inscrever-se no evento para cada jogador 8. foreach (var jogador in jogadores) 9. { 10. jogador.JogadorMudou += JogadorMudouHandler; 11. } 12. } 13. 14. // Handler do evento 15. private void JogadorMudouHandler(Jogador jogador, string mensagem) 16. { 17. Console.WriteLine($”Notificação: {jogador.Nome} {mensagem}”); 18. } 19. //... [Restante da classe não modificado]... 20. } Figura 32 – Event Handler na classe Jogo 77 PROGRAMAÇÃO ORIENTADA A OBJETOS II O método JogadorMudouHandler da linha 15 é um handler para o evento JogadorMudou e corresponde à assinatura do delegado JogadorEventHandler. Quando o evento é disparado em um objeto Jogador, ele é executado e imprime uma mensagem no console. No exemplo, sempre que um jogador ganhar uma ficha de honra (usando o método GanharFichaDeHonra), o evento JogadorMudou será disparado, e a classe Jogo (inscrita no evento) reagirá exibindo uma mensagem na tela. Isso exemplifica o uso de delegados e eventos para criar um sistema de notificação dentro do jogo. É possível expandir esse conceito para outros aspectos do jogo, como quando alguém é eliminado, ganha uma rodada, ou diante de outros eventos significativos. 1. class RoletaDeDuelo 2. { 3. private static RoletaDeDuelo instancia; 4. private Random aleatorio; 5. 6. // Construtor privado 7. private RoletaDeDuelo(Random random) 8. { 9. aleatorio = random; 10. } 11. 12. // Método estático para obter a instância 13. public static RoletaDeDuelo ObterInstancia(Random random) 14. { 15. if (instancia == null) 16. { 17. instancia = new RoletaDeDuelo(random); 18. } 19. return instancia; 20. } 21. // Restante da classe... 22. } Figura 33 – Singleton: classe RoletaDeDuelo Além da herança, do polimorfismo e dos métodos delegados, podemos relembrar os design patterns. Para incorporar um exemplo de padrão de projeto no código, vamos implementar o padrão Singleton 78 Unidade I para a classe RoletaDeDuelo (figura 33), garantindo que apenas uma instância dessa classe seja criada e compartilhada. Observe na linha 7 que o construtor é privado, para impedir a criação de instâncias da classe RoletaDeDuelo de fora da classe. Isso é essencial para o padrão Singleton, pois garante que apenas a própria classe crie sua instância. Também é necessário modificar a classe Jogo para usar a instância Singleton de RoletaDeDuelo (figura 34). 1. class Jogo 2. { 3. private RoletaDeDuelo roletaDeDuelo; 4. 5. public Jogo(Random random) 6. { 7. //... 8. roletaDeDuelo = RoletaDeDuelo.ObterInstancia(random); 9. //... 10. } 11. 12. // Restante da classe... 13. } 14. Figura 34 – Singleton: instância na classe Jogo Alternativamente, vamos implementar um padrão Factory para criar objetos Jogador, que cria objetos sem expor a lógica de criação ao cliente e refere-se ao objeto criado com uma interface comum. Vamos criar uma fábrica simples para a classe Jogador, conforme a figura 35. O Factory Pattern para Jogador centraliza a criação de objetos Jogador e oferece flexibilidade para futuras expansões ou modificações na maneira como os jogadores são criados. 1. class JogadorFactory 2. { 3. // Método de fábrica para criar um jogador 4. public static Jogador CriarJogador(string nome, Random random) 5. { 6. return new Jogador(nome, random); 7. } 8. } Figura 35 – Padrão Factory: classe JogadorFactory 79 PROGRAMAÇÃO ORIENTADA A OBJETOS II Também é necessário modificar o construtor da classe Jogo para usar JogadorFactory (figura 36). 1. public Jogo(Random random) 2. { 3. //... 4. jogadores = new List() { 5. JogadorFactory.CriarJogador(“D’Artagnan”, random), 6. JogadorFactory.CriarJogador(“Athos”, random), 7. //... 8. }; 9. //... 10. } Figura 36 – Padrão Factory: classe Jogo Para finalizar a revisão, podemos falar de melhorias de código. Uma área específica de melhoria identificada é a repetição de código, em particular o método RolarDado, que aparece em várias classes. Essa funcionalidade – crucial para a dinâmica do jogo – aparece repetidamente em diferentes classes, sobretudo em Jogo e Jogador. Tal repetição, embora aparentemente inofensiva, pode levar a problemas de manutenção e consistência à medida que o projeto evolui. A solução envolve consolidar a lógica de rolagem de dados em um único ponto, proporcionando um design de código mais limpo, fácil de manter e escalável. A estratégia ideal para abordar essa redundância é introduzir um método centralizado que possa ser chamado por diferentes partes do código. Considerando a natureza e o escopo do projeto, um método estático numa classe utilitária seria a solução mais adequada, pois não só elimina a duplicação, mas também garante que qualquer alteração futura na lógica de rolagem de dados seja implementada em um único lugar, refletindo-se em todo o código. Para implementar essa melhoria, cria-se uma classe chamada UtilitariosDeJogo (figura 37), que contém um método estático RolarDado (linha 5), que por sua vez encapsula a funcionalidade de gerar um número aleatório representando o resultado da rolagem de um dado de seis faces. 1. public static class UtilitariosDeJogo 2. { 3. private static readonly Random random = new Random(); 4. 5. public static int RolarDado() 6. { 7. return random.Next(1, 7); 8. } 9. } Figura 37 – Melhoria: classe UtilitariosDeJogo 80 Unidade I A classe UtilitariosDeJogo também mantém sua própria instância de Random, garantindo que a geração de números aleatórios seja centralizada e consistente. Implementada a classe utilitária, o próximo passo é refatorar as classes existentes (Jogo e Jogador) para utilizar o novo método estático. Isso envolve remover as implementações internas de RolarDado dessas classes e substituí-las por chamadas ao método UtilitariosDeJogo.RolarDado. Na linha 4 da figura 38, temos um exemplo dessa aplicação no método AtualizarTurno da classe Jogador. 1. public void AtualizarTurno(int numeroTotalDeCasas) 2. { 3. //... 4. int movimento = UtilitariosDeJogo.RolarDado(); 5. //... 6. } Figura 38 – Refatoração: método AtualizarTurno na classe Jogador Essa mudança não só reduz a duplicação de código, mas também melhora a clareza. Qualquer desenvolvedor que trabalhe no código pode agora entender facilmente que a rolagem de dados é um serviço fornecido de forma centralizada, sem necessidade de se preocupar com variações na implementação em diferentes locais do código. Essa abordagem de refatoração, focada em reduzir a repetição de código e centralizar funcionalidades comuns, é um exemplo clássico de aplicação de princípios de design de software limpo e eficiente. Em um ambiente de desenvolvimento colaborativo, essa mudança também faria outros desenvolvedores entenderem e utilizarem essa funcionalidade em outras partes do código, se necessário. 1.2 Apresentação geral dos recursos e elementos de interface no ambiente de programação C# A interface de programação no ambiente C# é mais do que um simples conjunto de ferramentas; é um ecossistema robusto e flexível que suporta e eleva o trabalho do desenvolvedor, proporcionando um ambiente no qual criatividade e eficiência podem florescer. Ao mergulhar mais profundamenteno layout-padrão do ambiente de desenvolvimento dentro do universo de programação em C#, descobrimos um espaço meticulosamente organizado, pensado para otimizar tanto a eficiência quanto a experiência do usuário; uma sinfonia de elementos de interface, cada um com seu papel, mas todos trabalhando em harmonia para criar um ambiente de desenvolvimento coeso e intuitivo. No centro desse ambiente estão as janelas de código, nas quais a programação ganha vida. Elas são mais do que meros recipientes para texto; são o epicentro da criação, oferecendo recursos como realce de sintaxe e autocompletar, que não apenas aumentam a legibilidade do código, mas também 81 PROGRAMAÇÃO ORIENTADA A OBJETOS II aceleram o processo de escrita. A facilidade com que se pode alternar entre diferentes arquivos de código, visualizá-los lado a lado ou mesmo em janelas separadas exemplifica a flexibilidade oferecida ao desenvolvedor. O realce de sintaxe permite diferenciar visualmente elementos como palavras-chave, variáveis, strings e comentários através de cores e estilos de fonte. Ele transforma um bloco de texto em uma paisagem codificada, na qual cada elemento se destaca claramente. Além de reduzir o cansaço visual, ajuda o programador a identificar rapidamente a estrutura do código, erros de sintaxe e padrões. Mais do que isso, o realce de sintaxe facilita a leitura e compreensão do código, especialmente em projetos complexos ou ao revisar o código de outras pessoas. Já o autocompletar age como assistente inteligente, sugere completamentos de código enquanto o programador digita. Isso não só acelera o processo de escrita – eliminando a necessidade de digitar cada caractere de uma função ou nome de variável –, mas também minimiza erros de digitação e inconsistências. Além disso, para programadores que estiverem aprendendo ou trabalhando com bibliotecas novas, essa funcionalidade é um recurso de aprendizado, pois apresenta funções e métodos disponíveis que talvez não sejam de conhecimento imediato. As dicas de código, por sua vez, fornecem informações contextuais sobre funções, métodos, parâmetros e tipos de dados diretamente no editor de texto. Ao posicionar o cursor sobre uma parte do código, uma pequena janela de informação aparece oferecendo descrições, documentação e até mesmo exemplos de uso. Essa funcionalidade é inestimável, pois fornece ao programador uma rápida referência sem precisar sair do contexto de codificação para buscar documentação externa. Paralelamente encontramos a janela de solução, que serve como mapa do tesouro para o projeto. Ela exibe a estrutura do projeto de maneira clara e permite que o desenvolvedor navegue rapidamente entre diferentes componentes, como classes, bibliotecas e recursos. Essa janela é um meio de organização e um ponto de controle no qual se pode adicionar, remover ou modificar facilmente elementos do projeto. A estrutura de um projeto C# é fundamental para o sucesso de qualquer aplicativo, sendo composta por uma série de arquivos e pastas organizados de maneira lógica, facilitando tanto o desenvolvimento quanto a manutenção. No núcleo encontramos os arquivos de código-fonte (onde reside a lógica do programa), complementados por arquivos de recursos, como imagens e arquivos de configuração, essenciais para funcionalidade e personalização do aplicativo. Além disso, a estrutura do projeto inclui arquivos de metadados, como o arquivo de solução (.sln) e os arquivos de projeto (.csproj), que contêm informações vitais para compilá-lo e executá-lo. Essa estrutura é uma forma de organizar arquivos e reflete a modularidade e a escalabilidade do projeto, permitindo um desenvolvimento ordenado e sistemático; porém a gestão eficaz deles é tão importante quanto sua estrutura. O gerenciamento de soluções e projetos no ambiente C# envolve várias atividades críticas, como configurar dependências, definir configurações de compilação e gerir múltiplas versões 82 Unidade I ou variantes do mesmo projeto. Um aspecto crucial desse gerenciamento é a capacidade de trabalhar com várias soluções e projetos simultaneamente, facilitando o desenvolvimento de aplicações complexas, que podem incluir várias bibliotecas, serviços e aplicações dependentes. O ambiente de desenvolvimento integrado (integrated development environnment – IDE) oferece ferramentas para visualizar e manipular a estrutura do projeto, tornando o processo de adicionar, remover ou atualizar componentes uma tarefa simples e intuitiva. As ferramentas comuns nesse layout são diversas e abrangem uma gama de funcionalidades. Há o painel de propriedades – no qual se pode ajustar as configurações de elementos selecionados – e o console de saída – que oferece feedback em tempo real do estado do programa durante a execução. As janelas de código, de solução, de propriedades e de saída oferecem uma visão ampla e ao mesmo tempo detalhada do projeto em desenvolvimento. Essas janelas não são estáticas, mas altamente personalizáveis, e permitem que cada desenvolvedor ajuste o ambiente ao seu estilo de trabalho. A habilidade de arrastar, fixar ou esconder painéis assegura que a interface possa se adaptar a diversas necessidades e preferências. Ferramentas de diagnóstico – como depurador e janelas de inspeção de variáveis – são indispensáveis para identificar e resolver problemas. Elas não apenas simplificam a tarefa de depuração, mas também proporcionam insights valiosos sobre o funcionamento interno do código. Depuração e diagnóstico são componentes vitais no desenvolvimento de software, especialmente no contexto da programação em C#, pois representam a arte e a ciência de identificar e resolver problemas dentro de um código – tarefa que exige tanto habilidade técnica quanto um pensamento analítico aguçado. Ferramentas e técnicas de depuração – como pontos de interrupção e acompanhamento de execução, além da capacidade de visualizar e alterar variáveis em tempo real – são fundamentais. Os pontos de interrupção são, talvez, a ferramenta mais imediatamente associada à depuração; eles permitem que o programador pause a execução do código em pontos específicos, a fim de inspecionar o estado do programa naquele exato momento. Essa pausa controlada é valiosa pois oferece uma janela para o mundo interno do programa, permitindo que o desenvolvedor observe o comportamento do código e identifique discrepâncias entre o funcionamento esperado e o real. A flexibilidade dos pontos de interrupção também é notável; eles podem ser configurados para ativar sob condições específicas ou quando certos tipos de dados são encontrados, tornando-os uma ferramenta poderosa para isolar e diagnosticar problemas complexos. Além disso, o acompanhamento de execução – geralmente atuando com os pontos de interrupção – proporciona uma visão dinâmica de como o programa se desenrola ao longo do tempo. Essa funcionalidade permite que o desenvolvedor percorra o código passo a passo, observando como os valores das variáveis e o estado do sistema mudam com cada operação. Esse método granular de examinar a execução do programa é crucial para entender o fluxo de controle e a lógica do código, especialmente quando o comportamento do programa não é imediatamente óbvio. A visualização e a alteração de variáveis em tempo real são outra faceta vital do processo de depuração. Durante uma sessão de depuração, o IDE permite que o programador inspecione os valores 83 PROGRAMAÇÃO ORIENTADA A OBJETOS II das variáveis, fornecendo insights imediatos sobre o estado do programa. A capacidade de visualizar o estado interno do programa nos ajuda muito a identificar onde as coisas estão indo mal; além disso, a habilidade de modificar os valores das variáveis em tempo real oferece uma maneira poderosa de testar hipóteses sobre a causa dos problemas, permitindo que o programador experimente correções e veja imediatamente os resultados de tais mudanças. No entanto, a depuração e o diagnóstico em C# vão além da simplescorreção de erros; eles representam uma oportunidade para os desenvolvedores aprofundarem o entendimento do código e melhorarem suas habilidades. Além disso, a integração com sistemas de controle de versão diretamente no IDE facilita o gerenciamento de mudanças no código, permitindo que equipes colaborem de maneira eficaz, aspecto que ressalta a natureza colaborativa da programação moderna, na qual o ambiente de desenvolvimento serve como ponto de encontro para ideias e esforços conjuntos. O IDE foi projetado com a ideia de centralização, em que todas as ferramentas necessárias estariam ao alcance do desenvolvedor, permitindo-lhe se concentrar na codificação sem distrações desnecessárias. Adicionalmente, recursos como busca integrada – que permite localizar rapidamente arquivos, classes, métodos ou mesmo linhas de código específicas – demonstram como o IDE é aliado poderoso na otimização do tempo de desenvolvimento. As ferramentas de navegação – que incluem a capacidade de ir diretamente para definições e referências, e a visualização de hierarquias de herança – são inestimáveis para a compreensão e a manutenção de códigos complexos. Importante também é a forma como o IDE facilita o entendimento e a correção de erros de código. Através de ferramentas de diagnóstico e de sugestões de correção automática, o ambiente de desenvolvimento não só aponta problemas, mas muitas vezes oferece soluções práticas – representando não apenas uma economia de tempo, mas também um recurso de aprendizado contínuo para o programador. O layout-padrão do ambiente de desenvolvimento em C#, com suas janelas e ferramentas, prima por design e funcionalidade, não apenas refletindo as necessidades práticas dos programadores, mas também encorajando um fluxo de trabalho mais intuitivo e produtivo. Em última análise, esse layout não é um mero aspecto técnico do desenvolvimento de software, mas um catalisador para criatividade e inovação no mundo da programação em C#. 1.3 Ambiente de desenvolvimento integrado (IDE) A escolha de um IDE para programar em C# é decisão crucial para qualquer desenvolvedor, pois é mais do que uma simples ferramenta; é o companheiro diário do programador e influencia significativamente a eficiência, a comodidade e até mesmo a qualidade do código produzido. Entre as opções disponíveis, Visual Studio e Rider se destacam, embora várias alternativas também mereçam consideração. O Visual Studio, desenvolvido pela Microsoft, é o mais escolhido para programar em C#. Sua integração perfeita com a linguagem C# eo .NET framework fazem dele uma escolha natural, especialmente para quem trabalha em projetos que se alinham estreitamente com o ecossistema da Microsoft. Esse IDE não é apenas robusto em termos de recursos – como depuração avançada, gestão de pacotes e suporte a controle de versão –, mas também é conhecido pela sua interface altamente personalizável e produtiva. 84 Unidade I Além disso, oferece suporte para uma vasta gama de projetos, desde aplicações web até desenvolvimento de jogos, tornando-o uma ferramenta versátil para muitos desenvolvedores. Por outro lado, o Rider, da JetBrains, se tornou popular especialmente para quem valoriza uma experiência de desenvolvimento ágil e eficiente. Embora seja uma opção mais recente em comparação com o Visual Studio, ele se destaca por sua performance rápida e recursos de refatoração inteligente, herdados de outras ferramentas da JetBrains, como o ReSharper. Sua interface é intuitiva, e sua capacidade de trabalhar sem problemas em diferentes sistemas operacionais – incluindo Windows, macOS e Linux – o torna atraente para equipes de desenvolvimento que operam em ambientes heterogêneos. Além dessas opções mais conhecidas, alternativas podem ser adequadas a depender das necessidades do desenvolvedor ou do projeto. Ferramentas como MonoDevelop ou Visual Studio Code oferecem ambientes mais leves e são frequentemente escolhidas para projetos menores ou desenvolvedores que buscam simplicidade e velocidade. Cada alternativa tem suas forças e limitações, como um conjunto de recursos diferente, suporte a extensões ou integração com determinadas tecnologias e frameworks. Visual Studio e Visual Studio Code são duas ferramentas poderosas, mas com propósitos e capacidades distintas. Essa diferença não é meramente nominal; ela reflete abordagens fundamentalmente diferentes para desenvolver software. O Visual Studio – IDE completo e robusto – é conhecido por sua ampla gama de funcionalidades e sua integração profunda com o ecossistema .NET e C#. É uma solução abrangente, que oferece suporte extensivo para desenvolver software em várias plataformas, incluindo desktop, mobile, web e até mesmo jogos. O Visual Studio se destaca pela sua capacidade de gerenciar projetos complexos e oferecer ferramentas avançadas para depuração, design de interface do usuário, gestão de banco de dados, análise de desempenho e outras funcionalidades integradas, sendo particularmente favorável para projetos que requerem abordagem intensiva e detalhada, como aplicações empresariais ou grandes sistemas de software. Já o Visual Studio Code (VS Code) é um editor de código-fonte mais leve, porém extremamente poderoso. Embora também suporte C# e outras linguagens, o VS Code focaliza simplicidade, velocidade e flexibilidade. Sua interface é mais enxuta, e ele opera com uma filosofia de extensões, permitindo aos usuários adicionar apenas as funcionalidades de que precisam, o que o torna notavelmente rápido e responsivo. É ideal para desenvolvimento web, scripts e projetos menores ou mais ágeis, nos quais a sobrecarga de um IDE completo não é necessária nem desejável. Um dos pontos mais evidentes de diferenciação é o consumo de recursos do sistema. O Visual Studio, com sua ampla gama de funcionalidades embutidas, tende a ser mais exigente em termos de recursos do sistema, o que o torna mais adequado para máquinas com especificações mais robustas. Em contraste, sendo mais leve, o VS Code pode funcionar de maneira eficiente mesmo em hardware menos potente. 85 PROGRAMAÇÃO ORIENTADA A OBJETOS II Outra distinção importante é sua abordagem de design e usabilidade. O Visual Studio oferece uma experiência mais integrada e coesa, com ferramentas e funcionalidades projetadas para trabalhar harmoniosamente dentro do seu ecossistema. Por outro lado, o VS Code fornece uma experiência mais modular e permite uma personalização mais granular através de extensões e plugins, o que pode atrair desenvolvedores que preferem uma abordagem mais ágil e personalizada. A escolha de uma IDE para C# depende de uma variedade de fatores, incluindo exigências específicas do projeto, a preferência pessoal do desenvolvedor, a necessidade de integração com outras ferramentas e plataformas, e até mesmo considerações de custo, já que há IDEs gratuitas e outras não. Portanto, essa escolha é tanto uma questão de adequação técnica quanto de conforto pessoal e eficiência no fluxo de trabalho; para o programador de C#, navegar por essas opções e encontrar a ferramenta certa é fundamental para estabelecer um ambiente de desenvolvimento que não só atenda às necessidades do projeto, mas também amplifique suas próprias habilidades e produtividade. Observação Neste livro-texto usaremos o Visual Studio, que contém a versão Community, amplamente acessível e gratuita (usaremos a versão 17.8.5 com a versão 4.8.09037 do .NET Framework). Também existem edições pagas – como o Visual Studio Professional e o Visual Studio Enterprise –, e cada uma oferece um conjunto de recursos e capacidades que vão além do que é disponibilizado na versão Community. Versões pagas são projetadas para atender às necessidades de desenvolvedores individuais e também para se alinhar aos requisitos de equipes maiores e projetos de uma escala mais ampla e complexa. Visual Studio Professional é um passo adiante em relação à edição Community, pois oferece funcionalidades adicionais úteis parapequenas e médias empresas, incluindo recursos avançados de colaboração e integração com ferramentas de DevOps, facilitando o trabalho em equipe e a gestão de projetos de software. Além disso, a versão oferece um suporte mais abrangente para testes automatizados e ferramentas de diagnóstico – cruciais para desenvolver software em ambiente profissional. Observação DevOps – combinação das palavras development (desenvolvimento) e operations (operações) – é uma filosofia e prática que permeia o mundo da tecnologia da informação, enfatizando a integração e comunicação entre desenvolvedores de software e outros profissionais de TI. A essência do DevOps reside na busca por automatizar e monitorar todas as fases de construção de software, desde integração, teste, liberação até a implantação e gestão da infraestrutura – abordagem que visa cultivar uma cultura de colaboração e eficiência, reduzir o ciclo de vida do desenvolvimento de sistemas e garantir entrega contínua de alta qualidade. 86 Unidade I O DevOps pressupõe que times de desenvolvimento e operações não devem operar em silos, mas colaborar de forma estreita durante todo o ciclo de vida do software. Essa colaboração significa que as mudanças são mais confiáveis e os problemas podem ser resolvidos mais rapidamente, com menos interrupções no serviço. Além disso, enfatiza a importância de feedback contínuo, adaptabilidade e aprendizado constante, com o objetivo de melhorar e inovar os processos de desenvolvimento e operação. As práticas do DevOps incluem integração, entrega e implantação contínuas, que juntas permitem às equipes de TI responder mais rapidamente às necessidades do negócio. Integração contínua refere-se à prática de mesclar automaticamente todas as alterações de código em um repositório comum, seguida por testes automáticos. Entrega contínua expande isso ao automatizar a liberação de mudanças para um ambiente de teste ou produção, enquanto a implantação contínua leva isso ainda mais longe, automatizando todo o processo, até a produção. DevOps também envolve a automação de infraestrutura, que se traduz em gerenciar e provisionar infraestruturas através de código e ferramentas, facilitando processos escaláveis e eficientes. Saiba mais Para se aprofundar no tema DevOps e suas práticas, recomendamos os dois livros a seguir. Ambos oferecem uma visão abrangente e detalhada do DevOps, abordando tanto aspectos teóricos quanto práticos. São recursos valiosos para quem busca entender e implementar suas práticas. KIM, G. et al. Manual de DevOps: como obter agilidade, confiabilidade e segurança em organizações tecnológicas. São Paulo: Alta Books, 2018. WALLS, C. Spring boot in action. Nova York: Manning, 2016. A versão Enterprise, por outro lado, é a mais robusta das edições do Visual Studio, destinada a grandes organizações e projetos de alta complexidade. Ela se destaca por incluir ferramentas de desenvolvimento, teste, entrega e diagnóstico de última geração, oferecendo funcionalidades como testes de carga avançados, análise de código mais sofisticada e ferramentas de simulação e modelagem – essenciais para projetos em larga escala. Além disso, fornece recursos extensivos para a segurança do aplicativo e a gestão do ciclo de vida do software, permitindo que equipes gerenciem eficientemente todas as fases do desenvolvimento, desde a concepção até a manutenção. Embora a versão Community do Visual Studio seja eficiente para estudantes, desenvolvedores individuais e pequenas equipes, as versões Professional e Enterprise oferecem um nível de suporte e funcionalidades essenciais em ambientes empresariais. As versões pagas proporcionam ferramentas 87 PROGRAMAÇÃO ORIENTADA A OBJETOS II mais avançadas e suporte técnico superior, além de opções de licenciamento mais adequadas a necessidades comerciais e organizacionais. Ao adentrar no mundo do Visual Studio, a experiência começa com a instalação e configuração – etapas nas quais o Visual Studio Installer desempenha papel crucial, especialmente na versão Community. Esses primeiros passos são fundamentais para moldar o IDE de acordo com as preferências individuais e as demandas de cada projeto. Visual Studio Installer é a porta de entrada para personalizar o ambiente de desenvolvimento. Ele guia o usuário por um processo de instalação intuitivo, no qual é possível selecionar componentes específicos, como ferramentas para desenvolvimento web, aplicações de desktop, programação mobile, entre outros. Essa abordagem modular permite que os desenvolvedores configurem o Visual Studio para atender exatamente às suas necessidades, evitando a instalação de recursos desnecessários que poderiam sobrecarregar o sistema. Para usuários da versão Community (gratuita), isso significa acessar um conjunto robusto de ferramentas de desenvolvimento sem custo associado, uma vantagem significativa para estudantes, desenvolvedores independentes e pequenas equipes. Após a instalação, a configuração básica no Visual Studio Community não difere muito das edições pagas e oferece uma gama de opções de personalização. Desenvolvedores podem ajustar desde as configurações de linguagem e editor até preferências de depuração e desempenho. Essa flexibilidade é vital para garantir que o ambiente de desenvolvimento esteja alinhado com as preferências individuais e as exigências do projeto, estabelecendo uma base sólida para uma programação eficiente e confortável. A personalização do layout e das preferências é uma das características mais apreciadas do Visual Studio, incluindo a versão Community. Desenvolvedores têm liberdade de moldar a interface do usuário de acordo com seus hábitos e estilos de trabalho, o que abrange reorganização de painéis e janelas, seleção de temas de cores e fontes e configuração de barras de ferramentas e menus para acesso rápido e eficiente. Essa personalização vai além da estética e influencia diretamente a produtividade e o conforto do desenvolvedor. Adicionalmente, tanto no Visual Studio Community quanto nas versões pagas, a capacidade de expandir funcionalidades do IDE por extensões e complementos é uma vantagem significativa. Essas extensões – disponíveis pelo gerenciador de extensões integrado – permitem adicionar desde utilitários de produtividade simples até ferramentas avançadas para analisar códigos e gerenciar projetos. Essa flexibilidade faz do Visual Studio, independentemente da edição, um ambiente altamente adaptável, capaz de atender às necessidades em evolução de desenvolvedores e projetos. Uma extensão popular costuma oferecer suporte aprimorado para linguagens de programação específicas ou frameworks; por exemplo, extensões para linguagens como JavaScript, Python ou para frameworks como Angular e React podem oferecer funcionalidades como realce de sintaxe melhorado, autocompletar inteligente e ferramentas de depuração específicas para essas tecnologias. Essas extensões tornam o Visual Studio mais adaptável a diferentes stacks tecnológicas, ampliando sua utilidade para além de .NET e C#. 88 Unidade I Outra categoria de extensões foca a melhoria da produtividade e eficiência do código. Ferramentas como analisadores de código e refatoradores automáticos ajudam a identificar problemas potenciais, sugerem melhorias e até automatizam a refatoração. Extensões como ReSharper ou CodeMaid são exemplos disso e oferecem uma série de funcionalidades para limpeza de código, otimização e reestruturação, o que ajuda a mantê-lo limpo, legível e eficiente. Extensões relacionadas ao controle de versão e colaboração também são úteis, pois podem oferecer integrações mais profundas com sistemas como Git e facilitar operações como commit, merge, push e pull diretamente do IDE. Ferramentas como GitLens, por exemplo, ampliam as capacidades do Visual Studio em termos de visualização de histórico de commits, comparação de branches e outras funcionalidades que tornam o trabalho colaborativo mais gerenciável e transparente(abordaremos o Git com mais detalhes neste tópico). Além disso, há extensões voltadas para a interface do usuário e experiência de desenvolvimento, como temas personalizados (por exemplo o Color Theme Editor), gerenciadores de janelas e ferramentas de navegação de código (como o Visual Assist). Essas extensões permitem personalizar a aparência do IDE, organizar o layout de trabalho de forma mais eficiente e navegar rapidamente entre arquivos e projetos. Por fim, extensões de integração contínua e entrega contínua (CI/CD) e gerenciamento de projetos também são populares, como Azure DevOps Extensions for Visual Studio, Jenkins Tools for Visual Studio e GitHub Actions for Visual Studio. Elas permitem que os desenvolvedores configurem pipelines de CI/CD dentro do Visual Studio, integrem ferramentas de rastreamento de bugs e projetos e gerenciem o ciclo de vida do desenvolvimento de software de maneira mais eficaz. Dentro do ambiente do Visual Studio, uma tarefa essencial e frequente é a gestão de dependências e pacotes, processo que garante a disponibilidade e atualização de todos os componentes necessários para um projeto. Essa gestão é crucial para a integridade e o sucesso de qualquer projeto de software, e no Visual Studio isso é feito predominantemente pelo NuGet, gerenciador de pacotes para .NET. Integrado ao Visual Studio, o NuGet serve como plataforma eficiente para gerenciar pacotes e permite que desenvolvedores busquem, instalem, atualizem e removam bibliotecas e ferramentas dentro de seus projetos de software de forma controlada e consistente. Essa funcionalidade é vital para manter a compatibilidade e a funcionalidade dos projetos, especialmente num ambiente onde múltiplas dependências e versões específicas podem determinar o sucesso ou falha de uma aplicação. Através do NuGet, desenvolvedores têm acesso a um vasto repositório de pacotes, o que facilita a descoberta e a incorporação de bibliotecas e ferramentas de terceiros em seus projetos. Esses pacotes podem variar desde frameworks e bibliotecas de user interface (UI) a pacotes para operações de rede ou manipulação de dados. A integração do NuGet no Visual Studio simplifica o processo de adicionar essas dependências, permitindo fazer isso diretamente através da interface do IDE, sem precisar baixar e configurar bibliotecas manualmente. Além disso, um aspecto crucial do gerenciamento de pacotes no Visual Studio é a capacidade de referenciar bibliotecas externas – funcionalidade que permite incluir e utilizar código que não tenha sido criado dentro do ambiente do seu projeto atual. 89 PROGRAMAÇÃO ORIENTADA A OBJETOS II Referenciar bibliotecas externas é uma prática comum, especialmente em projetos grandes e complexos, em que é necessário usar funcionalidades já desenvolvidas e testadas, economizando tempo e recursos. Bibliotecas externas são coleções de código desenvolvidas por terceiros que oferecem funcionalidades específicas que podem ser incorporadas em diversos projetos de software. Esse conceito é central para reutilizar código e permite incorporar soluções já existentes e testadas, em vez de criar novas funcionalidades do zero. Biblioteca externa, em sua essência, é um conjunto de funções, métodos ou classes que executam tarefas específicas e podem variar em complexidade, abrangendo desde simples algoritmos de ordenação até complexos frameworks que facilitam o desenvolvimento de aplicações web ou móveis; e seu uso traz uma série de vantagens. Primeiramente, economiza tempo e recursos, já que os desenvolvedores podem se concentrar nas partes únicas de seus projetos em vez de reinventar soluções para problemas comuns. Além disso, como geralmente são bem testadas pela comunidade, elas tendem a ser confiáveis e estáveis, aumentando a qualidade do software desenvolvido. No entanto, bibliotecas externas também requerem uma gestão cuidadosa. É fundamental que os desenvolvedores estejam atentos às versões das bibliotecas usadas, para garantir compatibilidade com o projeto em desenvolvimento; outrossim, segurança e manutenção de bibliotecas são aspectos importantes a considerar. Dependendo da fonte da biblioteca, podem surgir preocupações quanto à segurança do código e à frequência com que a biblioteca é atualizada para corrigir bugs ou vulnerabilidades. Nesse contexto, como o NuGet permite procurar, instalar e gerenciar as dependências de forma eficiente, ele também garante que os desenvolvedores incorporem facilmente funcionalidades externas em seus projetos, mantendo ao mesmo tempo o controle das versões e a compatibilidade dessas bibliotecas com o restante do código. Entre os pacotes mais populares e amplamente utilizados está o Newtonsoft.Json, uma biblioteca para trabalhar com JSON e oferecer funcionalidades como serialização e desserialização de objetos JSON de maneira eficiente. Observação JSON – que significa JavaScript Object Notation – é um formato leve para troca de dados. Amplamente utilizado para transmitir informações entre um servidor e um aplicativo web ou móvel, se tornou um dos formatos mais populares para isso, principalmente devido à sua simplicidade e facilidade de leitura tanto por humanos quanto por máquinas. Sua popularidade decorre da facilidade de uso e da eficácia na manipulação de dados JSON – tarefa comum na maioria dos projetos modernos de desenvolvimento web e móvel. Outro exemplo é o Entity Framework, um object-relational mapper (ORM) que permite trabalhar com bancos de dados usando objetos .NET, simplificando o acesso e a manipulação de dados. A capacidade de mapear classes .NET para tabelas de banco de dados e vice-versa sem precisar escrever consultas 90 Unidade I SQL detalhadas torna o Entity Framework uma ferramenta poderosa para quem trabalha com aplicações orientadas a dados. Para projetos de interface gráfica, bibliotecas como MahApps.Metro ou MaterialDesignInXamlToolkit são populares, pois oferecem conjuntos de controles e estilos que permitem criar interfaces de usuário modernas e atraentes com estilos Metro e Material Design, respectivamente. Elas são essenciais para quem deseja criar aplicações com aparência sofisticada e uma experiência de usuário rica. No âmbito do desenvolvimento web, pacotes como AspNet.Mvc ou AspNetCore.Mvc são cruciais para criar aplicações web, pois fornecem a estrutura para construir aplicações escaláveis e de alto desempenho com o padrão model-view-controller (MVC), facilitando a separação de lógica, interface do usuário e controle. Além disso, para testes e garantia de qualidade, pacotes como xUnit, NUnit ou Moq são amplamente utilizados, porque oferecem frameworks para escrever e executar testes unitários e de integração, bem como ferramentas para simular (mocking) objetos e serviços, garantindo que o software desenvolvido seja testado de maneira abrangente e eficaz. Não é raro confundir gestão de extensões com gestão de dependências e pacotes; ambos desempenham papéis fundamentais e são cruciais para a eficiência e funcionalidade do ambiente de desenvolvimento, mas atendem a necessidades diferentes. O gerenciador de extensões no Visual Studio é projetado para expandir e personalizar o ambiente de desenvolvimento adicionando novas funcionalidades ou melhorias às já existentes. Esse gerenciador permite procurar, instalar e gerenciar extensões, que são essencialmente complementos ou plugins que aprimoram o IDE e podem variar amplamente em sua função, abrangendo desde simples melhorias na interface do usuário até ferramentas complexas para análise de código, suporte a novas linguagens de programação ou integração com outros serviços e ferramentas. A principal função do gerenciador de extensões é, portanto, personalizar e melhorar a experiência de desenvolvimento, oferecendo flexibilidade e adaptabilidade ao ambiente do Visual Studio. Por outro lado, a gestão de dependências e pacotes – particularmente através do NuGet no Visual Studio – lida com a manutenção e o gerenciamentode bibliotecas e componentes de software necessários para um projeto específico funcionar corretamente. Esse processo envolve identificação, instalação, atualização e remoção de pacotes de software – que podem incluir bibliotecas, frameworks e outras ferramentas – necessários para construir e executar um projeto. A gestão eficaz dessas dependências é crucial para garantir integridade, compatibilidade e bom funcionamento do software desenvolvido. Diferente do gerenciador de extensões (que aprimora o IDE em si), a gestão de dependências e pacotes foca a composição e o funcionamento do projeto de software em desenvolvimento. Assim, enquanto o gerenciador de extensões atua mais como personalizador e ampliador do ambiente de desenvolvimento, a gestão de dependências e pacotes é atividade essencial dentro do próprio desenvolvimento de software, garantindo que todos os componentes necessários estejam disponíveis e funcionando harmoniosamente. 91 PROGRAMAÇÃO ORIENTADA A OBJETOS II No ambiente do Visual Studio, o controle de versão integrado, sobretudo o Git, representa uma dimensão vital do desenvolvimento de software. Essa integração não é meramente uma funcionalidade adicional; é fundamental para a gestão eficaz do código-fonte, especialmente num contexto de trabalho colaborativo. O Visual Studio abraça essa necessidade ao incorporá-lo diretamente no IDE, oferecendo uma experiência de controle de versão fluida e acessível, essencial para projetos modernos de software. Git é um sistema de controle de versão distribuído, amplamente utilizado no desenvolvimento de software para rastrear mudanças no código-fonte ao longo do tempo. Desenvolvido por Linus Torvalds, criador do Linux, ele facilita a colaboração entre desenvolvedores, permitindo que múltiplos usuários trabalhem em diferentes aspectos de um projeto simultaneamente, sem interferir uns nos outros. Esse sistema é caracterizado por sua eficiência, velocidade e suporte para desenvolvimento não linear, graças a características como branches e merges. Uma branch no Git é basicamente uma linha independente de desenvolvimento. Os desenvolvedores podem criar branches para trabalhar em novas funcionalidades ou correções, sem afetar a branch principal (geralmente chamada master ou main). Completo e testado o trabalho na branch, ele pode ser integrado de volta à branch principal através de um merge ou rebase. A integração do Git no Visual Studio facilita significativamente o trabalho dos desenvolvedores. Com o Git embutido, os usuários podem realizar a maioria das operações de controle de versão sem sair do IDE, incluindo tarefas básicas – como commit, push, pull e fetch –, assim como operações mais avançadas – como merge e rebase. Essa integração significa que desenvolvedores podem manter o foco no ambiente de desenvolvimento, sem a necessidade de alternar para uma ferramenta de linha de comando ou cliente Git separado. A abordagem unificada economiza tempo e reduz a complexidade do gerenciamento de código. Agora, um pouco sobre os conceitos mencionados: • Um commit no Git é como uma fotografia do estado atual de um projeto. Ele registra o estado atual dos arquivos no repositório, servindo como ponto de referência que pode ser revertido ou comparado com outros estados. Cada commit contém uma mensagem que descreve as mudanças realizadas, facilitando o entendimento do histórico do projeto. • O comando push envia commits locais a um repositório remoto, como GitHub ou Bitbucket. Essa ação atualiza o repositório remoto com mudanças locais, tornando-as acessíveis a outros colaboradores do projeto. • Pull é o processo de atualizar o repositório local com mudanças feitas por outros desenvolvedores no repositório remoto. Quando se faz um pull, o Git automaticamente tenta mesclar as mudanças do repositório remoto com o estado local do código. • Fetch é semelhante ao pull, mas em vez de mesclar automaticamente ele apenas busca as mudanças do repositório remoto e as armazena em uma branch local. Isso permite ao desenvolvedor revisar as mudanças antes de integrá-las ao trabalho atual. 92 Unidade I • Rebase é uma técnica que integra mudanças de uma branch para outra. Diferentemente do merge, que cria um novo commit para unificar duas histórias de commit, o rebase reescreve a história ao aplicar os commits de uma branch sobre outra, resultando em um histórico de projeto mais linear. Uma analogia pode ser feita com o mundo da escrita e publicação de um jornal colaborativo. Imagine que um grupo de jornalistas esteja trabalhando em diferentes artigos para a próxima edição de um jornal. Cada um trabalha em seu próprio artigo, mas todos os artigos precisam ser combinados para formar a edição completa. O ato de commit é semelhante a um jornalista finalizando um rascunho do seu artigo. Esse rascunho é um registro completo do artigo naquele momento, assim como commit é o registro de estado do código-fonte. O comando push é como enviar o rascunho finalizado do artigo para a redação do jornal, onde ele será revisado e eventualmente publicado. Similarmente, no Git, push envia as mudanças locais para um repositório remoto. Por outro lado, pull é como um jornalista recebendo as últimas versões dos artigos de seus colegas para revisar ou referenciar. No Git, pull atualiza o repositório local com as mudanças feitas no repositório remoto. Fetch é como receber as últimas versões dos artigos sem imediatamente começar a trabalhar nelas; é uma maneira de estar ciente das contribuições dos outros sem mesclá-las com o próprio trabalho. Rebase pode ser visto como um jornalista reescrevendo seu artigo para incorporar informações atualizadas ou para se alinhar melhor com os outros artigos; no Git, rebase reestrutura a série de commits para tornar o histórico mais limpo e organizado. Branch é similar a um jornalista trabalhando em um artigo especial ou uma série separada que não faz parte da edição principal; ele pode trabalhar de forma independente sem afetar o conteúdo principal. Finalmente, merge é como combinar os artigos de vários jornalistas em uma única edição. No Git, é o processo de combinar as mudanças de diferentes branches em uma única linha de desenvolvimento. A analogia do jornal colaborativo ajuda a entender como o Git facilita a colaboração e o gerenciamento de várias linhas de trabalho em desenvolvimento de software, garantindo que todas as contribuições sejam integradas de maneira organizada e eficiente para o produto final. Além disso, o Visual Studio oferece ferramentas visuais e intuitivas para a colaboração e o gerenciamento de branches. Essas ferramentas permitem que os desenvolvedores visualizem a estrutura de branches de seu projeto, facilitando a compreensão do fluxo de trabalho e a gestão de múltiplas linhas de desenvolvimento. Por exemplo, desenvolvedores podem facilmente alternar entre diferentes branches, criar modelos novos para experimentações ou funcionalidades específicas e mesclar mudanças de um branch para outro. Essa capacidade de gerenciá-los de forma eficiente é crucial em ambientes de desenvolvimento ágil, onde o trabalho em várias funcionalidades ou correções pode ocorrer simultaneamente. Também podemos destacar, no ambiente do Visual Studio, as ferramentas de análise de código e refatoração, que desempenham papel crucial ao contribuir significativamente com a qualidade e 93 PROGRAMAÇÃO ORIENTADA A OBJETOS II manutenção do software. Elas não são apenas complementos úteis; são essenciais para identificar problemas em potencial no código e torná-lo mais limpo, eficiente e sustentável ao longo do tempo. Refatoração é o processo de modificar o código-fonte de um software sem alterar seu comportamento funcional externo, com o objetivo de melhorar estrutura interna, legibilidade, eficiência ou manutenção do código. Essa prática é parte fundamental do desenvolvimento de software, pois ajuda a manter o código limpo, organizado e fácil de entender, o que é crucialpara sua manutenção e expansão. Na figura 39, um exemplo de refatoração manual no nosso jogo. 1. public void ExecutarJogo() 2. { 3. //... código existente... 4. foreach (Jogador jogador in jogadores) 5. { 6. // Lógica existente para jogar turno 7. 8. if (VerificarEAnunciarVencedor()) 9. { 10. jogoTerminado = true; 11. break; 12. } 13. } 14. //... código existente... 15. } 16. private bool VerificarEAnunciarVencedor() 17. { 18. // Implementação do método para verificar se o jogo terminou e anunciar o vencedor 19. //... código existente... 20. } Figura 39 – Refatoração do método ExecutarJogo da classe Jogo Inicialmente, a classe Jogo parece sobrecarregada de responsabilidades, desde a gestão de jogadores até o controle do fluxo do jogo. Podemos começar refatorando essa classe para melhor organizar suas responsabilidades e melhorar a clareza do código. Primeiramente, a lógica dentro do método ExecutarJogo pode ser simplificada e dividida em métodos menores; por exemplo, o código que verifica se o jogo terminou e anuncia o vencedor pode ser movido para um método separado chamado VerificarEAnunciarVencedor. Isso não só torna o método ExecutarJogo mais conciso, mas também torna o código mais fácil de entender e manter. 94 Unidade I Em seguida, na figura 40, a lógica de processamento de cada casa no método ProcessarCasa pode ser extraída para métodos separados. Por exemplo, o processamento do duelo de honra pode ser extraído para um novo método ProcessarDueloDeHonra, simplificando o ProcessarCasa e tornando o código mais modular. 1. private void ProcessarCasa(Casa casa, Jogador jogador) 2. { 3. //... código existente... 4. 5. switch (casa.Tipo) 6. { 7. case TipoCasa.DueloDeHonra: 8. ProcessarDueloDeHonra(jogador); 9. break; 10. //... outros casos... 11. } 12. } 13. 14. private void ProcessarDueloDeHonra(Jogador jogador) 15. { 16. // Lógica para processar um duelo de honra 17. //... código existente... 18. } Figura 40 – Refatoração do método ProcessarCasa 1. public void AtualizarTurno(int numeroTotalDeCasas) 2. { 3. //... código existente para verificar pausa... 4. int movimento = RolarDado(); 5. MoverJogador(movimento, numeroTotalDeCasas); 6. } 7. private void MoverJogador(int movimento, int numeroTotalDeCasas) 8. { 9. // Lógica para mover o jogador 10. //... código existente... 11. } Figura 41 – Refatoração do método AtualizarTurno 95 PROGRAMAÇÃO ORIENTADA A OBJETOS II Além disso, a classe Jogador pode ser refatorada para encapsular melhor suas responsabilidades. Na figura 41, por exemplo, o método AtualizarTurno pode ser dividido em métodos menores, como RolarDado e MoverJogador, melhorando a clareza e a manutenção do código – essas são apenas algumas das muitas maneiras possíveis de refatorar o código. O objetivo é sempre torná-lo mais limpo e mais fácil de entender e de manter, sem alterar o comportamento funcional do programa. As ferramentas de refatoração automática no Visual Studio são instrumentos poderosos para melhorar a estrutura do código sem alterar seu comportamento funcional, envolvendo mudanças como renomear variáveis, métodos e classes para nomes mais descritivos, dividir métodos ou classes grandes em unidades menores e mais gerenciáveis, e remover código duplicado. Essas ações, embora pareçam pequenas, têm impacto significativo na legibilidade e manutenção do código. A refatoração automática no Visual Studio torna o processo mais eficiente e permite que desenvolvedores apliquem essas mudanças com poucos cliques, garantindo ao mesmo tempo que as alterações sejam consistentes em todo o projeto. Já a análise estática de código no Visual Studio é um processo automatizado que examina o código-fonte antes de ser executado. Essa análise busca padrões comuns que podem indicar erros, como uso incorreto de sintaxe, potenciais vazamentos de memória, referências nulas e padrões de codificação inseguros ou ineficientes. O que torna a análise estática tão valiosa é sua capacidade de identificar problemas que podem não ser óbvios durante testes ou mesmo na execução normal do programa. Ao destacar essas questões cedo no ciclo de desenvolvimento, ela ajuda a prevenir bugs que poderiam ser custosos ou problemáticos para corrigir mais tarde. 1.4 Introdução ao .NET Framework e .NET Core Plataforma de desenvolvimento de software criada pela Microsoft, .NET Framework representa um marco significativo na história da programação. Desde seu lançamento no início dos anos 2000, evoluiu de um sistema apenas para Windows para uma plataforma versátil, capaz de suportar uma ampla gama de aplicações em diferentes sistemas operacionais. Framework, no desenvolvimento de software, é uma estrutura ou plataforma pré-concebida que serve como base para desenvolvê-lo e organizá-lo, fornecendo uma fundação e um conjunto de diretrizes para os desenvolvedores poderem construir e gerenciar aplicações de software de forma mais eficiente e padronizada. Pode incluir bibliotecas de códigos, compiladores, interfaces de programação de aplicações (APIs) e outras ferramentas que, juntas, facilitam o processo. Seu principal propósito é fornecer uma estrutura genérica e reutilizável, que permita aos desenvolvedores focar partes específicas de suas aplicações em vez de recriar do zero componentes e estruturas comuns. Isso economiza tempo e esforço e ajuda a garantir que a aplicação seja construída com práticas de codificação consistentes e eficientes. Além disso, como muitos frameworks são amplamente usados e testados, eles tendem a ser confiáveis e robustos. 96 Unidade I Frameworks existem para uma variedade de linguagens de programação e tipos de aplicação. Por exemplo, no desenvolvimento web, frameworks como Ruby on Rails, Django (Python) ou Angular (JavaScript) oferecem estruturas predefinidas para criar aplicações web. Em desenvolvimento móvel, frameworks como React Native ou Flutter permitem desenvolver aplicações para iOS e Android usando uma base de código comum. Para o desenvolvimento de software geral, .NET Framework e Java Framework são exemplos de sistemas abrangentes que fornecem uma ampla gama de funcionalidades. A escolha de um framework apropriado depende de vários fatores, incluindo requisitos específicos do projeto, linguagem de programação preferida, familiaridade da equipe de desenvolvimento com o framework, e comunidade e suporte disponíveis para ele. Utilizá-lo é uma maneira de garantir que as aplicações sejam desenvolvidas de forma mais rápida, segura e com um padrão de qualidade mais elevado. A jornada do .NET Framework começou como resposta da Microsoft à crescente necessidade de uma plataforma unificada, que simplificasse a criação de aplicações para internet e desktop. Inicialmente, o .NET Framework foi projetado para oferecer um ambiente gerenciado, conhecido como Common Language Runtime (CLR), que executa o código de forma segura e eficiente. Ele permitiu a execução de um código intermediário chamado Microsoft Intermediate Language (MSIL), que é independente da plataforma e da linguagem, e possibilitou o desenvolvimento em várias linguagens, como C#, VB.NET e F#. Outro componente essencial do .NET Framework é a extensa biblioteca de classes, conhecida como Framework Class Library (FCL). Ela fornece uma vasta gama de funcionalidades prontas para uso, abrangendo desde operações básicas de entrada e saída até interfaces gráficas complexas de usuário e acesso a bancos de dados. Também ajudou a acelerar significativamente o processo, permitindo que programadores se concentrassem na lógica específica da aplicação sem se preocupar com a implementação de baixo nível de muitas funcionalidades comuns. Ao longo dos anos, o .NET Framework foi se expandindo e se adaptando às mudanças nas demandas e tecnologias do mercado. Uma das evoluções mais notáveis foi a introdução do ASP.NET, plataforma para desenvolver aplicações web que permitiu criarsites dinâmicos e aplicações robustas, trazendo consigo tecnologias como Web Forms, MVC e Web API, cada uma atendendo a diferentes estilos e necessidades. A arquitetura do .NET Framework também se destacou pela sua interoperabilidade, com diferentes linguagens de programação e sistemas. A Microsoft promoveu uma estratégia de inclusão e permitiu que o .NET interagisse com outras plataformas e linguagens – passo significativo para adotar a plataforma numa variedade mais ampla de ambientes de desenvolvimento. Apesar do sucesso, o .NET Framework começou a enfrentar limitações, especialmente no que diz respeito à compatibilidade com sistemas que não eram Windows, levando ao desenvolvimento do .NET Core, uma versão mais moderna e multiplataforma do .NET, que oferece maior flexibilidade, desempenho aprimorado e suporte a containeres; uma evolução do .NET num mundo cada vez mais diversificado do desenvolvimento de software. 97 PROGRAMAÇÃO ORIENTADA A OBJETOS II A introdução do .NET Core representou um momento significativo na evolução da plataforma .NET, marcando uma mudança na forma como a Microsoft abordava o desenvolvimento de software. O .NET Core é uma versão reformulada do .NET Framework, projetada para atender às demandas emergentes de um ambiente de desenvolvimento moderno. Com o .NET Core, a Microsoft buscou criar uma plataforma mais modular, eficiente e, acima de tudo, multiplataforma, capaz de rodar em Windows, Linux e macOS. Uma das principais diferenças entre o .NET Core e o .NET Framework é a capacidade multiplataforma. Enquanto o .NET Framework foi construído primariamente para aplicações Windows, o .NET Core foi projetado desde o início para ser executado em várias plataformas –característica que abriu novas possibilidades para desenvolvedores que queriam ou precisavam executar suas aplicações em ambientes diferentes de Windows, uma necessidade cada vez mais comum no cenário tecnológico diversificado de hoje. Além disso, o .NET Core apresenta uma arquitetura modular, que permite aos desenvolvedores incluir apenas as partes do .NET de que eles precisam em suas aplicações. Isso se traduz em aplicações mais leves e com tempos de carregamento mais rápidos – vantagem significativa especialmente para aplicações web e serviços em nuvem. Essa modularidade é possível graças à introdução do NuGet como meio de entregar bibliotecas e componentes, permitindo que desenvolvedores personalizem projetos com maior precisão e flexibilidade. Desempenho também é um ponto de diferenciação. O .NET Core foi otimizado para oferecer maior desempenho em várias métricas, incluindo velocidade de inicialização da aplicação e uso de recursos. Essa melhoria no desempenho é consequência tanto da arquitetura modular quanto de várias outras otimizações no núcleo da plataforma. O .NET Core também elevou a plataforma .NET a um novo patamar de versatilidade. O suporte ao Linux e macOS, além do Windows, abriu caminho para desenvolver e hospedar aplicações .NET numa variedade mais ampla de ambientes. Isso é particularmente relevante no contexto de containeres e microsserviços, em que o .NET Core se encaixa perfeitamente com tecnologias como Docker e Kubernetes, o que facilita a criação de aplicações escaláveis e eficientes na nuvem. CLR é um componente fundamental do ecossistema .NET da Microsoft e desempenha papel crítico na execução de aplicativos desenvolvidos nele. Em resumo, é a máquina virtual que gerencia a execução de programas .NET e proporciona uma série de serviços importantes, incluindo gerenciamento de memória, segurança e manipulação de exceções. Uma das principais características do CLR é a execução de código gerenciado, que oferece vários benefícios em termos de produtividade do desenvolvedor, segurança e estabilidade da aplicação. O conceito de código gerenciado é central para o CLR; trata-se do código executado sob a supervisão do CLR, em oposição ao código não gerenciado, executado diretamente pelo sistema operacional. Quando um programa .NET é compilado, ele é convertido em MSIL. Ao executar um aplicativo .NET, o CLR converte o MSIL em código de máquina nativo usando um compilador just in time (JIT) – processo que não 98 Unidade I apenas garante ao código ser otimizado para a máquina específica em que está sendo executado, mas também permite que o CLR imponha políticas de segurança e lide com exceções de maneira mais eficaz. Um dos aspectos mais notáveis do CLR é o gerenciamento de memória, particularmente a coleta de lixo (ou garbage collection). Garbage collection automatizada é uma característica-chave do código gerenciado, pois libera os desenvolvedores da tarefa de gerenciar manualmente a memória. No modelo de garbage collection do CLR, o alocador de memória mantém o controle das referências de objeto e periodicamente percorre a memória para identificar objetos não mais utilizados pelo programa. Esses objetos são então coletados, e a memória que eles ocupavam é liberada, num processo que reduz significativamente a probabilidade de vazamentos de memória e erros a ela relacionados, comuns em ambientes onde ela deve ser gerenciada manualmente. Além disso, o CLR oferece outros serviços de gerenciamento de memória, como a compactação de heap – que ajuda a evitar a fragmentação da memória – e a geração de coletas de lixo – que categoriza objetos com base na sua longevidade na memória. Objetos usados por pouco tempo são coletados com mais frequência que os mais longevos, otimizando assim o desempenho do garbage collector. Trabalhar com bibliotecas de classe é uma prática essencial no desenvolvimento de software, especialmente em ambientes .NET, onde modularidade e reutilização são fortemente encorajadas. Uma biblioteca de classes em .NET é uma coleção de classes, interfaces e outros tipos que fornecem uma variedade de funcionalidades, desde operações básicas de entrada/saída até complexas interações com sistemas de banco de dados. O uso eficaz dessas bibliotecas, com a organização adequada através de namespaces, é fundamental para criar aplicações robustas e manuteníveis. Namespaces-padrão em .NET organizam e agrupam classes relacionadas, proporcionando uma maneira de controlar o escopo dos nomes em grandes projetos de software. Por exemplo, o namespace System é um dos principais namespaces do .NET Framework, contendo classes essenciais para o funcionamento de qualquer aplicação .NET. Dentro do System há sub-namespaces como o System.Collections – que fornece classes para gerenciar coleções de objetos – e o System.IO – que contém classes para leitura e escrita de arquivos e fluxos de dados. Esses namespaces ajudam os desenvolvedores a encontrar rapidamente classes necessárias e entendê-las dentro do contexto maior da biblioteca. Além de utilizar namespaces-padrão e bibliotecas de classes fornecidas pelo .NET Framework ou .NET Core, os desenvolvedores frequentemente criam suas próprias bibliotecas de classes reutilizáveis. Essas bibliotecas personalizadas permitem aos desenvolvedores encapsular funcionalidades específicas que podem ser reutilizadas em diferentes projetos, aumentando a eficiência e a consistência em todo o desenvolvimento de software. Para criar uma biblioteca de classes reutilizável, os desenvolvedores devem se concentrar na criação de código que seja genérico o suficiente para ser aplicado em diferentes contextos, mas também suficientemente robusto e bem documentado para garantir sua funcionalidade e facilidade de uso. Um aspecto importante na criação de bibliotecas de classes reutilizáveis é o cuidado com a definição clara das interfaces e com a abstração apropriada. Isso significa que classes e métodos devem ter interfaces bem definidas que ocultem detalhes internos e complexidades, o que torna a biblioteca mais fácil de 99 PROGRAMAÇÃO ORIENTADA A OBJETOS II usar e facilita a manutenção e atualizações futuras, já que mudanças internas podem ocorrer sem afetar os usuários da biblioteca. Tambémé crucial considerar as dependências. Bibliotecas eficazes devem minimizar suas dependências externas para reduzir o risco de conflitos e problemas de compatibilidade em projetos que as utilizem. Quando as dependências são necessárias, é importante gerenciá-las cuidadosamente e documentá-las claramente para os usuários. Trabalhar com bibliotecas de classes no .NET, seja com bibliotecas_padrão, seja criando outras personalizadas, é parte integral do desenvolvimento de software eficiente e sustentável. O uso de namespaces para organizar e gerenciar classes, com a criação de bibliotecas reutilizáveis, ajuda a construir um código mais limpo, mais modular e mais fácil de manter, contribuindo para a qualidade geral e a longevidade das aplicações de software. Desenvolvimento multiplataforma é uma abordagem crucial na era atual da tecnologia, com aplicações e serviços que precisam ser eficientes numa variedade de dispositivos e sistemas operacionais. Essa necessidade decorre da diversidade de dispositivos utilizados, incluindo smartphones, tablets, desktops e até mesmo wearables, cada um operando em diferentes plataformas, como Windows, macOS, iOS, Android e Linux. Para atender a essa demanda, desenvolvedores adotam estratégias e ferramentas específicas que permitem criar aplicações executáveis em múltiplas plataformas com pouco ou nenhum ajuste no código. Uma estratégia comum para o desenvolvimento multiplataforma são os frameworks, que suportam a escrita de código uma vez e sua execução em várias plataformas. Eles oferecem bibliotecas e APIs que abstraem as diferenças entre plataformas subjacentes, permitindo que desenvolvedores escrevam código que é, em grande parte, independente da plataforma. Isso economiza tempo e recursos e garante consistência na funcionalidade e na experiência do usuário em diferentes dispositivos. Dentre as ferramentas e frameworks que suportam o desenvolvimento multiplataforma, o Xamarin é um exemplo notável. Integrado ao ecossistema .NET da Microsoft, ele permite que os desenvolvedores usem C# para criar aplicações para iOS, Android e Windows, oferecendo acesso completo às APIs nativas de cada plataforma e permitindo que aplicações pareçam e funcionem como nativas em cada dispositivo. O Xamarin.Forms (extensão do Xamarin) vai além e permite que desenvolvedores criem interfaces de usuário compartilhadas em todas as plataformas, reduzindo ainda mais a necessidade de código específico de plataforma. Outra abordagem popular é o desenvolvimento de aplicações web progressivas (progressive web apps – PWAs), que usam tecnologias web modernas para oferecer uma experiência próxima à de um aplicativo nativo em navegadores de desktop e móveis. PWAs são responsivas, funcionam offline, são acessíveis através de um URL e podem ser adicionadas à tela inicial de dispositivos móveis, oferecendo uma experiência de usuário aprimorada em comparação com sites tradicionais. Além disso, plataformas como React Native, desenvolvida pelo Facebook, permitem que desenvolvedores usem JavaScript e React para construir aplicações nativas para iOS e Android. O React Native se concentra em oferecer uma experiência de usuário nativa, permitindo aos desenvolvedores acessar funcionalidades específicas do dispositivo, como câmera e localização, enquanto compartilham a lógica de negócios entre plataformas. 100 Unidade I Para aplicações que exigem alto desempenho – como jogos e softwares gráficos intensivos –, tecnologias como Unity são frequentemente utilizadas. Unity é um motor de jogo poderoso que suporta desenvolvimento multiplataforma e permite que desenvolvedores criem jogos e experiências interativas executáveis em quase qualquer dispositivo. O desenvolvimento multiplataforma, portanto, é facilitado por uma variedade de estratégias e ferramentas, cada uma adequada para diferentes tipos de projetos e requisitos. Seja através de frameworks como Xamarin para aplicações que se assemelham a nativas, PWAs para aplicações web acessíveis, React Native para uma abordagem baseada em JavaScript, ou Unity para projetos de jogos, os desenvolvedores têm à disposição um leque de opções para criar soluções eficazes que atendam a usuários em diversas plataformas. Esse ambiente diversificado de desenvolvimento multiplataforma é um testemunho da contínua inovação e adaptação no campo do desenvolvimento de software, respondendo às demandas de um mundo digital cada vez mais conectado e diversificado. 2 DESENVOLVIMENTO DA PRIMEIRA INTERFACE GRÁFICA O desenvolvimento de interfaces gráficas é uma área fundamental na criação de software e foca a construção de elementos visuais com os quais os usuários finais podem interagir diretamente. No contexto do Windows, uma das plataformas de sistemas operacionais mais utilizadas, desenvolver interfaces gráficas envolve compreender e aplicar tecnologias e ferramentas específicas para criar aplicativos não apenas funcionalmente ricos, mas também intuitivos e agradáveis. No cerne do desenvolvimento de interfaces gráficas está a ideia de que os usuários devem ser capazes de interagir com o software de maneira natural e fácil. Isso geralmente envolve a criação de janelas, menus, caixas de diálogo, botões, caixas de texto e outros elementos gráficos familiares aos usuários, que lhes permitam ações como inserir dados, selecionar opções, navegar por diferentes seções do software e receber feedback visual do sistema. A Microsoft fornece várias tecnologias e frameworks para apoiar o desenvolvimento de interfaces gráficas no Windows. Por exemplo, Windows Forms, parte do .NET Framework, é uma das ferramentas mais antigas e testadas para criar interfaces gráficas do usuário (GUIs) tradicionais, sendo conhecido por sua simplicidade e eficiência, e por permitir aos desenvolvedores arrastar e soltar elementos de interface gráfica em um ambiente de design visual. Um aspecto crucial no desenvolvimento de interfaces gráficas é a experiência do usuário (user experience – UX). Isso implica não apenas a aparência estética da interface, mas também a facilidade de uso, acessibilidade e intuitividade. Desenvolvedores devem considerar cuidadosamente como os usuários interagem com a aplicação, antecipar necessidades e comportamentos e projetar interfaces que facilitem essas interações. Usabilidade torna-se prioridade para garantir que o software seja acessível a uma ampla gama de usuários, incluindo aqueles com deficiências. Complementarmente, a interação humano-computador (IHC) envolve aspectos como estudo, planejamento e design de interação entre usuários e computadores. Neste tópico abordaremos o desenvolvimento de interfaces com Windows Forms e aspectos de IHC e UX. 101 PROGRAMAÇÃO ORIENTADA A OBJETOS II 2.1 Windows Forms Windows Forms, parte integral da plataforma .NET fornecida pela Microsoft, é uma tecnologia usada para criar GUIs baseadas em Windows. Desenvolvida especificamente para o ambiente do Windows, Windows Forms oferece uma ampla gama de controles, como botões, caixas de texto e grades de dados – essenciais para construir interfaces de usuário interativas e amigáveis. Esses controles são objetos que podem ser arrastados e soltos em um formulário (basicamente uma janela ou diálogo), fornecendo uma maneira intuitiva de projetar GUIs. O desenvolvimento com Windows Forms em C# é facilitado pelo uso do Visual Studio. Esse IDE fornece ferramentas e recursos como um designer de formulários, que permite aos desenvolvedores arrastar e soltar controles em um formulário, configurar suas propriedades e escrever código que define o comportamento do aplicativo. A abordagem visual do Visual Studio torna o design de uma GUI mais simples e mais eficiente, mesmo para aqueles com conhecimento limitado de programação. No coração do Windows Forms está o mecanismo de manipulação de eventos. Cada controle em um formulário Windows Forms é capaz de gerar eventos; por exemplo, um botão gerará um evento de clique quando um usuário clicarda unidade II serem efetivamente empregados. Desenvolvimento em camadas é uma abordagem que separa responsabilidades dentro de uma aplicação, tornando o código mais modular, fácil de gerenciar e escalável. O MVC, por outro lado, é uma abordagem de design arquitetônico que separa a aplicação em três componentes principais – modelo, visualização e controlador –, facilitando a manutenção do código, a escalabilidade da aplicação e promovendo seu reúso. No tópico 5 detalharemos o desenvolvimento em camadas e do MVC, além de trabalhar com o WPF, que é similar ao Windows Forms mas oferece recursos avançados, como gráficos 2D/3D e animações, com marcação XAML para design mais declarativo. O WinForms é indicado para aplicativos simples, enquanto o WPF é preferível para interfaces altamente personalizadas e escaláveis. Pelo mesmo motivo, falaremos do padrão model-view-viewmodel (MVVM) e finalizaremos com um projeto de aplicação prática. A realidade cada vez mais interconectada faz a computação em nuvem apresentar novos paradigmas. A habilidade de conectar-se com bancos de dados num ambiente de nuvem permite que os sistemas sejam escaláveis, resilientes e disponíveis globalmente. Desenvolvedores devem compreender esses conceitos e a complexidade subjacente das operações de nuvem, garantindo que desenvolvam sistemas que aproveitem ao máximo suas capacidades. No tópico 6 detalharemos o uso de strings de conexão, especialmente com SQLClient; também abordaremos o .NET Framework com seus diversos provedores de dados, como OLE DB e ODBC – variedade que oferece uma perspectiva ampla das ferramentas à disposição e nos prepara para selecionar a tecnologia mais apropriada para diferentes cenários de desenvolvimento. Parte do conteúdo é dedicada à arquitetura do ADO.NET, parte central da plataforma .NET. Elementos como DataSet, DataTable, DataView, DataRow e DataColumn são explorados em detalhe. Compreendendo-os em profundidade, futuros desenvolvedores serão capacitados para realizar operações complexas de bancos de dados com confiança e eficiência. Nos dias atuais é praticamente impossível pensar uma aplicação sem conexão com algum banco de dados, o que demonstra a importância de dominar a técnica de conectar artefatos de software com sistemas de gerenciamento de bancos de dados (SGBDs). Assim, faremos um projeto prático de conexão de um aplicativo desktop com um servidor SQL Server da Microsoft. 10 Indo além dos sistemas convencionais, o desenvolvimento cross-platform e a mobilidade – particularmente com ferramentas como Xamarin ou Microsoft’s Multi-platform App UI (MAUI) – revolucionaram a construção de softwares (a unidade IV trata disso). Essas ferramentas permitem aos desenvolvedores escrever um código-base que pode ser executado em várias plataformas, como Android, iOS e Windows, otimizando tempo e recursos. O tópico 7 aborda recursos assíncronos, como async e await, vitais para construir aplicações responsivas, permitindo que operações demoradas – como leitura de arquivos ou consultas a bancos de dados – sejam realizadas em segundo plano, mantendo a interface do usuário ágil e responsiva. Além disso, a emergência do domain-driven design (DDD) trouxe uma perspectiva renovada ao desenvolvimento de software, enfatizando a modelagem baseada no domínio do problema e buscando estabelecer uma linguagem ubíqua e bem definida entre desenvolvedores e stakeholders. O tópico também explica o desenvolvimento de aplicações seguras. Na vanguarda do desenvolvimento moderno, microsserviços em C# são uma abordagem arquitetônica que decompõe aplicações em pequenos serviços que funcionam de forma independente, o que resulta em sistemas mais modulares e escaláveis. Complementarmente, arquiteturas baseadas em eventos com C# oferecem uma maneira de criar sistemas reativos que respondem em tempo real a estímulos, tornando as aplicações mais dinâmicas e resilientes (abordaremos esses assuntos, bem como o desenvolvimento cross-plataform e mobilidade, no tópico 8). Apresentaremos também o Xamarin, plataforma baseada em C# que permite criar aplicativos nativos para Android, iOS e Windows com uma única base de código. Se usarmos C# junto com Xamarin, aproveitamos o poder da linguagem e as facilidades da plataforma. De modo resumido, a unidade I trata das bases para a programação e do ambiente à disposição do programador para seu trabalho. Já a unidade II aborda o desenvolvimento de interfaces gráficas para desktop usando Windows Forms, e há um projeto prático para que o conhecimento técnico de programação seja aplicado à luz das melhores práticas de IHC e UX. A unidade III aprofunda conceitos fundamentais de arquitetura no desenvolvimento de aplicações de maior porte, por isso o projeto da unidade anterior será reformulado e expandido. Também analisa a integração da aplicação com o banco de dados, tarefa imprescindível em qualquer sistema (dada a relevância do tema, desenvolveremos um projeto prático de aplicação conectado a um servidor de banco de dados). Finalmente, a unidade IV trata de aspectos do desenvolvimento de aplicações seguras (incluindo autenticação e autorização) e de cross-plataform e mobile. 11 PROGRAMAÇÃO ORIENTADA A OBJETOS II Unidade I 1 REVISÃO DE CONCEITOS DE POO E INTRODUÇÃO AO AMBIENTE C# Ao adentrar no mundo da POO em C#, especialmente no desenvolvimento de aplicativos para desktop e mobile que se conectam a bancos de dados integrados, nos deparamos com uma série de conceitos fundamentais que se entrelaçam e tornam essa abordagem muito poderosa; portanto dominar os conceitos de classes e objetos é imprescindível. Classe é como um blueprint que define um tipo de objeto, determinando quais operações podem ser realizadas com ele. Quando criamos uma instância dessa classe, temos o que chamamos de objeto. Essa distinção é vital pois, enquanto a classe é como um molde, o objeto é a concretização desse molde; daí surge a ideia de encapsulamento, que se refere a como escondemos detalhes internos de uma classe e expomos apenas o que é estritamente necessário para o mundo exterior. Isso não apenas protege a integridade dos dados, mas também torna nosso código mais modular e reutilizável. Não podemos deixar de mencionar a herança, conceito que permite construir uma nova classe com base em uma já existente. Isso significa que podemos herdar propriedades e comportamentos, permitindo reutilizar o código. Ao lado da herança temos o polimorfismo, uma maneira de tratar diferentes objetos como se fossem do mesmo tipo; seria como ter vários formatos de peça, mas todas se encaixam na mesma moldura. Abstração é outro pilar fundamental da POO. Criando classes abstratas e interfaces, podemos definir uma espécie de contrato, o que nos dá uma estrutura para trabalhar, sem necessariamente entrar em detalhes de implementação específicos de cada classe. Conforme exploramos as relações entre classes, encontramos conceitos como associação, agregação e composição. Imagine duas classes, uma representando uma conexão com o banco de dados e outra representando uma consulta. O relacionamento entre essas duas classes pode ser definido usando esses conceitos, dependendo da natureza da relação. Além das relações entre classes, dentro de uma mesma classe temos métodos – ou seja, ações que um objeto pode executar – e propriedades – que permitem acessar e definir valores. Atributos são outra característica interessante, pois fornecem uma maneira de adicionar metadados (ou informações adicionais) ao código. Isso pode influenciar como o código é executado ou fornecer informações valiosas em tempo de execução. Além disso, propriedades automáticas, inicializadores de objetos e inicializadores de coleção simplificam a criação e inicialização de objetos e coleções. Por exemplo, em vez de escrever várias linhas de código para configurar um novo objeto, podemos fazer tudo isso numa única linha. No que diz respeito à manipulação de dados, coleções genéricas e classesnele. Os desenvolvedores escrevem código em C# para responder a esses eventos, definindo a lógica do aplicativo, o que permite criar aplicativos interativos nos quais as ações do usuário no frontend desencadeiam processos específicos de backend. Outra característica notável do Windows Forms é sua flexibilidade e extensibilidade. Desenvolvedores podem não apenas usar os controles existentes, mas também criar seus próprios controles personalizados. Isso é útil quando os requisitos de um aplicativo são únicos e os controles-padrão podem não ser suficientes. Além disso, o Windows Forms suporta o uso de recursos gráficos avançados, como gráficos e animações, que podem ser integrados para melhorar a experiência do usuário. Apesar de suas muitas vantagens, é importante notar que o Windows Forms é adequado principalmente para aplicativos que serão executados em ambientes Windows. Com o surgimento de tecnologias como Windows Presentation Foundation (WPF) e Universal Windows Platform (UWP), que oferecem mais recursos e uma melhor integração com os sistemas Windows modernos, o Windows Forms pode parecer menos atraente para projetos que exigem interfaces mais ricas e dinâmicas. No entanto, para projetos que visam um desenvolvimento rápido e uma curva de aprendizado menos íngreme (especialmente para quem está familiarizado com o paradigma tradicional do Windows), o Windows Forms continua a ser uma escolha viável e robusta. Com ele os desenvolvedores podem criar aplicativos eficientes para Windows com interfaces ricas e personalizáveis enquanto aproveitam o suporte e os recursos fornecidos pela plataforma .NET e pelo ambiente de desenvolvimento integrado Visual Studio. A capacidade de lidar facilmente com eventos, a integração com outras tecnologias do .NET Framework e a possibilidade de criar controles personalizados tornam o Windows Forms uma ferramenta poderosa para desenvolver aplicativos Windows tradicionais. Criar um formulário no Windows Forms do C# usando o Visual Studio Community é um processo que envolve várias etapas inter-relacionadas, começando com a configuração do projeto e avançando 102 Unidade I para o design e a codificação da interface do usuário. Primeiramente, o desenvolvedor deve iniciar o Visual Studio Community e criar um novo projeto. Ao fazer isso, ele seleciona a opção “Windows Forms App” na lista de modelos de projeto, o que lhe dá uma base para começar a desenvolver uma aplicação GUI em C#. Ao se deparar com as opções “Windows Forms App” e “Windows Forms App (.NET Framework)” no Visual Studio, a escolha entre elas depende das necessidades específicas do seu projeto e do ambiente de destino para o aplicativo. A primeira opção geralmente se refere a um projeto Windows Forms que utiliza o .NET Core ou o .NET 5/6 e é recomendada se estivermos iniciando um novo projeto e desejamos aproveitar os benefícios das atualizações mais recentes, melhor suporte a longo prazo e capacidade de executar o aplicativo em diferentes sistemas operacionais (embora a GUI do Windows Forms seja específica para Windows, o núcleo do aplicativo pode ser compartilhado entre plataformas). Já a segunda opção é a que usaremos nos exemplos deste tópico e é indicada para projetos que utilizam o .NET Framework tradicional, uma plataforma que tem sido a base para muitos aplicativos Windows ao longo dos anos. Podemos escolhê-la se estivermos trabalhando em um projeto que precisa ser compatível com versões mais antigas do Windows, ou se estivermos mantendo ou atualizando um aplicativo existente já construído com o .NET Framework. Embora o .NET Framework ainda seja suportado, ele não recebe mais novos recursos, e a Microsoft está direcionando os desenvolvedores ao .NET Core e suas versões subsequentes para novos desenvolvimentos. Criado o projeto, o Visual Studio abre um formulário em branco, conhecido como Form1 por padrão. É a tela principal do aplicativo, e é aqui que os elementos da interface do usuário – como botões, caixas de texto e etiquetas – serão adicionados. O desenvolvedor pode alterar o tamanho e as propriedades do formulário ajustando-as no devido painel, que oferece opções para modificar aspectos como título da janela, dimensões e comportamento de minimização/máxima. O próximo passo é arrastar e soltar os controles desejados do Toolbox para o formulário, que contém uma variedade de controles como botões, caixas de texto, rótulos (labels), caixas de combinação (comboboxes) e muitos outros. Cada controle pode ser colocado e redimensionado no formulário conforme necessário. Colocado um controle, o desenvolvedor pode ajustar suas propriedades usando novamente o painel de propriedades. Por exemplo, ele pode alterar o texto de um botão, ajustar a fonte de um rótulo ou configurar a lista de itens em uma caixa de combinação. Com os controles no lugar, o passo seguinte é escrever o código que define o comportamento do aplicativo. O desenvolvedor pode adicionar funcionalidades aos controles programando manipuladores de eventos; por exemplo, ao dar um duplo clique em um botão no designer, o Visual Studio automaticamente cria um manipulador de eventos de clique para esse botão. No editor de código que se abre, o desenvolvedor pode então escrever o código C# que será executado quando o botão for clicado. O processo de adicionar controles ao formulário, ajustar suas propriedades e programar seus eventos se repete até que o desenvolvedor tenha construído a interface do usuário conforme desejado. O Visual Studio Community oferece também ferramentas para depurar e testar o aplicativo, e o desenvolvedor 103 PROGRAMAÇÃO ORIENTADA A OBJETOS II pode executá-lo dentro do ambiente do Visual Studio para ver como a interface do usuário se comporta e interagir com ela em tempo real. Isso permite que ele identifique e corrija quaisquer problemas ou bugs antes de finalizá-lo. Ao longo do processo, o desenvolvedor tem flexibilidade para retornar ao designer a qualquer momento para fazer ajustes na interface do usuário, adicionar novos controles ou modificar os existentes. Esse ciclo iterativo de design, codificação e teste continua até que o aplicativo atenda às necessidades e expectativas estabelecidas. Ao final, o desenvolvedor tem um aplicativo Windows Forms totalmente funcional, construído em C# com o Visual Studio Community, pronto para ser distribuído ou utilizado. 2.2 Formulários e caixas de mensagem No desenvolvimento de interfaces gráficas utilizando Windows Forms com C#, é essencial criar formulários e caixas de mensagem para interagir com o usuário. Este subtópico ensina a criar formulários customizados fundamentais para coletar informações do usuário, além de apresentar informações e perguntas por caixas de mensagem. Na customização desses formulários, os desenvolvedores são livres para definir layout, estilos, cores e outros elementos de design que façam o formulário não apenas funcionar bem, mas também parecer atraente e se alinhar com a identidade visual do aplicativo. No núcleo da criação de um formulário está a classe Form, parte integral do namespace System. Windows.Forms do .NET Framework. Ao instanciar um objeto dessa classe, o desenvolvedor cria uma janela de aplicativo padrão, que pode ser modificada e estilizada. A personalização de um formulário envolve a definição de várias propriedades, como seu título, ícone, cor de fundo, tamanho e estilo de borda. Essas propriedades podem ser definidas programaticamente no código C# ou visualmente usando o Designer de Formulários do Visual Studio, que proporciona uma interface de arrastar e soltar para construir interfaces de usuário. Além da aparência, os formulários gerenciam o comportamento. Por exemplo, eventos são aspectos fundamentais dos formulários: eventos como Load, Click e Resize podem ser manipulados para executar ações específicas quando ocorrem interações do usuário ou outras atividades. Um evento Load pode inicializar certos controles ou carregar dados quando o formulárioé aberto pela primeira vez. Um aspecto importante é a gestão de layout, e o Windows Forms oferece várias opções para gerenciar a disposição dos controles num formulário. Layouts podem ser fixos, e o desenvolvedor define posições e tamanhos exatos para cada controle, ou podem ser dinâmicos, utilizando gerenciadores de layout como FlowLayout ou TableLayoutPanel. Esses gerenciadores permitem que os controles se ajustem automaticamente ao redimensionar o formulário, melhorando a responsividade e a flexibilidade da interface do usuário. Para criar um form: 1) Inicie o Visual Studio. 2) Selecione “Criar um novo projeto”. 104 Unidade I 3) Escolha “Aplicativo Windows Forms (.NET Framework)” na lista de modelos de projeto. 4) Dê um nome ao seu projeto e clique em Criar. 5) O Form1 será automaticamente criado. Ao clicar com o botão direito no formulário e, a seguir, em “Propriedades” no menu suspenso, é possível alterar as propriedades do formulário. A título de exemplo, vamos alterar algumas propriedades: • em Aparência, mudaremos a propriedade Text para “Os três mosqueteiros”; • em Design, mudaremos a propriedade (Name) para TelaPrincipal; • em Estilo da Janela, mudaremos a propriedade Icon para false. Em Aparência, vamos alterar também a propriedade BackgroundImage. No caso, o clique abrirá uma tela com o título “Selecionar recurso”. Basta clicar no botão Importar, escolher um arquivo de imagem (extensões jpg, png, bmp etc.) e clicar em Ok. A imagem escolhida virará o pano de fundo do formulário. Figura 42 – Formulário original (à esquerda) e formulário com as propriedades alteradas (à direita). A imagem usada como pano de fundo do formulário foi produzida pelo próprio autor com tecnologia DALL-E, uma ferramenta de inteligência artificial desenvolvida pela OpenAI Para finalizar, também vamos alterar para Stretch a propriedade BackgroundImageLayout. Para ver o formulário em ação, clique no botão Iniciar na barra de ferramentas. A figura 42 mostra o formulário original e o formulário após as alterações; note que as mudanças de algumas propriedades já produzem um efeito visual importante. O quadro 5 mostra algumas propriedades úteis de um formulário. A lista não é exaustiva, mas sim uma pequena amostra para um primeiro contato. Essas propriedades são fundamentais para controlar a aparência dos formulários. 105 PROGRAMAÇÃO ORIENTADA A OBJETOS II Quadro 5 – Propriedades úteis de um formulário Propriedade Descrição BackColor Define a cor de fundo do formulário FormBorderStyle Especifica o estilo da borda do formulário, como None, FixedSingle, Fixed3D, FixedDialog etc. Text Texto que aparece na barra de título do formulário Size Define o tamanho do formulário. Geralmente é um objeto do tipo Size que inclui largura e altura StartPosition Define a posição inicial do formulário quando ele é exibido pela primeira vez. Pode ser CenterScreen ou outro Icon Define o ícone a ser exibido na barra de títulos do formulário e na barra de tarefas do Windows WindowState Define o estado inicial do formulário, como Maximized, Minimized ou Normal TopMost Valor booleano que define se o formulário deve ser exibido acima de todos os outros formulários Opacity Define o nível de opacidade do formulário; um valor entre 0 (totalmente transparente) e 1 (totalmente opaco) ShowInTaskbar Valor booleano que determina se o formulário aparece na barra de tarefas do Windows No botão Iniciar – ícone de triângulo verde – o Visual Studio inicia a compilação. O código-fonte escrito em C# é compilado em um executável ou uma biblioteca, dependendo da natureza do projeto. Essa etapa é crucial, pois verifica a correção sintática e lógica do código, garantindo que não haja erros que impeçam a execução do programa. Após a compilação bem-sucedida, o Visual Studio procede para executar a aplicação; nesse ponto o ambiente de desenvolvimento inicia a aplicação dentro de um contexto que permite a depuração e o teste. O formulário principal da aplicação – ou seja, a interface gráfica desenhada e programada pelo desenvolvedor – é exibido. Nesse momento o desenvolvedor pode interagir com o formulário, exatamente como um usuário final faria. Essa interação é vital para o processo de desenvolvimento, e o desenvolvedor pode testar a funcionalidade dos elementos da interface, como botões, caixas de texto, menus e outros controles. Erros lógicos, problemas de interface ou falhas na experiência do usuário podem ser identificados e corrigidos. Além disso, é possível verificar como o aplicativo responde a diferentes ações, como cliques do mouse, entradas de teclado e redimensionamento de janelas. Um aspecto importante desse processo é a depuração. Se o desenvolvedor inseriu pontos de interrupção no código, a execução do programa será pausada nesses pontos, permitindo a inspeção de variáveis, o rastreamento de execução e a análise do comportamento do programa; é uma ferramenta poderosa para entender e resolver problemas complexos que podem não ser imediatamente aparentes. Concluídos os testes e a depuração, o desenvolvedor pode parar de executar a aplicação selecionando Parar no Visual Studio. Esse ciclo de iniciar, testar e parar se repete inúmeras vezes durante o desenvolvimento de uma aplicação, evidenciando a natureza iterativa e incremental do desenvolvimento de software. Se dermos um clique duplo no formulário, ele abrirá o código-fonte (arquivo Form1.cs), que pode ser editado. A figura 43 mostra o código-fonte aberto na tela. 106 Unidade I Figura 43 – Form1.cs aberto para edição na tela Note que há uma aba Form1.cs e ao lado a aba Form1.cs [Design], que serve para retornar ao modo gráfico (Design). Observe ainda que o código-fonte foi aberto automaticamente com o cursor posicionado no método Form1_Load para editar um código executável no evento Load. Eventos são fundamentais para gerenciar o ciclo de vida do formulário e para interagir com o usuário, e permitem que os desenvolvedores implementem comportamentos específicos em resposta às ações do usuário ou a mudanças no estado do formulário. O quadro 6 mostra uma lista não exaustiva de eventos. Cada um pode ser manipulado escrevendo código no método de evento correspondente, que pode ser gerado automaticamente no Visual Studio quando criamos um manipulador de eventos para um caso específico. Quadro 6 – Eventos mais comuns associados a um Form em Windows Forms com C# Evento Aplicação Load Antes da primeira exibição do formulário. É comumente usado para inicializar variáveis ou estado Shown Depois que o formulário é exibido pela primeira vez Closing Quando o formulário está sendo fechado, permitindo interrupção ou alteração do fechamento Closed Depois que o formulário fecha. É utilizado para limpar ou liberar recursos Resize Quando o formulário é redimensionado. Útil para ajustar o layout dos controles MouseMove Quando o mouse é movido sobre o formulário Click Quando o formulário é clicado DoubleClick Quando o formulário é clicado duas vezes rapidamente KeyPress Quando uma tecla é pressionada enquanto o formulário tem foco GotFocus Quando o formulário recebe o foco LostFocus Quando o formulário perde o foco 107 PROGRAMAÇÃO ORIENTADA A OBJETOS II Evento Aplicação Paint Quando o formulário precisa ser redesenhado. Útil para desenho personalizado KeyDown Quando uma tecla é pressionada enquanto o formulário tem foco KeyUp Quando uma tecla é liberada enquanto o formulário tem foco Activated Quando o formulário é ativado e se torna o principal Deactivate Quando o formulário perde o status de ativo para outro formulário Além dos formulários, as caixas de mensagem desempenham papel vital na comunicação com o usuário. No universo do C#, existe uma variedade de caixas de mensagem que podem ser utilizadas conforme a necessidade. Por exemplo, há caixas de mensagem usadas apenas para fornecer informações ao usuário, como avisos ou confirmações de ações realizadas. Essas caixas são geralmente simples e possuemum botão para fechar a mensagem. A figura 44 mostra a execução de uma caixa de mensagem, iniciada pelo evento Load do Form, conforme o código-fonte da mesma figura. Figura 44 – Caixa de mensagem simples Outro tipo importante de caixa de mensagem é a que solicita uma confirmação do usuário. É frequentemente utilizada quando é necessário garantir que o usuário deseja prosseguir com uma ação específica, como excluir um arquivo ou fechar um aplicativo. Normalmente apresenta opções como Sim, Não e às vezes Cancelar, permitindo-lhe tomar uma decisão informada. A figura 45 mostra essa caixa, e novamente utilizamos o evento Load do Form. Note também que o ícone de interrogação foi colocado pelo parâmetro MessageBoxIcon.Question. 108 Unidade I Figura 45 – Caixa de mensagem com opções para o usuário Caixas de mensagem que apresentam erros são cruciais para informar usuários sobre problemas durante a execução do aplicativo. Elas fornecem feedbacks essenciais quando algo não funciona conforme o esperado, ajudando-os a entender o que deu errado. Essas caixas geralmente têm um ícone de erro e fornecem detalhes suficientes para o usuário ou desenvolvedor identificar e corrigir o problema. A figura 46 exemplifica um código que faz uma divisão ilegal (por zero), e portanto uma exceção é gerada e colocada na tela pela caixa de mensagem. Figura 46 – Exceção capturada e mostrada numa caixa de mensagem Um aspecto importante das caixas de mensagem é a capacidade de capturar a resposta do usuário pelo DialogResult. 109 PROGRAMAÇÃO ORIENTADA A OBJETOS II 1. private void Form1_Load(object sender, EventArgs e) 2. { 3. string mensagem = “Você deseja executar esta ação?”; 4. string titulo = “Confirmação Necessária”; 5. // Exibe a caixa de mensagem com botões ‘Sim’, ‘Não’ e ‘Cancelar’ 6. DialogResult result = MessageBox.Show(mensagem, titulo, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); 7. // Verifica qual botão foi pressionado 8. if (result == DialogResult.Yes) 9. { 10. // Código para o caso de ‘Sim’ 11. MessageBox.Show(“Você escolheu ‘Sim’”); 12. } 13. else if (result == DialogResult.No) 14. { 15. // Código para o caso de ‘Não’ 16. MessageBox.Show(“Você escolheu ‘Não’”); 17. } 18. else 19. { 20. // Código para o caso de ‘Cancelar’ 21. MessageBox.Show(“Você escolheu ‘Cancelar’”); 22. } 23. } Figura 47 – DialogResult As linhas 7, 10 e 15 ilustram o DialogResult. A funcionalidade permite que o aplicativo tome decisões baseadas nas escolhas do usuário. Por exemplo, se um usuário selecionar Sim numa caixa de mensagem de confirmação, o aplicativo pode prosseguir com a ação correspondente. Por outro lado, se o usuário escolher Não, pode abortar a ação ou oferecer alternativas. 2.3 Elementos gráficos básicos: rótulos, caixas de texto e botões Um formulário, no contexto do WindowsForms, é essencialmente uma janela ou tela na qual os controles são colocados para interagir com o usuário. A maneira como esses controles são integrados e gerenciados dentro do formulário determina a funcionalidade, a estética e a usabilidade da aplicação, e ele atua como container cujos controles são alocados e organizados. Essa relação de container e conteúdo é crucial para definir como a interface do usuário será apresentada e como responderá às interações. Nesse contexto, alguns controles básicos – como Label (rótulo), TextBox (caixa de texto) e Button (botão) – desempenham papel essencial e proporcionam a base para a interação entre usuário e software. 110 Unidade I O Label é primordial em qualquer interface gráfica, pois serve como meio de apresentar texto de forma não editável. As propriedades de texto do Label permitem que o desenvolvedor especifique o conteúdo a ser exibido; além disso, o alinhamento do texto dentro do Label contribui para a estética e a legibilidade da interface. Estilos variados podem ser aplicados ao Label, incluindo diferentes fontes, cores e tamanhos, o que possibilita uma customização avançada para se adequar ao design da aplicação. TextBox, por sua vez, é um controle interativo que permite a entrada de texto pelo usuário. Uma de suas características mais notáveis é o modo multilinha, que o torna ideal para inputs de texto mais extensos. Outro aspecto importante são os eventos associados, especialmente o TextChanged – evento disparado toda vez que o texto dentro do TextBox é alterado, seja por digitação, seja por programação, permitindo que o software responda dinamicamente a essas mudanças. Por exemplo, pode-se implementar uma lógica de validação de dados em tempo real, melhorando a interação do usuário com a aplicação. Finalmente, Button é um elemento crucial em quase todas as interfaces gráficas e age como gatilho para executar ações. O evento Click do Button é talvez o mais utilizado, acionado quando o usuário clica no botão – funcionalidade central para iniciar processos como enviar formulários, abrir novas janelas ou iniciar cálculos. Além disso, a associação de atalhos de teclado com botões é uma prática que realça a usabilidade e permite que os usuários realizem ações rapidamente, sem precisar do mouse. Tal característica é útil em aplicações que demandam rapidez ou para usuários que preferem ou necessitam de interações no teclado. Quadro 7 – Exemplos de propriedades e eventos dos controles Label, Texbox e Button Controle Tipo Nome Descrição Label Propriedade Text Define o texto do Label TextAlign Alinha o texto dentro do Label Font Estilo da fonte do texto ForeColor Cor da fonte do texto BackColor Cor de fundo do Label Evento Click Evento ao clicar DoubleClick Evento ao clicar duas vezes MouseEnter Evento ao entrar com o mouse MouseLeave Evento ao sair com o mouse MouseMove Evento ao mover o mouse sobre o Label TextBox Propriedade Text Texto inserido pelo usuário Multiline Permite texto em várias linhas ReadOnly Impede modificação do texto MaxLength Máximo de caracteres permitidos ForeColor Cor da fonte do texto Evento TextChanged Altera quando o texto muda KeyDown Evento ao pressionar uma tecla KeyUp Evento ao soltar uma tecla Enter Evento ao entrar no TextBox Leave Evento ao sair do TextBox 111 PROGRAMAÇÃO ORIENTADA A OBJETOS II Controle Tipo Nome Descrição Button Propriedade Text Texto do botão BackColor Cor de fundo do botão ForeColor Cor da fonte do botão Image Imagem no botão FlatStyle Estilo visual do botão Evento Click Evento ao clicar no botão DoubleClick Evento ao clicar duas vezes no botão MouseEnter Evento ao entrar com o mouse no botão MouseLeave Evento ao sair com o mouse do botão MouseMove Evento ao mover o mouse sobre o botão O quadro mostra exemplos úteis de eventos e propriedades. A compreensão e o uso eficiente desses controles – Label, TextBox e Button – são fundamentais para desenvolver interfaces gráficas em WindowsForms com C#. Cada controle oferece funcionalidades específicas que, quando bem usadas, podem enriquecer a experiência do usuário e a eficácia da aplicação. A hierarquia e o posicionamento também são fundamentais na relação dos controles com o formulário. Controles são posicionados dentro do formulário e seguem uma ordem hierárquica que determina como são renderizados e como interagem entre si. Por exemplo, um TextBox para entrada de dados é usualmente acompanhado por um Label para sua identificação e um Button para submeter as informações inseridas. Essa disposição não é apenas estética, mas também funcional, e determina a sequência de navegação e interação do usuário com a aplicação. Outrossim, a interação entre controles e formulário é gerenciada através de eventos. Num formulário WindowsForms, eventos como cliques do mouse ou pressionamentos de tecla são capturados e tratados. O formulário, portanto, não apenas hospeda controles, mas também serve como ponto central para gerenciar eventos e garantir que as ações do usuário sejam interpretadas e respondidasadequadamente. 2.4 Elementos gráficos de apoio: painéis, grupos, rótulos com vínculo e dicas de ferramenta Além de controles básicos como Label, TextBox e Button, outros elementos enriquecem a experiência do usuário, como Panel, GroupBox, LinkLabel e ToolTip – cada um com funcionalidades que contribuem para uma interface mais organizada, intuitiva e informativa. Panel é um controle versátil no contexto de WindowsForms. Sua principal função é servir como container para outros controles. Essa característica é particularmente útil quando se deseja agrupar elementos de interface de forma lógica. Por exemplo, um formulário com vários campos de entrada e etiquetas pode ser organizado de maneira mais clara e coerente dentro de um Panel. Além disso, oferece capacidades de rolagem, indispensáveis se o espaço na tela for limitado. Com isso, mesmo uma grande quantidade de controles pode ser acomodada em uma área confinada e melhorar a usabilidade sem comprometer a estética. 112 Unidade I Um dos principais atributos do Panel é sua flexibilidade em termos de layout e design. Desenvolvedores podem utilizar Panels para segmentar visualmente a interface do usuário, organizando-a em seções lógicas e coerentes. Por exemplo, em uma aplicação com múltiplas funções, cada conjunto de controles relacionados a uma função específica pode ser agrupado num Panel separado. Isso não apenas melhora a estética da aplicação, mas também facilita a navegação e a compreensão por parte do usuário, que pode identificar e interagir com diferentes funcionalidades de forma mais intuitiva. Outro aspecto relevante do Panel é sua capacidade de aninhamento. Panels podem conter outros Panels e permitir a criação de estruturas hierárquicas complexas. Essa capacidade de aninhamento é útil para criar interfaces ainda mais organizadas e segmentadas, onde cada seção pode ter seu próprio layout e conjunto de controles, gerenciados de forma independente. Do ponto de vista técnico, o Panel é facilmente personalizável. Os desenvolvedores controlam várias propriedades, como cor de fundo, bordas e tamanho, permitindo que o Panel se integre harmoniosamente ao design geral da aplicação. Além disso, a interação do Panel com outros controles é gerenciada de maneira fluida e eficiente pelo WindowsForms, garantindo que os eventos de usuário sejam tratados corretamente, mesmo quando envolvem controles aninhados dentro de Panels. GroupBox é outro controle que facilita a organização de interfaces gráficas. Como o nome sugere, seu propósito é agrupar controles relacionados. Diferente do Panel, ele não apenas contém os controles, mas também os apresenta visualmente como conjunto distinto, usualmente com uma borda e um título opcional. Isso é particularmente benéfico em formulários complexos ou em aplicações com múltiplas opções e configurações, em que é crucial a clareza na segmentação de diferentes seções. Seu uso contribui para uma interface mais intuitiva, permitindo que os usuários identifiquem rapidamente conjuntos de controles relacionados e entendam como interagir com cada parte da aplicação. Ele é essencialmente um container, mas com a característica distintiva de apresentar borda e título opcionais. Essa estrutura visual é benéfica em interfaces complexas, nas quais diferentes conjuntos de controle precisam ser claramente delineados; por exemplo, num formulário com várias seções de entrada de dados, cada seção pode ser encapsulada num GroupBox separado, com um título descritivo. Isso não só ajuda os usuários a navegar pela interface mais eficientemente, mas também proporciona uma experiência de usuário mais intuitiva, facilitando a identificação de grupos de controle que trabalham juntos para cumprir uma função específica. Embora sua função primária seja agrupar controles visuais, ele também oferece uma variedade de propriedades que podem ser ajustadas para se adequar ao design geral da aplicação, incluindo personalização da fonte do título, a cor da borda e até mesmo a opção de habilitar ou desabilitar o GroupBox, o que pode alterar a disponibilidade dos controles contidos nele. Este último aspecto é útil quando determinados grupos de controle só devem ser acessíveis em certas condições. Outro aspecto técnico importante do GroupBox é sua capacidade de influenciar o comportamento dos controles que contém. Por exemplo, desabilitar um GroupBox automaticamente desabilita todos os controles dentro dele – funcionalidade que simplifica a lógica de programação quando um conjunto inteiro de controles precisa ser ativado ou desativado em resposta a certas condições da aplicação. 113 PROGRAMAÇÃO ORIENTADA A OBJETOS II Por fim, o GroupBox se integra perfeitamente ao mecanismo de layout do WindowsForms, permitindo que os desenvolvedores posicionem e redimensionem os controles internos com facilidade. Isso é essencial para criar interfaces responsivas que se adaptem a diferentes tamanhos de tela e resoluções. Rótulos com vínculo, conhecidos como Linked Labels, são uma adição significativa à gama de recursos disponíveis para desenvolvedores. Esses rótulos – que combinam as características de um rótulo tradicional com a funcionalidade de um hiperlink – oferecem uma maneira interativa e intuitiva de proporcionar uma experiência de usuário mais rica e conectada. São uma variação do controle Label comum, mas com a capacidade adicional de responder a interações do usuário, como cliques do mouse. Essa capacidade permite que os desenvolvedores os utilizem não apenas para exibir texto, mas também para funcionar como pontos de navegação dentro da aplicação ou para abrir páginas externas da web. Por exemplo, um rótulo com vínculo pode direcionar o usuário para uma seção diferente da aplicação, abrir um formulário de ajuda ou um site com informações relevantes. Visualmente, costumam diferir de rótulos normais pela cor e pelo sublinhado, indicando que são interativos e funcionam como o hiperlink de um navegador. Além disso, propriedades do texto – como fonte, cor e alinhamento – podem ser ajustadas para se adequar ao design geral da interface do usuário. Essa customização visual melhora a estética da aplicação e ajuda na identificação intuitiva desses rótulos como elementos interativos pelo usuário, que podem ser usados para fornecer atalhos rápidos e intuitivos, melhorando a experiência do usuário para pessoas com diferentes habilidades de navegação. Isso é valioso em interfaces complexas, onde a navegação rápida e direta pode melhorar significativamente a usabilidade. Já os tooltips proporcionam um meio simples mas poderoso de melhorar a experiência, fornecendo informações contextuais de forma discreta e acessível. Bem implementados, tooltips podem aumentar significativamente a usabilidade, a compreensão e a acessibilidade de uma aplicação, tornando-a mais intuitiva e amigável. Esses pequenos textos informativos emergem quando o usuário posiciona o cursor do mouse sobre determinado elemento da interface, oferecendo esclarecimentos, dicas ou instruções adicionais. O uso eficiente de tooltips é sinal de uma interface bem pensada, que busca orientar e informar o usuário de maneira intuitiva e discreta. São fáceis de implementar, mas oferecem um grande impacto na usabilidade e na acessibilidade da aplicação. Geralmente se associam a um controle específico, como um botão, ícone ou campo de texto. A mensagem que aparece é breve mas direta, destinada a fornecer informações úteis que podem não ser imediatamente óbvias a partir do layout ou do rótulo do controle. Por exemplo, um tooltip pode explicar a função de um botão com um ícone ambíguo ou oferecer orientações sobre como preencher um campo de formulário específico. 114 Unidade I Quadro 8 – Propriedades e eventos dos controles Panel, GroupBox, LinkLabel e tooltip Controle Tipo Nome Descrição Panel Propriedade AutoScroll Habilita rolagem automática BackColor Cor de fundo do painel BorderStyle Estilo da borda Dock Como o painel éacoplado Evento Click Evento ao clicar DoubleClick Evento ao clicar duas vezes Enter Evento ao entrar no painel Leave Evento ao sair do painel GroupBox Propriedade Text Texto do GroupBox Font Estilo da fonte do texto Enabled Habilita ou desabilita o GroupBox Dock Como o GroupBox é acoplado Evento Click Evento ao clicar DoubleClick Evento ao clicar duas vezes Enter Evento ao entrar no GroupBox Leave Evento ao sair do GroupBox LinkLabel Propriedade Text Texto do LinkLabel LinkColor Cor do link VisitedLinkColor Cor do link visitado LinkBehavior Comportamento do link Evento LinkClicked Evento ao clicar no link MouseHover Evento ao passar o mouse sobre o link MouseMove Evento ao mover o mouse sobre o link Tooltip Propriedade Active Ativa ou desativa o tooltip AutomaticDelay Atraso automático AutoPopDelay Atraso de desaparecimento automático InitialDelay Atraso inicial Evento Popup Evento quando o tooltip aparece Draw Evento de desenho Closed Evento quando o tooltip fecha Desenvolvedores são livres para modificar vários aspectos dos tooltips, incluindo o tempo de atraso antes de aparecerem, o tempo de permanência na tela e até mesmo sua aparência, como fonte e cor. Essa capacidade de personalização permite que os tooltips se integrem de forma coesa ao design geral da aplicação, mantendo a consistência visual e reforçando a identidade da marca ou do estilo da interface. Além de seu valor estético e informativo, são extremamente valiosos do ponto de vista da acessibilidade. Eles podem ajudar usuários que podem ter dificuldades em entender o propósito de determinados controles ou que precisam de lembretes adicionais sobre a funcionalidade de certos elementos da interface. Para usuários com deficiências visuais que dependem de leitores de tela, tooltips podem ser configurados para ser lidos por essas tecnologias, proporcionando uma camada adicional de acessibilidade. 115 PROGRAMAÇÃO ORIENTADA A OBJETOS II Outra característica dos tooltips é a gestão do equilíbrio entre informação e sobrecarga. Embora seja tentador usar tooltips para fornecer grandes quantidades de informação, são mais bem utilizados em dicas curtas e diretas, pois isso mantém a interface limpa e focada, evitando confusão ou distração do usuário com informações excessivas ou desnecessárias. Para usar um tooltip no Windows Forms do .NET, siga estes passos: 1) Arraste o tooltip para o formulário. Será criado o tooltip1; 2) Defina o texto do tooltip para cada controle no qual quisermos exibir uma dica de ferramenta. Para isso: a) Selecione o controle (por exemplo, button1). b) Na janela de propriedades, procure em Diversos uma propriedade chamada ToolTip em tooltip1. c) Defina o valor dessa propriedade para o texto desejado. 3) Configure propriedades adicionais, caso queira, para personalizar o comportamento do tooltip, como AutoPopDelay, InitialDelay, ReshowDelay etc. 4) Habilite o tooltip para ele funcionar (a propriedade Enabled deve estar definida como true, que é o padrão). 5) Execute e teste o aplicativo. Ao passar o mouse sobre o controle para o qual definimos o tooltip, a dica de ferramenta configurada deve aparecer. Figura 48 – Resumo: elementos gráficos básicos e de apoio Este tópico apresentou diversos recursos básicos para o desenvolvedor começar a desenvolver interfaces para o usuário. A figura 48 apresenta visualmente todos os controles aqui mencionados. 116 Unidade I Resumo Nesta unidade introduzimos um projeto de programação inspirado no romance Os três mosqueteiros. O jogo, denominado Os desafios dos mosqueteiros: duelos & destinos, é uma aplicação prática para console e tem como objetivo pedagógico apoiar a revisão de conceitos de POO utilizando a linguagem C#. Mergulhamos em aspectos técnicos da programação do jogo, dentre os quais: • Criar e utilizar classes, que são estruturas centrais na POO. • Implementar propriedades e métodos nas classes, essenciais para definir comportamentos e estados dos objetos. • Usar enums (enumerações), que ajudam a organizar conjuntos de constantes relacionadas. • Implementar delegados, em particular Action, para manipular métodos como parâmetros. • Usar encapsulamento, princípio-chave da POO, para proteger o estado interno de um objeto. • Gerar números aleatórios utilizando a classe Random, crucial para elementos de sorte no jogo. • Usar listas e coleções genéricas (List), fundamentais para armazenar e manipular conjuntos de dados. • Adotar conceitos de exceções e programação defensiva para aumentar a robustez do código. • Manipular eventos e callbacks, particularmente no contexto de interações no jogo. Esses e outros assuntos foram explicados com exemplos práticos relacionados ao desenvolvimento do jogo. Foi proposto que o leitor compilasse e executasse a primeira versão do jogo e, na sequência, percebesse alguns conceitos adicionais não implementados, como herança, polimorfismos e design patterns. Foram apresentadas as alterações no código relacionadas a esses temas, incluindo exemplos dos padrões Singleton e Factory. 117 PROGRAMAÇÃO ORIENTADA A OBJETOS II Finalizada a revisão conceitual e prática, oferecemos uma visão abrangente sobre o ambiente de desenvolvimento integrado (IDE) para programação em C#, destacando o Visual Studio como ferramenta primordial. Discutimos amplamente suas funcionalidades, estrutura e flexibilidade, enfatizando a importância de suas capacidades de personalização, gestão de projetos, depuração e integração com sistemas de controle de versão. Além disso, abordamos as diferenças entre as edições do Visual Studio, incluindo as versões Community, Professional e Enterprise, e ressaltamos a relevância do .NET Framework e .NET Core no desenvolvimento de aplicações multiplataforma. Também evidenciamos a importância das práticas de refatoração, análise de código e gestão de dependências e pacotes, utilizando o NuGet para a manutenção da integridade e sucesso de projetos de software. Também abordamos o desenvolvimento de interfaces gráficas de usuário (GUIs) no contexto do Windows, com foco em Windows Forms, uma tecnologia do .NET Framework. Introduzimos alguns aspectos cruciais da experiência do usuário (UX) e da interação humano-computador (IHC) usando controles específicos para criar interfaces ricas e funcionais. Foi enfatizada a simplicidade e eficiência do Windows Forms para desenvolvedores, e mencionamos a capacidade de arrastar e soltar elementos gráficos, além de discutir suas características, como manipulação de eventos e flexibilidade na criação de controles personalizados. Apresentamos um guia para criar um formulário no Windows Forms usando C# no Visual Studio Community, detalhando desde a configuração do projeto até a adição de controles e programação de eventos. Discutimos a importância de elementos como formulários e caixas de mensagem na interação com o usuário, abordando temas como personalização, gerenciamento de layout e manipulação de eventos. Também foram explorados controles básicos e adicionais, como rótulos, caixas de texto, botões, painéis e grupos, detalhando suas propriedades e eventos associados para otimizar a funcionalidade e a estética da interface do usuário. Vimos que Form é a janela ou container principal que hospeda outros elementos de interface. É o espaço básico de interação entre o aplicativo e o usuário. MessageBox apresenta informações, alertas ou solicita confirmações dos usuários, desempenhando papel crucial na comunicação interativa. Labels identificam outros elementos de interface ou fornecem breves instruções, melhorando a compreensão do usuário sobre a funcionalidade da interface. 118 Unidade I GroupBox e Panel são containers que ajudam a agrupar elementos relacionados, contribuindo para uma organização lógica e uma apresentação visual clara. TextBox é um campo fundamental para entrada de texto, permitindo aos usuários inserir informações como nome, endereço ou feedback.Por fim, tooltips melhoram o entendimento e a acessibilidade das aplicações. 119 PROGRAMAÇÃO ORIENTADA A OBJETOS II Exercícios Questão 1. (UFV 2022, adaptada) Em relação aos conceitos envolvidos na POO, avalie as afirmativas. I – Abstração, encapsulamento, herança e polimorfismo são pilares do paradigma de orientação a objetos. II – Classe abstrata é uma classe não instanciada. III – Generalização é a técnica para esconder detalhes internos (atributos/métodos) de uma classe. IV – Herança múltipla ocorre quando mais de um método é herdado. É correto o que se afirma em: A) I, apenas. B) III, apenas. C) I e II, apenas. D) II e IV, apenas. E) I, II, III e IV. Resposta correta: alternativa C. Análise das afirmativas I – Afirmativa correta. Justificativa: o paradigma de orientação a objetos tem quatro pilares: abstração, encapsulamento, herança e polimorfismo. A abstração permite simplificar a complexidade do mundo real, representando elementos por objetos, de forma a ocultar detalhes desnecessários e focar apenas as informações relevantes. O encapsulamento é um recurso que oculta os detalhes internos do funcionamento dos métodos de uma classe, pois conhecer a implementação interna da classe é desnecessário do ponto de vista do objeto. A herança permite que objetos herdem características e comportamentos de outros objetos, o que ajuda reutilizar o código. Por fim, polimorfismo refere-se à capacidade de um mesmo método ser aplicado de maneiras distintas em classes ou objetos diferentes. 120 Unidade I II – Afirmativa correta. Justificativa: classe abstrata não pode ser instanciada, ou seja, não pode dar origem a um objeto diretamente. Ela apenas fornece um modelo para gerar outras classes, que podem herdar suas características. III – Afirmativa incorreta. Justificativa: a técnica utilizada para esconder detalhes internos de uma classe é o encapsulamento. IV – Afirmativa incorreta. Justificativa: no contexto de orientação a objetos, a herança múltipla se dá quando uma classe herda características e comportamentos de mais de uma classe. A linguagem C#, no entanto, não suporta herança múltipla de classes. Essa limitação pode ser contornada com a implementação de interfaces. Questão 2. (Fundatec 2023, adaptada) O .NET é uma plataforma de desenvolvimento de software que suporta diversas linguagens de programação, incluindo C#. Nesse contexto, assinale a alternativa que indica um benefício de utilizarmos a plataforma .NET para desenvolver aplicações. A) Permite desenvolver aplicações somente para desktop. B) Possui um conjunto limitado de bibliotecas para criar interfaces gráficas de usuário. C) É uma plataforma dedicada ao desenvolvimento em linguagem C#. D) Possui uma grande variedade de ferramentas e recursos que permitem desenvolver aplicações para diferentes plataformas e dispositivos. E) É uma plataforma exclusiva para desenvolver aplicações em nuvem. Resposta correta: alternativa D. Análise das alternativas A) Alternativa incorreta. Justificativa: a plataforma .NET permite o desenvolvimento de diversos tipos de aplicações, não apenas para desktop. B) Alternativa incorreta. Justificativa: o .NET tem um rico conjunto de bibliotecas para a criação de interfaces gráficas de usuário, não um conjunto limitado. 121 PROGRAMAÇÃO ORIENTADA A OBJETOS II C) Alternativa incorreta. Justificativa: o .NET suporta várias linguagens de programação, como C#, F# e VB.NET, entre outras. Portanto, a plataforma não é dedicada apenas ao desenvolvimento em C#. D) Alternativa correta. Justificativa: a plataforma detém uma grande variedade de ferramentas e recursos que permitem desenvolver aplicações para diferentes plataformas e dispositivos, incluindo aplicações para desktop, web, mobile, jogos e internet das coisas, entre outras. E) Alternativa incorreta. Justificativa: o .NET não é exclusivo para desenvolver aplicações em nuvem, embora também possa ser utilizado para isso.genéricas no C# oferecem maneiras 12 Unidade I flexíveis e eficientes de armazenar e manipular dados, permitindo escrever um código que seja tanto seguro quanto reutilizável. Em aplicativos para desktop e mobile, frequentemente usamos eventos e delegados para responder a interações do usuário, seja um clique do mouse, seja uma entrada do teclado. No C#, delegado é um tipo que pode fazer referência a um método. Essa referência pode ser alterada em tempo de execução, tornando os delegados instrumentos poderosos para criar sistemas responsivos e dinâmicos. Associados a delegados temos eventos, essencialmente mecanismos que permitem a uma classe notificar outras classes se ocorrer algo de interesse; isso é particularmente relevante em aplicações de interface gráfica, nas quais ações como cliques ou entradas do usuário podem desencadear respostas específicas no software. Também não podemos esquecer dos padrões de projeto – como Singleton, Factory e Repository –, que, embora não sejam exclusivos da POO, muitas vezes se alinham com seus conceitos para ajudar a resolver problemas comuns de design. Por fim, ao lidar com aplicativos que se conectam a bancos de dados, é essencial um bom tratamento de exceções para gerenciar situações inesperadas, como falhas de conexão. Além disso, a integração com o .NET Framework ou o .NET Core/.NET 5+ proporciona acesso a uma vasta biblioteca de classes e métodos prontos para uso, desde a manipulação de strings e matemática complexa até funcionalidades de rede e entrada/saída de arquivos. Neste tópico passaremos por revisão e aprofundamento, com exercícios práticos de vários aspectos-chave da POO e do C#, que são importantes para desenvolver interfaces desktop e mobile. Também será apresentado o ambiente de desenvolvimento integrado (IDE) e alguns detalhes do .NET Framework e .Net Core. 1.1 Revisando conceitos de POO: exercícios práticos (console) Para revisar conceitos, vamos criar um jogo inspirado no romance Les Trois Mousquetaires, traduzido no Brasil como Os três mosqueteiros, escrito por Alexandre Dumas e publicado pela primeira vez em 1844. O jogo criado para este livro-texto terá como nome Os desafios dos mosqueteiros: duelos & destinos. Antes de detalhar o jogo, um breve resumo do livro: D’Artagnan, um jovem das regiões rurais da Gasconha, viaja para Paris com esperanças de se juntar aos Mosqueteiros do Rei, a guarda de elite do rei Luís XIII. No entanto, no início de sua jornada, ele se envolve em duelos individuais com três homens diferentes devido a mal-entendidos: Athos, Porthos e Aramis. Em vez de lutar contra eles, os quatro são interrompidos por guardas do cardeal Richelieu, o ministro-chefe do rei, e juntos eles enfrentam os guardas e solidificam uma amizade duradoura. O quarteto se envolve em diversas aventuras, e muitas delas os colocam em oposição ao cardeal Richelieu e sua agente, a bela e perigosa Milady de Winter. Enquanto D’Artagnan é apaixonado pela jovem Constance Bonacieux, a trama se desenrola em um fundo de intrigas reais e políticas, incluindo os esforços de Richelieu para frustrar os planos da rainha Ana da Áustria e seu suposto amante, o Duque de Buckingham. 13 PROGRAMAÇÃO ORIENTADA A OBJETOS II Figura 1 – Os mosqueteiros do nosso jogo; imagem produzida com tecnologia DALL-E, ferramenta de inteligência artificial desenvolvida pela OpenAI Observação Neste tópico ainda não será abordada a interface gráfica (falaremos disso em detalhe depois). Por ora faremos os exercícios usando o console do C#, cuja característica é fornecer uma simples saída de caracteres na tela. Em virtude desse fato, a imagem de nossos mosqueteiros neste primeiro momento seria um pouco diferente: Figura 2 – Os mosqueteiros do nosso jogo como arte ASCII; imagem produzida também com tecnologia DALL-E 14 Unidade I No ambiente empresarial contemporâneo, uma série de etapas é cumprida por diferentes profissionais, fundamentais para o sucesso do projeto muito antes de um programador iniciar a codificação de um sistema em C#. Esse processo começa com a fase de concepção, na qual especialistas em negócios e analistas de sistemas trabalham juntos para compreender e definir os objetivos do projeto, fazendo uma análise minuciosa das necessidades do usuário e do mercado, identificando requisitos críticos que o sistema deve atender. Após essa fase inicial, entra em cena a equipe de análise e design de sistemas, cujos profissionais são responsáveis por traduzir os requisitos coletados em um projeto de software detalhado, utilizando metodologias e ferramentas de modelagem, como unified modeling language (UML), para criar uma representação visual e técnica do sistema proposto. Essa etapa é crucial pois guia os desenvolvedores durante a fase de programação. Além disso, a arquitetura do sistema é cuidadosamente planejada. Arquitetos de software determinam sua estrutura geral, incluindo a seleção de tecnologias, definição de padrões de design e garantia de que o sistema será escalável e seguro – planejamento essencial para assegurar que o sistema atenda tanto às necessidades atuais quanto às futuras demandas. Paralelamente, a equipe de gerenciamento de projetos desempenha papel vital na coordenação de todas essas atividades, estabelecendo cronogramas, alocando recursos e garantindo que todas as equipes estejam alinhadas com os objetivos do projeto. Uma gestão eficaz é fundamental para manter o projeto dentro do prazo e do orçamento. Finalmente, antes de o programador começar a escrever o código em C#, a equipe de qualidade de software entra em ação. Essa equipe é responsável por definir os padrões e processos de qualidade que serão seguidos durante a fase de desenvolvimento. Eles também planejam estratégias de teste que serão implementadas para garantir que o software seja robusto, confiável e livre de erros. Portanto, quando um programador inicia a programação em C#, ele se baseia num extenso trabalho preparatório realizado por uma equipe multidisciplinar. Colaboração e planejamento meticulosos são essenciais para criar um sistema eficaz e de alta qualidade. Para nosso propósito, considere que as equipes já fizeram seu trabalho e obtiveram as seguintes informações: 15 PROGRAMAÇÃO ORIENTADA A OBJETOS II Quadro 1 – Detalhes do jogo Objetivo Avançar pela vida de um mosqueteiro, desde sua iniciação até alcançar o reconhecimento máximo, seja por ser condecorado pelo rei, seja por alcançar um posto de grande honra e responsabilidade. Os jogadores enfrentarão eventos aleatórios que afetarão sua fortuna e honra Componentes Tabuleiro com 60 casas: é linear e estilizado como um mapa da França do século XVII, começando na província e terminando em Paris. Ao longo do caminho, há espaços coloridos: alguns têm instruções fixas e outros indicam que uma carta de evento deve ser retirada Cartas de evento: detalham eventos aleatórios, positivos ou negativos, que afetam o jogador Dado: para determinar o número de espaços a mover Roleta de duelo: para definir o desfecho (vitória, empate ou derrota) de um duelo entre dois mosqueteiros Fichas de fortuna e honra: moedas douradas (para fortuna) e emblemas de espada cruzada (para honra) Peças: representam jogadores (cada uma representa um mosqueteiro: D’Artagnan, Athos, Porthos e Aramis) Roleta de duelo Vitória (verde): “Você venceu o duelo! Avance três casas e ganhe duas fichas de honra” Empate (amarelo): “O duelo terminou empatado. Fique onde está e ganhe uma ficha de honra” Derrota (vermelho): “Você foi derrotado. Volte duas casas e perca uma ficha de honra” Movimento Em sua vez, cada participante gira a roda ou joga o dado e move sua peça pelo número indicado de espaços. Não é permitido dividir a casa. Quando isso acontecer, o jogador da vez deve ocupar o próximo espaço livre Espaços de evento Se um jogador pousar num espaço colorido sem instrução fixa, ele deve retirar uma carta de evento e seguir as instruções Espaços fixos Alguns espaçostêm instruções permanentes Configuração Cada jogador escolhe um mosqueteiro e coloca sua peça no início do tabuleiro As cartas de evento são embaralhadas e colocadas em uma pilha ao lado do tabuleiro Cada jogador começa com cinco fichas de fortuna e cinco de honra Duelo de honra Quando um jogador cai no espaço do duelo de honra, ele pode desafiar qualquer outro jogador usando a roleta de duelo O vencedor do duelo ganha duas fichas de honra do perdedor. Em caso de empate, nenhum jogador perde nem ganha fichas Tributo ao cardeal Ao cair nesse espaço, o jogador deve pagar duas fichas de fortuna como tributo ao cardeal Richelieu. Se o jogador não tiver fortuna suficiente, perde uma ficha de honra Audiência com o rei É a finalização da jornada dos mosqueteiros, momento em que eles são chamados para uma audiência final com o rei para receber seu julgamento Ao chegar a essa casa, o jogador decide se quer o “reconhecimento da corte” (e nesse caso fica sem jogar, aguardando que todos os demais cheguem à audiência) ou se quer apostar todas as suas fichas de fortuna e honra na “roleta do julgamento real”. Essa roleta terá apenas dois marcadores: “condecoração real” e “desprezo da corte” Condecoração real: o jogador vence o jogo Desprezo da corte: o jogador perde todas as suas fichas de honra e fortuna Vencedor O jogo termina quando algum dos participantes recebe condecoração real ou quando todos chegam ao final do tabuleiro. Neste caso, eles contam suas fichas de fortuna e honra O vencedor é determinado pela combinação mais alta de fichas: um ponto para cada ficha de fortuna e dois pontos para cada ficha de honra. O jogador com o maior total vence 16 Unidade I Considere também que a equipe de projeto já criou um diagrama de classes, que facilita o entendimento do jogo: Figura 3 – Diagrama de classes do jogo Por enquanto não é necessário se preocupar com cada detalhe do jogo. As informações apresentadas até aqui são úteis para que o programador tenha uma visão exemplificada do processo anterior à programação em si. Lembre-se que a POO surgiu como paradigma que ajuda a equipe a quebrar problemas complexos em problemas menores. Ao longo deste tópico, desenvolveremos gradativamente o código-fonte do jogo, exercitando a POO em C#. Também revisitaremos alguns conceitos e adicionaremos alguns detalhes complementares. O quadro 1 e o diagrama da figura 3 também servirão como referência de consulta em outras etapas do desenvolvimento do jogo. O quadro 2 fornece uma descrição resumida da figura 3: 17 PROGRAMAÇÃO ORIENTADA A OBJETOS II Quadro 2 – Resumo das classes do jogo, seus métodos e atributos Classe Descrição Atributos e métodos principais Programa Classe principal do programa, responsável por iniciar o jogo – globalRandom: campo estático do tipo Random para gerar números aleatórios – Main: método estático que é o ponto de entrada do programa. Cria uma instância do jogo e o inicia Jogo Representa o jogo gerenciando os elementos principais, como jogadores, tabuleiro e baralho – tabuleiro: objeto da classe Tabuleiro – jogadores: lista de objetos Jogador – baralhoDeEventos: objeto da classe Baralho para eventos do jogo – aleatorio: objeto Random para gerar números aleatórios – roletaDeDuelo: objeto da classe RoletaDeDuelo para duelos – Métodos para gerenciar o jogo, como ExecutarJogo, IniciarJogo, JogarTurno e outros Jogador Representa um jogador, com suas propriedades e ações – Propriedades como Nome, Posicao, FichasDeFortuna, FichasDeHonra, DevePausar, NoReconhecimentoDaCorte, Eliminado – Métodos como PausarProximoTurno, AtualizarTurno e RolarDado Tabuleiro Representa o jogo com suas casas e configurações – numeroDeCasas: número total de casas no tabuleiro – casas: lista de objetos Casa que compõem o tabuleiro – Métodos para inicializar e obter informações das casas do tabuleiro Casa Representa uma casa individual no tabuleiro, com um tipo específico e ações associadas – Tipo: enumeração TipoCasa para indicar o tipo da casa – CasaDeRetorno: valor opcional que indica para qual casa o jogador deve retornar, se aplicável – Construtor para inicializar a casa com um tipo específico e casa de retorno Baralho Representa as várias cartas de evento do jogo – cartas: lista de objetos CartaDeEvento – random: objeto Random para escolher cartas aleatoriamente – Método SortearCarta para sortear uma carta do baralho CartaDeEvento Representa um evento individual com descrição e efeito no jogador – Descricao: texto que apresenta o evento da carta – Efeito: ação (delegate Action) que define o efeito da carta no jogador – Construtor para inicializar a carta com descrição e efeito RoletaDeDuelo Determina o resultado de duelos no jogo – aleatorio: objeto Random para gerar o resultado do duelo – Método Girar para obter um resultado de duelo (ResultadoDuelo) ResultadoDuelo Enumeração dos possíveis resultados de um duelo Enumeração com valores Vitoria, Empate, Derrota TipoCasa Enumeração dos tipos de casa no tabuleiro Enumeração com valores como Normal, Evento, EscolaDeEsgrima, DueloDeHonra, Duelo, Estalagem, TributoAoCardeal, Retorno, AudienciaComORei 18 Unidade I Vamos iniciar a programação desenvolvendo a classe principal do jogo, chamada Programa: 1. using System; 2. using System.Collections.Generic; 3. using System.Linq; 4. class Programa 5. { 6. private static readonly Random globalRandom = new Random(); 7. static void Main(string[] args) 8. { 9. Jogo jogo = new Jogo(globalRandom); 10. jogo.IniciarJogo(); 11. } 12. } Figura 4 – Classe Programa: revisão de conceitos básicos Podemos analisar em detalhe as linhas do código-fonte da figura 4. As três primeiras linhas contêm uma diretiva using, que informa ao compilador C# os namespaces que o programa irá utilizar. System é um namespace fundamental na biblioteca de classes .NET, que fornece funcionalidades básicas como entrada e saída de dados, manipulação de strings, entre outras. System.Collections.Generic é um namespace que contém definições para classes genéricas de coleção – como listas e dicionários –, usadas para armazenar e manipular conjuntos de dados (veremos adiante que ela será usada na criação de listas de jogadores, cartas de baralho, entre outras ferramentas). Já System.Linq fornece funcionalidades para consultar e manipular dados em coleções e outras fontes de dados usando LINQ. Também veremos neste tópico que ele será usado nos métodos ExecutarDueloDeHonra e AnunciarVencedor da classe Jogo. Na linha 4, definimos uma classe chamada Programa, cujo conteúdo interno e funcionamento se estendem da linha 5 até a 12. Em C#, classe é uma estrutura que pode conter dados e métodos para trabalhar com esses dados (por questões didáticas, deixaremos a revisão da linha 6 para depois). As linhas 7 a 11 referem-se ao método Main, que é o ponto de entrada de qualquer aplicativo C#. Quando compilamos e executamos um programa C#, o ambiente de execução .NET (como o .NET Core ou .NET Framework) carrega o assembly (o arquivo compilado) e procura pelo método Main como ponto de entrada. Uma vez encontrado, ele começa a execução a partir desse método. Observação O método Main não precisa estar especificamente dentro de uma classe chamada Program (ou Programa). O nome Program para a classe que contém o método Main é uma convenção muitas vezes utilizada nos templates-padrão do Visual Studio para aplicativos de console, mas não é obrigatório. 19 PROGRAMAÇÃO ORIENTADA A OBJETOS II Na linha 9 criamos uma instância da classe Jogo, passando globalRandom como argumento para o construtor; e jogo é o nome da variável que referencia essa instância. Em breve iniciaremos a programação da classe Jogo (ainda não foi criada). Já a linha 10 demonstra uma chamada ao método IniciarJogo do objeto jogo – responsável por iniciar a lógica dele (e teremos que programá-lo também). Agora, algumas características da linha 6, que declara na classe Programaum campo batizado de globalRandom com a seguinte sintaxe: • private: modificador de acesso que torna o campo acessível apenas dentro da própria classe Programa. Ou seja, estamos rememorando o encapsulamento. • static: significa que o campo pertence à classe Programa em si, e não a uma instância específica dela. • readonly: indica que o valor do campo só pode ser atribuído à declaração ou ao construtor da classe. • Random: tipo de campo que é uma classe do .NET Framework usada para gerar números aleatórios. Para usá-la, você normalmente cria uma instância dessa classe, que é um objeto. • globalRandom: nome dado ao campo. • = new Random();: o campo é inicializado com uma nova instância da classe Random. Random representa o fator sorte no jogo. Por exemplo, mosqueteiros avançam as casas no tabuleiro conforme o número sorteado no lançamento de um dado; no caso, usando Random o número que será sorteado a cada rodada do jogo é aproximadamente aleatório. O mesmo vale para duelos: a decisão sobre quem ganha, perde, ou sobre o empate vem de um sorteio. Na audiência real (final do jogo) também há uma aposta na qual a sorte é novamente representada. Observação Em programação, a expressão aproximadamente aleatório se refere ao conceito de pseudoaleatoriedade. O termo aproximadamente é usado porque, embora os números gerados por tais métodos pareçam aleatórios e tenham propriedades estatísticas de números aleatórios, eles são gerados por um processo determinístico, ou seja, podem ser previstos se você conhecer o estado inicial do gerador de números aleatórios. Em C#, como em muitas outras linguagens de programação, Random é uma classe que implementa um gerador de números pseudoaleatórios (PRNG) e usa um algoritmo que toma um valor inicial, conhecido como semente (seed), e a partir dela gera uma série de números que têm 20 Unidade I aparência de aleatoriedade. Na prática, isso significa que, se você iniciar duas instâncias de Random com a mesma semente, elas produzirão a mesma sequência de números: não há aleatoriedade real. Além disso, no longo prazo, a sequência de números vai começar a se repetir. O período de repetição depende do algoritmo usado pelo PRNG. Em resumo, aproximadamente aleatório significa que, embora os números produzidos se comportem como aleatórios para muitas aplicações práticas, são gerados de maneira previsível e replicável. Por exemplo, os números gerados por Random não devem ser usados para aplicações que requeiram alta segurança, como criptografia ou geração de senhas seguras. Para esses fins, é melhor usar classes como RNGCryptoServiceProvider no .NET, que são projetadas para ser mais imprevisíveis. Colocamos o Random no Program, uma vez que pode não ser ideal criar novas instâncias da classe Random em métodos diferentes, especialmente se esses métodos forem chamados em rápida sucessão, o que pode levar a resultados repetidos e, portanto, degradar o fator sorte. Isso ocorre porque Random é inicializado com um valor de semente baseado no relógio do sistema, e criar várias instâncias rapidamente pode fazer com que elas tenham a mesma semente. Idealmente, uma única instância de Random deve ser compartilhada ou reutilizada, o que justifica a linha 6 ter sido arbitrariamente colocada em Program. Saiba mais A aleatoriedade gerada pela classe Random é suficiente para muitos usos gerais (como neste jogo), mas não é apropriada para aplicações que exigem alto nível de segurança, como criptografia. Para esses casos, são necessárias abordagens mais sofisticadas; uma boa referência para aprofundar o tema é este livro: TEILHET, S.; HILYARD, J. C# Cookbook. Cebastopol: O’Reilly Media, 2004. v. 1 Introduzido o assunto aleatoriedade, vamos desenvolver a classe RoletaDeDuelo, parte fundamental do jogo para determinar seus resultados. Ela utiliza um gerador de números aleatórios para simular uma roleta, e com base no número gerado retorna se o duelo resultou em vitória, empate ou derrota. Esses resultados são usados em outras partes do jogo para definir as consequências dos duelos para os jogadores. 21 PROGRAMAÇÃO ORIENTADA A OBJETOS II 1. class RoletaDeDuelo 2. { 3. private Random aleatorio; 4. 5. // Construtor da RoletaDeDuelo 6. public RoletaDeDuelo(Random random) 7. { 8. aleatorio = random; 9. } 10. 11. // Método para girar a roleta 12. public ResultadoDuelo Girar() 13. { 14. int resultado = aleatorio.Next(3); // 0, 1 ou 2 15. switch (resultado) 16. { 17. case 0: return ResultadoDuelo.Vitoria; 18. case 1: return ResultadoDuelo.Empate; 19. default: return ResultadoDuelo.Derrota; 20. } 21. } 22. } Figura 5 – Classe RoletaDeDuelo Novamente analisaremos cada linha, para revisar o conteúdo de programação C#. A classe é declarada na linha 1 e finaliza na 22, contendo um campo privado (batizado de aleatório) e dois métodos (um construtor e um que gira a roleta). Na linha 3, o campo privado do tipo Random é usado para gerar números aleatórios, cruciais para a roleta funcionar. Na linha 6, o construtor da classe recebe um objeto Random como argumento e o atribui ao campo privado aleatório; isso permite que a roleta use um gerador de números aleatórios fornecido externamente, o que é útil para garantir a aleatoriedade e facilitar testes (lembremos também que o método construtor tem o mesmo nome da classe). Girar (linha 12) é o método principal da classe: quando chamado, ele simula o giro da roleta para determinar o resultado de um duelo. Para fazer isso ele gera um número inteiro aleatório entre 0 e 2 (linha 14) com o auxílio do método Next (membro da classe Random), que tem várias sobrecargas, ou seja, pode ser chamado de diferentes maneiras, a depender dos argumentos fornecidos. 22 Unidade I As sobrecargas mais comuns são: • Next(): retorna um número inteiro positivo aleatório de 32 bits. O valor retornado está no intervalo de 0 a Int32.MaxValue (exclusivo), ou seja, entre 0 e 2147483647. • Next(int maxValue): retorna um número inteiro aleatório que está dentro de um intervalo especificado. O valor retornado está no intervalo de 0 a maxValue (exclusivo). Em nosso caso, Next(3) retorna um número entre 0 e 2. • Next(int minValue, int maxValue): retorna um número inteiro aleatório dentro de um intervalo especificado. O valor retornado está no intervalo de minValue a maxValue (exclusivo). Por exemplo, Next(5, 10) retorna um número entre 5 e 9. A linha 15 inicia uma estrutura switch, que usa o número gerado para selecionar um resultado do duelo: • Caso 0: retorna ResultadoDuelo.Vitoria, indicando que o jogador venceu. • Caso 1: retorna ResultadoDuelo.Empate, indicando empate. • Caso-padrão (default): retorna ResultadoDuelo.Derrota, indicando que o jogador perdeu. Portanto, a probabilidade esperada de vitória, derrota e empate em cada rodada é de um terço, garantindo o equilíbrio na distribuição dos duelos. Note que ainda não desenvolvemos ResultadoDuelo; faremos isso usando uma enumeração, conforme a figura 6 (é um tipo distinto, que engloba um conjunto de constantes nomeadas): 1. enum ResultadoDuelo 2. { 3. Vitoria, 4. Empate, 5. Derrota 6. } Figura 6 – Enumeração do resultado dos duelos Aproveitando o assunto, podemos também criar uma enumeração para representar os diferentes tipos de casa do tabuleiro (figura a seguir): 23 PROGRAMAÇÃO ORIENTADA A OBJETOS II 1. enum TipoCasa 2. { 3. Normal, 4. Evento, 5. EscolaDeEsgrima, 6. DueloDeHonra, 7. Duelo, 8. Estalagem, 9. TributoAoCardeal, 10. Retorno, 11. AudienciaComORei 12. } Figura 7 – Tipos de casa do tabuleiro O tabuleiro tem 60 casas; destas, 38 sem nenhuma descrição, ou seja, caso o jogador caia nela, nada acontece. As 22 casas restantes têm descrições especiais (quadro 3), as quais podem ser agrupadas em 9 categorias, inseridas na enumeração TipoCasa (figura 7). Quadro 3 – Casas do tabuleiro com descrições especiais Casa Descrição 1 Início – província 5 Evento(pegue uma carta de evento) 6 Escola de esgrima: “Treine suas habilidades. Ganhe uma ficha de honra” 8 Duelo de honra 10 Duelo (gire a roleta de duelo para determinar seu destino) 11 Estalagem: “Pague pela sua estadia. Perca uma ficha de fortuna” 12 Tributo ao cardeal 15 Retorno (seta apontando para a casa 5: “Desafio inesperado! Retorne à casa 5”) 20 Evento (pegue uma carta de evento) 25 Duelo (gire a roleta de duelo) 26 Duelo de honra 29 Retorno (seta apontando para a casa 20: “Reveses na jornada! Retorne à casa 20”) 30 Tributo ao cardeal 35 Evento (pegue uma carta de evento) 24 Unidade I Casa Descrição 40 Duelo (gire a roleta de duelo) 45 Duelo de honra 46 Evento (pegue uma carta de evento) 50 Retorno (seta apontando para a casa 35: “Missão falhou! Retorne à casa 35”) 51 Tributo ao cardeal 55 Evento (pegue uma carta de evento) 56 Duelo (gire a roleta de duelo) Enums são úteis porque melhoram a legibilidade do código e reduzem a possibilidade de erro ao restringir valores a um conjunto finito de opções. São essencialmente grupos de constantes inteiras que têm nomes para tornar um programa mais fácil de ler e manter. Como definimos o tipo de casa, seria interessante definir também a própria classe Casa (figura 8). 1. class Casa 2. { 3. public TipoCasa Tipo { get; private set; } 4. public int? CasaDeRetorno { get; private set; } // Usado para casas de retorno 5. 6. public Casa(TipoCasa tipo, int? casaDeRetorno = null) 7. { 8. Tipo = tipo; 9. CasaDeRetorno = casaDeRetorno; 10. } 11. } Figura 8 – Classe Casa A linha 3 define uma propriedade chamada Tipo. TipoCasa é o tipo enumerado (definido na figura 7), que lista possíveis tipos de casa no tabuleiro (como Normal, Evento, Duelo etc.). Modificador public significa que qualquer parte do código pode acessar essa propriedade, enquanto private set indica que somente a própria classe Casa pode alterar seu valor. Criamos também outra propriedade na linha 4, chamada CasaDeRetorno, e int? significa que pode armazenar um número inteiro ou ser nulo (null), sendo usado em casas especiais que enviam o jogador de volta a outra casa. 25 PROGRAMAÇÃO ORIENTADA A OBJETOS II Finalmente, da linha 6 à 10 definimos o construtor da classe Casa. Lembre-se: construtor é um método especial chamado quando criamos um novo objeto da classe; ele configura o objeto com valores iniciais. O método tem dois parâmetros de entrada: • TipoCasa tipo: parâmetro obrigatório ao criar um objeto Casa. Ele define o tipo da casa. • int? casaDeRetorno = null: parâmetro opcional. Se não for fornecido, seu valor será null. Ele indica a casa para a qual um jogador deve retornar, se aplicável. As linhas 8 e 9 atribuem os valores recebidos pelos parâmetros às propriedades da classe. Isso significa que, quando criamos um objeto Casa, definimos seu tipo e opcionalmente uma casa de retorno. Programadas as casas, podemos montar o tabuleiro: o código apresentado na figura 9 ilustra o tabuleiro no qual os jogadores, representados por personagens como D’Artagnan e Athos, avançam por ele nos diferentes tipos de casa, enfrentando eventos variados e duelos. 1. class Tabuleiro 2. { 3. private int numeroDeCasas; 4. private List casas; 5. 6. // Adicionar um getter público para numeroDeCasas 7. public int NumeroDeCasas 8. { 9. get { return numeroDeCasas; } 10. } 11. 12. // Construtor do Tabuleiro 13. public Tabuleiro(int numeroDeCasas) 14. { 15. this.numeroDeCasas = numeroDeCasas; 16. casas = new List(); 17. InicializarCasas(); 26 Unidade I 18. } 19. private void InicializarCasas() 20. { 21. // Adiciona casas vazias ou com ações especificadas 22. for (int i = 1; i casas.Count) 77. { 78. throw new InvalidOperationException(“A posição do jogador está fora do tabuleiro.”); 79. } 80. return casas[posicao – 1]; // Ajuste para índice baseado em zero 81. } 82. 83. } Figura 9 – Classe Tabuleiro A lógica da classe Tabuleiro é fundamental para o jogo funcionar, sendo responsável por definir o layout e as regras do tabuleiro, composto por um número especificado de casas (numeroDeCasas), cada uma com um tipo definido (TipoCasa). Esses tipos variam entre normal, evento, escola de esgrima, duelo de honra e outros, influenciando diretamente o desenrolar do jogo a cada turno. A decisão de incluir um getter público (linha 7) para uma propriedade privada (linha 3) na classe Tabuleiro é um exemplo clássico de encapsulamento, pilar central da POO. Essa abordagem – que pode parecer menos direta do que simplesmente tornar a propriedade pública – traz consigo uma série de benefícios sutis, mas poderosos para a estrutura e integridade do seu código. Comecemos considerando o que encapsulamento realmente significa. Em seu ser cerne está a ideia de esconder detalhes internos de uma classe do mundo externo, expondo apenas o que é necessário. Ao manter a propriedade numeroDeCasas privada e fornecer um getter público, controlamos rigorosamente como essa informação é acessada por outras partes do programa. Isso significa que, enquanto outras classes podem ler o valor de numeroDeCasas, elas não podem modificá-lo diretamente. É uma maneira de proteger os dados internos da classe Tabuleiro contra alterações inesperadas ou indesejadas, garantindo assim a integridade do estado do tabuleiro durante o jogo. Essa proteção não é apenas uma questão de segurança de dados, mas também de manutenção e flexibilidade do código. Imagine que no futuro mude a lógica para determinar o número de casas no tabuleiro; se numeroDeCasas fosse propriedade pública, qualquer alteração na sua lógica de cálculo exigiria mudanças em todas as partes do código que a acessam. Com um getter, no entanto, essa mudança seria encapsulada dentro da classe Tabuleiro. O restante do seu programa não precisaria saber como numeroDeCasas é calculado ou armazenado; ele só precisa saber que pode obter esse valor através do getter, tornando seu código muito mais fácil de manter e evoluir. Além disso, um getter abre a porta para possíveis validações ou transformações dos dados quando acessados. 29 PROGRAMAÇÃO ORIENTADA A OBJETOS II Contrastando com a abordagem de uma propriedade pública – na qual numeroDeCasas seria exposto diretamente –, a abordagem atual oferece uma camada adicional de proteção. Com uma propriedade pública, qualquer parte do código poderia alterar o valor de numeroDeCasas, potencialmente levando a estados incoerentes ou a bugs difíceis de rastrear. A abordagem adotada,com um campo privado e um getter público, assegura que a classe Tabuleiro tenha controle total sobre seu estado interno. Portanto, ao escolher um getter público para a propriedade privada numeroDeCasas, adotamos uma prática de design que protege os dados internos da classe ao mesmo tempo que oferece flexibilidade, segurança e facilidade de manutenção para o código. Ao encapsular a propriedade dessa maneira, a classe Tabuleiro mantém um controle rigoroso sobre como seus dados internos são acessados e manipulados, garantindo que o estado do tabuleiro seja sempre apresentado de forma consistente e confiável ao resto do programa. Na linha 13 o construtor da classe Tabuleiro recebe o número total de casas como parâmetro e inicializa a lista de casas. Em C# as listas são parte essencial do trabalho com coleções de dados; tecnicamente, lista é a implementação do conceito de uma lista dinâmica, fornecida pela classe List no namespace System.Collections.Generic. Essa estrutura é amplamente utilizada devido à sua versatilidade e eficiência em armazenar e manipular sequências de elementos. Listas em C# são fortemente tipadas graças aos recursos genéricos da linguagem. O tipo T na declaração List representa o tipo de elementos que a lista pode conter; isso significa que, se você criar uma List, por exemplo, ela só poderá conter inteiros. Essa tipagem forte ajuda a evitar erros em tempo de execução e melhora a legibilidade e a manutenção do código. Outro aspecto importante das listas é a variedade de métodos disponíveis para manipular dados, incluindo adicionar (Add), remover (Remove), encontrar (Find), ordenar (Sort) e muitos outros. Listas também suportam iteração, permitindo que os desenvolvedores percorram seus elementos facilmente usando loops foreach, por exemplo. É interessante discutir a decisão de usar um parâmetro numeroDeCasas na classe Tabuleiro, mesmo quando o jogo tem número fixo de 60 casas, o que inicialmente pode parecer uma inclusão desnecessária. No entanto, essa abordagem traz várias vantagens do ponto de vista do design de software e da flexibilidade do código. Ao parametrizar o número de casas, o código fica mais flexível e adaptável a mudanças. Se no futuro for decidido alterar o jogo para ter, digamos, 80 casas, essa mudança pode ser feita facilmente, sem necessidade de alterar a lógica interna da classe Tabuleiro. Essa abordagem segue o princípio de design de software conhecido como open/closed principle, para o qual as classes devem estar abertas para extensão, mas fechadas para modificação. Além disso, ao definir explicitamente o número de casas como parâmetro, o código se torna mais claro e autoexplicativo. Outros desenvolvedores que trabalharem com ele poderão entender rapidamente que o tamanho do tabuleiro é uma característica configurável. Por fim, ao permitir que o número de casas seja definido como parâmetro, fica mais fácil testar a classe Tabuleiro; por exemplo, para testes unitários, um tabuleiro menor pode ser criado para testar certas funcionalidades sem precisar percorrer todas as 60 casas. 30 Unidade I A função InicializarCasas (linha 17) é chamada para preencher a lista, onde cada casa é instanciada com um tipo específico, a depender de sua posição no tabuleiro. Isso é feito por uma série de instruções switch-case, que associam tipos específicos de casas a certas posições no tabuleiro. Por exemplo, casas numeradas 5, 20, 35, 46 e 55 (linha 26 à 30) são definidas como casas de evento (linha 31). Note o uso do método Add que citamos e observe também que, ao adicionar algumas casas (por exemplo, na linha 56), a chamada ao construtor colocou um número como parâmetro. Esse número é a casa de retorno. Além disso, o método ObterCasa (linha 73) da classe Tabuleiro é crucial. Ele recebe a posição do jogador no tabuleiro e retorna a casa correspondente a essa posição. Essa função é essencial para determinar eventos ou ações que ocorrem quando um jogador cai em determinada casa. A instrução throw no método ObterCasa da classe Tabuleiro é um exemplo clássico de como a programação defensiva é implementada em C#. A princípio, pode parecer estranho incluir uma checagem para uma condição que, sob circunstâncias normais, nunca deveria ocorrer – um jogador saindo dos limites do tabuleiro. No entanto, essa abordagem é justificada e benéfica em diversos aspectos. Em primeiro lugar, a programação defensiva, que prepara o código para lidar com situações inesperadas, é prática recomendada. No caso, embora a lógica sugira que os jogadores sempre estejam dentro dos limites do tabuleiro, erros inesperados podem ocorrer. Pode haver bugs no código ou interações imprevistas entre diferentes partes do jogo que, por alguma razão, movam alguém para uma posição inválida. Ao usar throw para lançar uma exceção quando isso acontece, o código fornece um meio de identificar rapidamente tais problemas. Adicionalmente, ao lidar com uma exceção de maneira explícita, o jogo se torna mais robusto e confiável. Isso é particularmente importante em ambientes onde a estabilidade é crucial. Se um jogador, por acidente ou bug, for colocado fora dos limites do tabuleiro, essa exceção interromperá o fluxo normal do programa e destacará o problema, permitindo que seja corrigido mais facilmente do que se o programa continuasse a operar em estado inválido. Além disso, throw é uma forma de comunicar claramente aos outros desenvolvedores sobre as expectativas do código. Quando outro desenvolvedor lê o código da classe Tabuleiro, a presença da exceção sinaliza que a posição de um jogador deve sempre estar dentro de um intervalo específico. Isso aumenta a legibilidade e a manutenção do código, facilitando o entendimento e o trabalho com ele no futuro. Por fim, num jogo complexo, com várias lógicas e interações, não é possível prever todas as situações; há sempre uma chance, mesmo que pequena, de um jogador acabar fora dos limites do tabuleiro devido a uma sequência rara e não antecipada de eventos. A exceção serve como uma rede de segurança para tais casos, garantindo que o jogo não perca sua lógica interna. Para ilustrar a importância dessa rede, considere que há um problema comum em jogos conhecido como clipping (ou travamento de objetos), em que personagens ou entidades ficam presas em objetos do ambiente, como paredes. Isso geralmente se deve a falhas no sistema de colisão ou na física do jogo, e possivelmente as exceções não foram adequadamente colocadas no código. 31 PROGRAMAÇÃO ORIENTADA A OBJETOS II A seguir, alguns jogos famosos que contêm problemas do tipo: • Franquia Grand theft auto (GTA): em vários jogos da série evidenciam-se personagens ou veículos presos em edifícios ou outros objetos. Isso se deve muitas vezes à complexidade de seu mundo aberto e à liberdade de ação oferecida. • Minecraft: em algumas ocasiões, especialmente em versões mais antigas ou em servidores com lag, os jogadores podem experimentar clipping através do terreno, que às vezes os deixa presos. • World of Warcraft: como massive multiplayer online (MMO), teve sua parcela de clipping – os jogadores ficavam presos em texturas ou terrenos do jogo. Isso geralmente ocorria em áreas recém-introduzidas ou após grandes atualizações. É importante entender que a classe Tabuleiro age como estrutura de dados que mapeia o estado do tabuleiro do jogo. Ela não apenas armazena informações sobre cada casa, mas também fornece métodos para interagir com o tabuleiro, como ObterCasa, essencial para avançar o jogo a cada turno. Em termos técnicos, a classe Tabuleiro exemplifica como encapsular dados (no caso, o layout do tabuleiro) e comportamento (como obter uma casa baseada na posição do jogador) de maneira que sejam fáceis de gerenciar e manter. O encapsulamento, um dos princípios da POO, ajuda a manter o código organizado e flexível, facilitando modificações futuras, como adição de novos tipos de casa ou alteração do layout do tabuleiro. Se já temos o tabuleiro,vamos às cartas. Iniciaremos pela classe CartaDeEvento (figura 10), responsável por introduzir eventos aleatórios que afetam o estado e as decisões dos jogadores. Ela denota o dinamismo e a imprevisibilidade inerentes ao jogo. 1. class CartaDeEvento 2. { 3. public string Descricao { get; private set; } 4. public Action Efeito { get; private set; } 5. 6. public CartaDeEvento(string descricao, Action efeito) 7. { 8. Descricao = descricao; 9. Efeito = efeito; 10. } 11. } Figura 10 – Classe CartaDeEvento CartaDeEvento é uma classe com duas propriedades principais: Descricao e Efeito. 32 Unidade I Quadro 4 – Cartas e suas descrições e efeitos no jogador Carta Descrição/efeito 1 Desafio do cardeal: você foi pego em uma emboscada pelos guardas de Richelieu. Perca uma rodada enquanto escapa 2 O olhar de Constance: Constance Bonacieux, por quem você se apaixona, lhe deu um olhar encorajador. Avance duas casas com a alegria do amor 3 Intriga da rainha: a rainha Ana da Áustria confiou a você uma missão secreta para recuperar diamantes. Avance três casas e ganhe uma ficha de honra 4 Duelo com mosqueteiro: um desentendimento o levou a um duelo com um de seus camaradas. Volte uma casa e perca uma ficha de honra 5 Trama de Milady: a sedutora e astuta Milady de Winter tramou contra você. Volte duas casas enquanto desvenda a armadilha 6 Auxílio de Buckingham: o duque de Buckingham, aliado da rainha, oferece ajuda em sua jornada. Avance duas casas com o apoio dele 7 Festa em Paris: há uma festa extravagante em Paris, e você é o convidado de honra. Perca uma rodada enquanto celebra 8 Missão secreta: uma carta selada da rainha lhe é entregue, pedindo uma missão sigilosa. Avance três casas com sua nova responsabilidade 9 Emboscada na estrada: enquanto viaja, você é emboscado por inimigos desconhecidos. Volte duas casas enquanto escapa do perigo 10 Defesa do rei: Luís XIII está sob ameaça e você o defendeu bravamente. Avance duas casas e ganhe duas fichas de honra 11 Convite do cardeal: Richelieu, num movimento inesperado, convida você para um banquete. Perca uma rodada devido à cautela, mas ganhe uma ficha de fortuna pelas oportunidades que surgem 12 Refúgio num mosteiro: escapando de perseguidores, você se refugia num mosteiro. Perca uma rodada, mas ganhe uma ficha de honra pela proteção oferecida 13 Treinamento intensivo: você treina duro com os companheiros mosqueteiros. Avance uma casa e ganhe uma ficha de fortuna 14 Intriga na corte: rumores sobre você circulam na corte. Volte uma casa, mas ganhe uma ficha de fortuna pelo aprendizado com a situação 15 Mensagem de socorro: Constance lhe envia uma mensagem urgente. Avance duas casas enquanto se apressa em ajudá-la e ganhe uma ficha de honra 16 Dádiva do duque: o duque de Buckingham presenteia você com ricas vestimentas. Ganhe duas fichas de fortuna 17 Perseguição nas ruas: envolvido numa perseguição pelas ruas de Paris, você escapa por pouco. Perca uma ficha de honra, mas ganhe uma de fortuna pela experiência 18 Rivalidade na guarda: um rival na Guarda do Rei desafia você. Volte duas casas enquanto lida com a situação, mas ganhe uma ficha de fortuna pelo aprendizado 19 Conspiração desvendada: você descobre uma conspiração contra os mosqueteiros. Avance duas casas e ganhe uma ficha de honra 20 Perdido numa floresta: durante uma missão, você se perde em uma floresta densa. Volte duas casas enquanto encontra seu caminho 21 Presente da rainha: a rainha Ana da Áustria lhe dá um presente por sua lealdade. Ganhe três fichas de fortuna A propriedade Descricao (linha 3) é uma string que fornece uma narrativa ou contexto para o evento, enquanto Efeito (linha 4) é um delegado do tipo Action, particularmente interessante pois encapsula a lógica que será aplicada ao jogador quando a carta for usada. Essencialmente, efeito é uma função que recebe um objeto Jogador e executa ações que modificam seu estado, como alterar posição, fichas de fortuna ou honra, entre outras. A flexibilidade da classe CartaDeEvento reside na sua capacidade de encapsular diferentes tipos de lógica dentro de Efeito, tornando cada carta única em termos de impacto no jogo. Isso é feito por expressões lambda passadas no construtor da CartaDeEvento, permitindo uma variedade de efeitos possíveis, desde mover o jogador para frente ou para trás no tabuleiro, alterar suas fichas de fortuna 33 PROGRAMAÇÃO ORIENTADA A OBJETOS II ou honra, até forçar o jogador a perder um turno. Veremos essas expressões quando analisarmos a classe Baralho, que instancia os objetos CartaDeEvento e, portanto, aciona o construtor passando cada expressão lambda desejada como parâmetro. Delegado é um tipo que representa referências a métodos com uma lista de parâmetros e um tipo de retorno específicos. Pode ser pensado como um tipo de ponteiro de função, mas com segurança de tipo e mais integrado ao sistema de tipos do C#. Delegados são usados para passar métodos como argumentos para outros métodos, permitindo alta flexibilidade e reusabilidade do código. Action é um tipo de delegado genérico fornecido pelo framework .NET que pode encapsular um método que recebe um único parâmetro do tipo T e não retorna um valor (ou seja, seu tipo de retorno é void). No seu caso, Action representa um delegado que pode apontar para qualquer método que aceite um único argumento do tipo Jogador e não retorne nada. Dentro da classe CartaDeEvento, a propriedade Efeito é do tipo Action. Isso significa que ela pode armazenar uma referência a qualquer método que corresponda à assinatura especificada: um método que leva um Jogador como argumento e não retorna um valor. Por exemplo, pode ser um método que modifique a posição do jogador no tabuleiro, altere sua quantidade de fichas de honra ou fortuna, ou qualquer outra ação que afete o estado do jogador. Observação Além do Action, projetado para métodos que não retornam valor, existem os delegados Func e Predicate, cada um com propósitos específicos: Func – delegado genérico, representa um método que recebe um ou mais parâmetros e retorna um valor. O último tipo genérico especifica o tipo de retorno do método. Por exemplo, Func representa um método que recebe dois inteiros (int) como parâmetros e retorna um booleano (bool). A classe Func pode suportar de 0 a 16 parâmetros de entrada; Predicate – especialização do Func, o Predicate é um delegado usado para expressar um teste que retorna um valor booleano. Ele recebe um único parâmetro do tipo T e então retorna um bool. É frequentemente usado em métodos de busca e filtragem, como os métodos Find ou Exists em listas, em que é necessário testar cada elemento para uma condição específica. Esses delegados são parte do namespace System, e sua disponibilidade e uso generalizado na plataforma .NET simplificam muito a passagem de métodos como argumentos, tornando o código mais flexível e reutilizável. São essenciais para programação funcional em C# e são amplamente usados em LINQ e para manipular eventos e callbacks. Por exemplo: Action pode representar um método que recebe um inteiro e não retorna nada; Func representa um método que recebe um 34 Unidade I inteiro e retorna um double; Predicate pode ser um método que recebe uma string e retorna um booleano, indicando se a string satisfaz uma condição particular. Esses delegados tornam o código mais limpo e expressivo, além de facilitar operações como manipulação de coleções, programação assíncrona e construção de pipelines de processamento de dados. O uso de Action como tipo para a propriedade Efeito oferece várias vantagens. Em primeiro lugar, permite que diferentes ações sejam atribuídas a diferentes cartas de evento; e cada carta de evento pode ter efeito único sem precisar criar uma nova classe para cada tipo de ação. Em segundo lugar, permite encapsulamento e clareza,