Baixe o app para aproveitar ainda mais
Prévia do material em texto
1 FACULDADE DE COMPUTAÇÃO E INFORMÁTICA BACHARELADOS EM CIÊNCIA DA COMPUTAÇÃO E SISTEMAS DE INFORMAÇÃO E TECNOLOGIA EM ANÁLISE E DESENVOLVIMENTO DE SISTEMAS Linguagem de Programação I – SEMANA 06 TEORIA: RELACIONAMENTOS/DEPENDÊNCIAS ENTRE CLASSES e COLEÇÕES DE OBJETOS Nossos objetivos nesta aula são: Introduzir os fundamentos de Desenvolvimento Orientado a Testes (TDD) para resolução dos exercícios Iniciar o estudo de relacionamento/dependências entre classes através de coleções de objetos Mapear coleções de objetos em vetores simples de Java A referência para esta aula é: Capítulo 12 Seção 2.3 (Relacionamentos entre Classes) e Capítulo 7 (Arrays e Listas de Arrays) do nosso livro- texto: Horstmann, C. Conceitos de Computação com Java. 5.ed. Porto Alegre: Bookman, 2009. Não deixem de ler estes capítulos antes e após a aula! DESENVOLVIMENTO ORIENTADO A TESTES Desenvolvimento Orientado a Testes (ou TDD - Test Driven Development) é uma prática muito utilizada no desenvolvimento de software. Esta prática consiste em utilizar os requisitos da solução (ou seja, a descrição daquilo que a solução deve fazer) para escrever primeiro as classes de teste, e depois desenvolver as classes da solução. Ou seja, tendo um teste especificado (em JUnit, por exemplo), construímos uma classe que passe no teste. Isto, a muito grosso modo, é o que chamamos de Desenvolvimento Orientado a Testes (TDD-Test Driven Development). Trata-se de um método ágil de desenvolvimento de software muito utilizado no mercado. Vamos criar a classe de teste utilizando o framework JUnit. Esse framework é disponibilizado como um pacote (.JAR) para a linguagem Java: 2 Para utilizar este framework, basta criar a classe ExemploTest conforme o código abaixo (no NetBeans, escolha selecione o menu Arquivo -> Novo Arquivo -> Categoria Testes de Unidade -> Tipos de Arquivo TesteJUnit e a seguir digite o código): EXERCÍCIO TUTORIADO Construa uma classe Exemplo que passe no teste em JUnit abaixo: import org.junit.Test; import org.junit.Assert; public class ExemploTest { @Test public void testeArea() { Retangulo r = new Retangulo(5,10,20,30); Assert.assertEquals (600, r.area()); } @Test public void testeTransladar() { Retangulo r = new Retangulo(5,10,20,30); Assert.assertEquals (5, r.getX()); r.transladar(10,10); Assert.assertEquals (15, r.getX()); Assert.assertEquals (20, r.getY()); } } Vamos ver um exemplo no contexto do nosso problema da conta bancária: Suponha que quem encomendou o sistema de gerenciamento da conta bancária tenha definido que "a operação de saque (withDraw) em uma conta deverá debitar o valor de saque do saldo da conta". Para cumprir este requisito, a nossa classe BankAccount possui o método withDraw, que é descrito como: 3 - nome do método: withDraw - descrição: decrementa o valor a ser sacado (amount) do saldo (balance) da conta - parâmetros: amount (valor a ser sacado) - retorno: nenhum Uma maneira de testar se o método withDraw está correto é: - criar uma instância de BankAccount com um determinado saldo inicial; - efetuar um saque; - consultar o saldo; - verificar se o saldo atual é igual ao valor (saldo inicial - saque); se forem iguais, a implementação está correta. import org.junit.Test; import org.junit.Assert; public class BankAccountTest { @Test public void withDrawTest() { double saldoInicial = Math.random() * 1_000_000; BankAccount conta = new BankAccount(saldoInicial); double saque = Math.random() * 1_000_000; conta.withDraw(saque); double saldoEsperado = saldoInicial - saque; double saldoObtido = conta.getBalance(); Assert.assertEquals(saldoEsperado, saldoObtido, 0.01); } } Note que o programa de teste gera valores pseudoaleatórios para o saldo inicial e para o saque. Assert.assertEquals é um método na biblioteca JUnit que verifica se o valor esperado coincide com o valor obtido (dentro de uma margem de erro, que neste caso foi especificado como 0.01). Crie a classe BankAccount (com o código desenvolvido em aulas anteriores) no mesmo projeto e execute o teste (no NetBeans, pressione o botão direito do mouse no arquivo BankAccountTest e selecione Testar Arquivo). Verifique que o resultado do teste é de SUCESSO. Para verificar o que acontece em caso de falha, introduza um erro na implementação do método withDraw (por exemplo, faça a soma em lugar da subtração) e execute novamente o teste. 4 EXERCÍCIO TUTORIADO Suponha que quem encomendou o sistema de gerenciamento da conta bancária tenha definido que "a operação de depósito (deposit) em uma conta deverá adicionar o valor de depósito ao saldo da conta". Para cumprir este requisito, a nossa classe BankAccount possui o método deposit, que é descrito como: - nome do método: deposit - descrição: adiciona o valor do depósito (amount) ao saldo (balance) da conta - parâmetros: amount (valor a ser depositado) - retorno: nenhum Acrescente um método à classe BankAccountTest para efetuar o teste desta operação. import org.junit.Test; import org.junit.Assert; public class BankAccountTest { @Test public void withDrawTest() { // método desenvolvido no exercício anterior } @Test public void depositTest() { } } 5 EXERCÍCIO COM DISCUSSÃO EM DUPLAS Suponha que quem encomendou o sistema de gerenciamento da conta bancária tenha definido que "os números das contas do banco devem ser gerados de forma sequencial, à medida que as instâncias da conta são criadas". Para cumprir este requisito, a nossa classe BankAccount gera o número da conta (accountNumber) no seu construtor. Este número da conta pode ser consultado após a instanciação utilizando o método getAccountNumber, que é descrito como: - nome do método: getAccountNumber - descrição: retorna o número da conta - parâmetros: nenhum - retorno: número da conta Acrescente um método à classe BankAccountTest para efetuar o teste desta operação. import org.junit.Test; import org.junit.Assert; public class BankAccountTest { @Test public void withDrawTest() { // método desenvolvido no exercício anterior } @Test public void depositTest() { // método desenvolvido no exercício anterior } @Test public void accountNumberTest() { } } 6 RELACIONAMENTOS e DEPENDÊNCIAS ENTRE CLASSES Até o presente momento, trabalhamos somente com uma classe BankAccount. Porém, para sistemas mais complexos, várias classes podem ser necessárias para representá-los corretamente. Quando várias classes representam um sistema e interagem entre si, podemos ter relacionamentos ou dependências, ou ambos, entre estas classes. Um relacionamento de uma classe A para uma classe B ocorre quando, na definição da parte estrutural da classe A for necessária a definição da classe B. Por exemplo, vamos considerar novamente parte da definição estrutural da classe BankAccount, onde temos um número de conta, uma senha, nome do correntista e saldo da conta: class BankAccount{ private int accountNumber; // número da conta privateString password; // senha da conta private String owner; // proprietário da conta private double balance; // saldo da conta ... } Para que a classe BankAccount pudesse ser definida estruturalmente, foi necessária a classe String. Neste contexto, temos dois relacionamentos direcionados de BankAccount para String (um para senha e outro para nome). Um relacionamento pode ser classificado em duas grandes categorias: o em relação a sua semântica (significado): associação, agregação, composição ou herança o em relação a sua multiplicidade: 1:1 (lê-se um-para-um), 1:N (um-para-muitos) ou N:N (muitos-para-muitos) Os vários tipos de relacionamentos podem ser caracterizados por diagramas relacionais: Relacionamento Diagrama Relacional Semântica associação A classe A está relacionada com a classe B e vice-versa. agregação Tipo especial de associação do tipo todo- parte, onde dizemos que B é parte de A. Na agregação, a classe A não precisa gerenciar o ciclo de vida de B, ou seja, B 7 não precisa ser criado, manipulado ou destruído por A. composição Tipo mais forte de agregação, onde a classe A gerencia o ciclo de vida de B, ou seja, A cria, manipula e destrói B. herança Tipo especial de associação do tipo generalização- especialização, onde B é uma versão mais especializada da classe A, mais genérica. Estudaremos a relação de herança na próxima semana. No exemplo dos relacionamentos entre BankAccount e String, podemos pensar os dois relacionamentos da seguinte forma: o password: composição de multiplicidade 1:1 o owner: composição de multiplicidade 1:1 Assim, nosso construtor para a classe BankAccount vai tomar a seguinte forma: public BankAccount (int accountNumber, String password, String owner, double balance){ this.accountNumber=accountNumber; if (password==null) // se ainda não houver uma senha password = “123” ; // cria uma senha-padrão else this.password=password; if (owner==null) // Se não houver nome criado, utiliza um nome-padrão owner=”Nome não-informado”; else this.owner=owner; this.balance = balance; } Uma dependência de uma classe A para uma classe B ocorre quando, na definição da parte comportamental da classe A for necessária a definição da classe B. É importante observar que a parte comportamental inclui, além dos métodos, os construtores da classe. Como exemplo, suponha que façamos a seguinte alteração do construtor da classe BankAccount para incluir mensagens aos usuários sobre as ações tomadas quando a senha e/ou nome do correntista não forem fornecidos: 8 public BankAccount (int accountNumber, String password, String owner, double balance){ this.accountNumber=accountNumber; if (password==null) { // se ainda não houver uma senha System.out.println(“Senha não informada. Será utilizada a senha-padrão”); password = “123” ; // cria uma senha-padrão } else this.password=password; if (owner==null) { // Se não houver nome criado, utiliza um nome-padrão System.out.println(“Senha não informada. Será utilizada a senha-padrão”); owner=”Nome não-informado”; } else this.owner=owner; this.balance = balance; } Neste exemplo, para que o construtor possa funcionar corretamente, dependemos da classe System para nos fornecer o atributo out e, a partir dele, acessar o método println. Além disto, observem que também dependemos da classe String para definir os parâmetros no construtor. O último problema está vinculado a dois conceitos importantes em POO: coesão e acoplamento. Coesão está vinculado ao quão concisa é uma classe, ou seja, qual a variabilidade de responsabilidades atribuídas. Classes com uma variação muito grande de responsabilidades tendem a ser menos coesas que aquelas com um número reduzido de responsabilidades. Acoplamento está vinculado à quantidade de dependências existentes estre classes. Um baixo nível de acoplamento é importante nos processos de manutenção/alteração de classes, pois a quantidade de testes necessários numa eventual modificação tende a ser menor. Assim, retomando o princípio de sempre buscarmos implementações com alta coesão e baixo acoplamento. 9 Adicionalmente, sempre que fizermos uma modificação em uma classe B da qual outras classes dependem, vamos realizar (além do teste unitário da classe B) testes para verificar se as classes que dependem de B ainda continuam funcionando. RELACIONAMENTOS COM MULTIPLICIDADE 1:1 Relacionamentos 1:1 são bastante simples de se implementar. A seguir, descreveremos como implementá-los como associação, agregação e composição. Diagrama Relacional IMPLEMENTAÇÃO associação Podemos colocar A como um atributo de B ou B como um atributo de A. public class A{ ou public class B { B b ; A a ; public A(B b){ public B(A a){ this.b=b; this.a=a; } } } } agregação A única possibilidade é colocar B como um atributo de A, pois B é parte de B: public class A{ B b ; public A(B b){ this.b=b; } } 10 composição Muito semelhante à implementação da agregação, porém B será um atributo privado de A e devemos garantir que B exista: public class A{ private B b ; public A(B b){ if (b==null) // se b não existe, cria uma instância this.b=createB(); else this.b=b; } } EXERCÍCIO COM DISCUSSÃO EM DUPLAS Refatore a classe BankAccount para conter um novo atributo para armazenar o tipo de conta. Serão possíveis dois tipos: CONTA-CORRENTE ou POUPANÇA. Caso o tipo de conta não seja informado, assumir o tipo CONTA-CORRENTE como padrão. Mostre qual o tipo de relacionamento você escolheu para fazer isto e justifique a sua escolha. class BankAccount{ private int accountNumber; // número da conta private String password; // senha da conta private String owner; // proprietário da conta private double balance; // saldo da conta public BankAccount (int accountNumber, String password, String owner, double balance) { this.accountNumber=accountNumber; if (password==null) // se ainda não houver uma senha password = “123” ; // cria uma senha-padrão else this.password=password; if (owner==null) // Se não houver nome criado, utiliza um nome-padrão owner=”Nome não-informado”;else this.owner=owner; this.balance = balance; } 11 RELACIONAMENTOS COM MULTIPLICIDADE 1:N Relacionamentos com multiplicidade 1:N, entre uma classe A e uma classe B, são interpretados da seguinte forma: para cada instância da classe A, teremos N instâncias da classe B. Normalmente, relacionamento 1: N são mapeados da seguinte forma: o criarmos um container (vetor, por exemplo) de objetos B como um atributo da classe A o dependendo do tipo de relacionamento (associação, agregação ou composição), aplicamos regras específicas para inserção de cada instância de B no vetor Para dar um exemplo destes mapeamos, vamos supor que queiramos criar uma classe Bank para armazenar todas as contas bancarias e que permita as seguintes operações: o adicionar um conta o calcular o saldo total de todas as contas sobre o controle do banco o procurar por uma determinada conta, dado o seu número o procurar pela conta bancária com o maior saldo possível o contar quantas contas possuem saldo acima de um determinado valor Na nossa implementação, vamos optar por um relacionamento do tipo agregação, para não ter que controlar o ciclo de vida das contas bancárias, deixando a instanciação das contas bancarias a serviço de elementos externos à classe Bank. Como primeiro passo, vamos considerar a seguinte estrutura básica para nossa classe: public class Bank { private BankAccount accounts[]; // contas bancárias private int last; // último índice do banco inserido public Bank(){ accounts=new BankAccount[100]; // banco com, no máximo, 100 contas bancárias last=0; } ... } 12 EXERCÍCIO TUTORIADO Implemente o método addAccount para inserir uma conta na classe Bank: public class Bank { private BankAccount accounts[]; // contas bancárias private int last; // último índice do banco inserido public Bank(){ accounts=new BankAccount[100]; // banco com, no máximo, 100 contas bancárias last=0; } public void addAccount(BankAccount a){ // Insere conta na classe Bank } ... } 13 EXERCÍCIO COM DISCUSSÃO EM DUPLAS Implemente o método getTotalBalance, que devolve o saldo total de todas as contas bancárias: public class Bank { private BankAccount accounts[]; // contas bancárias private int last; // último índice do banco inserido public Bank(){ accounts=new BankAccount[100]; // banco com, no máximo, 100 contas bancárias last=0; } public void addAccount(BankAccount a){ // Insere conta na classe Bank ... } public double getTotalBalance(){ // Calcula o saldo total de todas as contas inseridas no Banco } } 14 EXERCÍCIO COM DISCUSSÃO EM DUPLAS Implemente o método find, que recebe o número de uma conta bancária e devolve o objeto vinculado a ela. Caso tal objeto não exista, devolver null. public class Bank { private BankAccount accounts[]; // contas bancárias private int last; // último índice do banco inserido public Bank(){ accounts=new BankAccount[100]; // banco com, no máximo, 100 contas bancárias last=0; } public void addAccount(BankAccount a){ // Insere conta na classe Bank ... } public double getTotalBalance(){ // Calcula o saldo total de todas as contas inseridas no Banco ... } public BankAccount find(int accountNumber){ } } 15 EXERCÍCIO COM DISCUSSÃO EM DUPLAS Implemente o método getMaximum, que devolve a conta bancária com o maior saldo possível. Caso tal conta não exista, devolver null. public class Bank { private BankAccount accounts[]; // contas bancárias private int last; // último índice do banco inserido public Bank(){ accounts=new BankAccount[100]; // banco com, no máximo, 100 contas bancárias last=0; } public void addAccount(BankAccount a){ // Insere conta na classe Bank ... } public double getTotalBalance(){ // Calcula o saldo total de todas as contas inseridas no Banco ... } public BankAccount find(int accountNumber){ // Devolve a conta vinculada a um número de conta ... } public BankAccount getMaximum(){ // Devolve a conta com o maior saldo possível } } 16 EXERCÍCIO COM DISCUSSÃO EM DUPLAS Implemente o método count, que recebe um limite e devolve a quantidade de contas bancárias cujo saldo seja maior ou igual a este limite. public class Bank { private BankAccount accounts[]; // contas bancárias private int last; // último índice do banco inserido public Bank(){ accounts=new BankAccount[100]; // banco com, no máximo, 100 contas bancárias last=0; } public void addAccount(BankAccount a){ // Insere conta na classe Bank ... } public double getTotalBalance(){ // Calcula o saldo total de todas as contas inseridas no Banco ... } public BankAccount find(int accountNumber){ // Devolve a conta vinculada a um número de conta ... } public BankAccount getMaximum(){ // Devolve a conta com o maior saldo possível ... } public int count(int limit){ // Calcula o número de contas com saldo superior ou igual a um limite } } 17 ATIVIDADE DE LABORATÓRIO – Etapa 01 Nossa atividade de laboratório irá estender várias funcionalidades da classe Bank, implementada na aula teórica. 1. Inicialmente, queremos ler a informações nas nossas contas bancárias de um arquivo. Para fazer isto, vamos precisar aprender um pouco sobre manipulação de arquivos em Java. Pequeno Tutorial de Leitura/Gravação de Arquivos em Java O processo de leitura e gravação de dados em arquivos, em Java, é feito através de streams. Existem diversas classes que implementam streams no pacote java.io. Neste primeiro contato, utilizaremos as classes BufferedReader e BufferedWriter. Vamos iniciar pela leitura e vamos supor que tenhamos um arquivo chamado entrada.txt com a seguinte estrutura: entrada.txt 5 Maria do Carmo José da Silva Pedro Moreira Carlos Alberto Evandro Mesquita A primeira linha deste arquivo nos informa a quantidade de nomes que vamos ler (5). A partir disto, temos 5 nomes para ler. Um trecho de código que lê e exibe estes nomes em tela é mostrado abaixo: import java.io.*; //pacote de classes de stream ...... try{ BufferedReader r = new BufferedReader (new FileReader(“entrada.txt”)); // abre arquivo if (r!=null) { // se arquivo pôde ser aberto String linha = r.readLine(); // quantidade de dados a serem lidosint q = Integer.parseInt(linha); for(int i = 0; i < q; i++){ linha = r.readLine(); } r.close(); // fecha arquivo de entrada } catch(Exception e){ System.exit(-1); } 18 Como operações de I/O podem gerar exceções, colocamos todas as operações de arquivos dentro um bloco try-catch. Caso ocorra alguma exceção dentro do bloco try, a execução é transferida para o bloco catch. Para o processo de gravação, utilizaremos a classe BufferedWriter. Suponha, no exemplo anterior, que queiramos gravar todos os nomes lidos em um arquivo chamado saída.txt, contendo os nomes em maiúsculas. Para isto, fazemos: try{ BufferedReader r=new BufferedReader(new FileReader(“entrada.txt”)); // leitura BufferedWriter w=new BufferedWriter(new FileWriter(“saida.txt”)); // gravação if (r!=null && w!=null ) { // arquivos puderam ser abertos String linha = r.readLine();// quantidade de dados a serem lidos w.write (linha); // grava quantidade no arquivo de saída w.newLine(); int q = Integer.parseInt(linha); for(int i = 0; i < q; i++){ linha = r.readLine(); w.write (linha.toUpperCase()); w.newLine(); } r.close(); // fecha arquivo de entrada w.flush(); // força escrita final de dados no arquivo de saída w.close(); // fecha arquivo de saída } catch(Exception e) { System.exit(-1); } Baseado no tutorial acima, implemente um construtor na classe Bank que receba como entrada o nome de um arquivo contendo informações das contas e preencha o vetor de contas do banco. O formato do arquivo de entrada deverá ser da seguinte forma: Número de contas Número da Conta 1#Senha da Conta 1#Proprietário da Conta 1#saldo da Conta1 Número da Conta 2#Senha da Conta 2#Proprietário da Conta 2#saldo da Conta2 ... Número da Conta N#Senha da Conta N#Proprietário da Conta N#saldo da ContaN Observe que os campos de cada conta estão separados por #. Sugestão: utilize o método split(...) da classe String para separar os campos. 19 class Bank { ... public Bank(String filename){ // Inicializa as contas bancárias com base em um arquivo } } 2. Implemente uma classe principal para visualizar e testar se o construtor dessa classe está correto. 20 EXERCÍCIOS EXTRA-CLASSE 1. Simule um comportamento de um carro. Nessa simulação, um carro pode andar uma quantidade de quilômetros. Considerando que ele inicialmente tenha um tanque vazio, será necessário enchê-lo. Nessa simulação, considere um carro flex (gasolina e álcool). Além disso, sabe-se que ele tem um desempenho de 14 km com um litro de combustível (para esse exercício, considere consumo igual). Por fim, é importante saber quantos litros de combustível ainda restam no tanque. Construa a classe Carro que passe no teste seguinte. import org.junit.Test; import org.junit.Assert; public class CarroTest { @Test public void tanqueDeGasolinaTest() { Carro car = new Carro(); car.encherTanque(new Gasolina(10.0)); car.andar(14.0); Assert.assertEquals(9.0, car.getCombustivel(), 0.001); } @Test public void tanqueDeAlcoolTest() { Carro car = new Carro(); car.encherTanque(new Alcool(10.0)); car.andar(28.0); Assert.assertEquals(8.0, car.getCombustivel(), 0.001); } } 2. Considere o seguinte enunciado: Um empregado contratado por uma empresa pode solicitar um aumento. Esse aumento é calculado dado certa porcentagem. É importante também saber qual o nome e o cargo deste funcionário. Construa a classe Empregado que passe no teste seguinte. 21 import org.junit.Test; import org.junit.Assert; public class EmpregadoTest { @Test public void empregadoTest() { Empregado joao = new Empregado(“Joao da Silva”, “Desenvolvedor”, 2356.98); Assert.assertEquals(“Joao da Silva”, joao.getNome()); Assert.assertEquals(“Desenvolvedor”, joao.getCargo()); Assert.assertEquals(2356.98, joao.getSalario(), 0.001); } @Test public void aumentarSalarioTest() { Empregado joao = new Empregado(“Joao da Silva”, “Desenvolvedor”, 2000.00); joao.aumentarSalario(10.0); Assert.assertEquals(2200.00, joao.getSalario(), 0.001); } }
Compartilhar