Classes e Objetos

Todo o código em POO é colocado dentro de classes. Um bloco de código que define uma classe é semelhante ao seguinte

1
2
3
4
class Class1
{
      // os membros são definidos aqui.
}

A palavra chave “class” define a criação de uma nova classe. Em seguida colocamos o nome a ser dado a essa classe, no caso, Class1.

Para criarmos ou “instanciarmos” objetos de uma determinada classe, utilizamos o operador new.

1
Class1 obj = new Class1();

No código acima, estamos criando um novo objeto da classe Class1 nomeado de “obj”. A partir desta instrução, os membros deste objeto tornam-se disponíveis para uso.

A título de exemplo, vamos criar uma classe simples com alguns membros e utilizá-los para verificar seu funcionamento.

Vamos iniciar escrevendo aplicativos em um editor de texto comum, no caso, o Notepad (Bloco de Notas) do Windows. Para compilar os aplicativos, vamos utilizar o compilador csc.exe (C Sharp Compiler) que acompanha a instalação do .NET Framework. Facilitaremos o acesso ao compilador através do Prompt do Visual Studio. Acione Iniciar->Programas->Microsoft Visual Studio .NET 2003->Visual Studio .NET Tools-> Visual Studio .NET 2003 Command Prompt:

Visual Studio .NET 2003 Command Prompt

Aponte o prompt para C:\Treinamento com o comando “cd C:\Treinamento”. Crie um arquivo texto com a extensão “.cs” dentro da pasta Treinamento. A extensão “.cs” identifica o arquivo como um “arquivo de código C#” (C# Source File). Para facilitar a criação do arquivo, entre com o comando “notepad exemplo1.cs” no prompt e confirme a criação do arquivo. Entre com o seguinte código:

1
2
3
4
5
6
7
8
class Class1
{
      static void Main()
      {
            // Escreve o texto no Console
            System.Console.WriteLine("Olá mundo!");
      }
}

Como o C# é case sensitive, é preciso tomar atenção às letras maiúsculas e minúsculas pois estas são diferenciadas, exceto textos e strings .Salve e feche o arquivo. Ainda no prompt, digite o comando “csc exemplo1.cs”. Esse comando executa a compilação do arquivo exemplo1.cs usando o compilador csc. Se o código tiver sido digitado corretamente, será impressa a mensagem padrão do compilador e o arquivo executável de nosso aplicativo será criado. Por padrão, o nome do executável é o mesmo do arquivo fonte, portanto, para executar o aplicativo, digite “exemplo1” no prompt:

Saída no Console do programa exemplo 1

Veja a saída do aplicativo no Console. Agora que temos as ferramentas, vamos construir nossa primeira classe com alguns membros usando o Notepad. Crie um novo arquivo chamado exemplo2.cs e insira o seguinte código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// cria um alias para facilitar o acesso aos membros do System
using System;
 
// Cria a nossa classe de exemplo
class MinhaClasse
{
int n1 = 2, n2 = 4;
 
      // Função para somar e retonar os valores declarados anteriormente
      public int soma()
      {
            return n1 + n2;
      }
}
 
// classe para execução do aplicativo
class Class1
{
      static void Main()
      {
            // cria uma nova instância da classe MinhaClasse
            MinhaClasse obj = new MinhaClasse();
            // cria uma variável para receber a soma dos valores n1 e n2 da classe
            int result = obj.soma();
            // Coloca o resultado na tela
            Console.WriteLine("{0}", result.ToString());
      }
}

O entendimento do programa acima é intuitivo e simples. Algumas curiosidades são:

  • O termo using System;

usado como um atalho para facilitar a referência a membros e funções. No primeiro exemplo usamos a chamada completa à função WriteLine como comando “System.Console.WriteLine”. No segundo exemplo, o alias nos permite resumir a chamada a apenas “Console.WriteLine”. Consequentemente, se tivéssemos feito um alias para “using System.Console”, a chamada poderia ser resumida para “WriteLine”.

  • A função soma

para termos acesso à função, declaramos com o modificador “public”. Por padrão, os membros declarados sem modificadores são “private”. A função soma é do tipo “int” e a instrução “return” devolve o valor para a função chamadora, no nosso caso, a instrução “int result = obj.soma()”.

Compile o arquivo com a instrução “csc exemplo2.cs”, execute e veja a saída na console:

Saída no Console do obj.soma()

Vamos agora testar o acesso às variáveis n1 e n2. Modifique o método Main para o seguinte:

1
2
3
4
5
6
7
8
9
10
11
...
static void Main()
      {
            // cria uma nova instância da classe MinhaClasse
            MinhaClasse obj = new MinhaClasse();
            // declara as variáveis para receber os valores de n1 e n2
            int N1 = obj.n1, N2 = obj.n2;
            // Coloca os valores na tela
            Console.WriteLine("N1 = {0} e N2 = {1}", N1.ToString(), N2.ToString());
      }
...

Compile o código e veja o resultado:

Saída no Console - Erro

Corretamente o compilador acusa erro de nível de acesso à variável da classe MinhaClasse, pois como elas não possuem modificadores, são “private” por padrão. Para que o aplicativo compile sem problemas, bastaria alterar as declarações das variáveis n1 e n2 para public conforme a seguir:

1
2
3
4
5
6
...
class MinhaClasse
{
      public int n1 = 2, n2 = 4;
}
...

Compile novamente e veja o resultado:

Saída no Console

Tornar os campos públicos não é a maneira mais aconselhável de obter acesso direto aos campos de uma classe, apesar de prática. O motivo é garantir o funcionamento de uma classe que dependa do valor de um campo e que porventura você precise acessá-lo até para saber seu valor. Isso nos relembra o conceito de “Encapsulamento” onde tínhamos que garantir o correto funcionamento da classe. As linguagens do .NET fornecem o uso de propriedades para contornar essa situação. Abordaremos o conceito de propriedades no decorrer do curso.

Classes Abstratas

Neste ponto se faz justo definir o que é uma classe abstrata até para que algumas afirmações anteriormente feitas façam sentido.

Uma classe é abstrata quando ela apenas descreve o que deve ser feito, mas não especifica como deve ser feito.

Algumas afirmações podem esclarecer um pouco a idéia desta classe:

  • é um tipo de classe especial que de alguma forma não é totalmente implementada. Sua estrutura prevê algumas definições e / ou algumas implementações.
  • não podemos criar objetos de uma classe abstrata.
  • uma classe abstrata existe somente para ser herdada.
  • funciona como uma espécie de contrato entre a classe base e a classe derivada, o que significa que a classe derivada deve obrigatoriamente implementar tudo o que não foi definido na classe base para que possa ser utilizada.
  • Uma classe totalmente abstrata contém somente definições e nenhuma implementação.

O motivo para utilizar classes abstratas é a padronização. Dessa forma, você pode ainda em fase de planejamento, definir como serão as classes a serem utilizadas, sem a necessidade de implementar código. Quando o desenvolvimento tiver início, eles já terão um padrão sob o qual irão desenvolver, herdando as classes abstratas definidas na fase de planejamento.

Polimorfismo

Este é um dos conceitos mais complexos da POO.  Ele é conseqüência do processo de herança que teve que resolver vários problemas dado seu nível de abrangência.

Polimorfismo significa “várias formas” ou “multi-forma”. Pode-se entender este processo como a habilidade de uma classe adquirir várias formas em vários objetos, ou, de se comportar de maneira diferente dependendo de em que objeto ela atua.

A princípio é um pouco difícil de entender, mas vamos tentar interpretar de maneira simples. Imagine a classe veículo comentada no início deste capítulo. Todo veículo deve ter o método “movimentar” ou andar só pelo fato de ser um veículo. Também deve implementar um método para possibilitar a sua parada, que neste caso, poderia ser chamado de “parar”.

Até aqui estes métodos aplicados à classe veículo estão claros e concisos à aplicação da classe. Porém, quando quisermos construir uma classe carro que herdará a classe veículo, esses métodos se aplicarão, mas “movimentar” não faz muito sentido para um objeto carro. O melhor neste caso seria acelerar, que junto a outros métodos provocariam o “movimento” do carro. A mesma idéia poderia ser aplicada ao método parar, que para o carro, seria melhor “brecar” ou “frear”.

Mas não é só de nomes que estamos falando. Mesmo que na classe carro este método se chamasse “movimentar”, ele teria uma conotação bem diferente do que o “movimentar” da classe veículo, que deve ser genérico para se aplicar a todas as subclasses que a herdarão.

Ainda pode parecer confuso, mas tente agora imaginar uma outra classe que poderia herdar a classe veículo, como por exemplo, a classe bicicleta. Uma bicicleta é um veículo, e como tal, deve possuir as características deste, bem como seus métodos como o “movimentar” e o “parar”. Assim como para a classe carro, o método movimentar na classe bicicleta deverá sofrer algumas adaptações para que funcione como se fosse um método “pedalar”, e este sim provocaria movimento no veículo bicicleta. Ainda assim, o problema não são os nomes, mas sim suas implementações. Vamos confrontar as classes agora:

Polimorfismo

Os membros “Movimentar” e “Parar” seriam perfeitamente herdados, mas para seu correto funcionamento, algumas adaptações deverão ser feitas. O resultado seria algo como:

Polimorfismo

Ambas as classes possuem suas definições corretas para os membros da classe base. Esta então deve prever uma implementação genérica para que as classes derivadas sejam obrigadas a definir, adaptar ou utilizar esses métodos como se fosse um preço que se paga por querer tirar proveito de suas características. Sabemos que os membros “Pedalar” e “Acelerar” são incompatíveis entre si, tanto pelo motivo de pertencer a classes distintas, como na sua estrutura, mas são compatíveis com o membro “Movimentar” da classe base, que por sua vez serviu como base para sua implementação.

Podemos generalizar que, mesmo sendo classes distintas, bicicleta e carro podem ser considerados e “referenciados” como sendo objeto do tipo “Veículo”.

Mais adiante isso ficará claro com a utilização de código.

Herança Múltipla

Herança múltipla é outro importante conceito a ser abordado na POO. Ele consiste em que uma classe pode ter não apenas uma classe base, mas duas ou mais. Nesse processo, todas as características de todas as classes bases são passadas para a classe derivada. O processo é ilustrado na figura a seguir:

Classes Utilizando Herança Múltipla

Uma pergunta que deve ter surgido pela interpretação da figura acima, é:

Os membros “Nome do cliente”, “nº da conta” e “Saldo” estão nas duas classes bases. Então, de qual delas esses membros são herdados?

É uma pergunta muito pertinente. Nas linguagens que implementam herança múltipla como o C++, existe um processo chamado “Ambigüidade em herança múltipla”, que trata desse tipo de problema.

Para os propósitos deste curso, não será abordado este processo, já que as linguagens a serem estudadas não suportam herança múltipla face aos problemas por elas causados. É claro que a herança múltipla nos oferece uma série de recursos, mas nas novas linguagens, isso é contornado de maneira simples e inteligente.

É aconselhável que os programadores ao menos saibam da existência deste conceito, pois se trata de um dos pilares a POO.

A dupla Encapsulamento X Herança

Após ter entendido o processo de herança de classes, será mais fácil entender algumas vantagens propostas pelo Encapsulamento, recurso explicado anteriormente. Quando se herda uma classe, espera-se que esta esteja em seu mais perfeito funcionamento. Portanto, se por algum motivo houver algum erro durante a criação ou utilização deste novo objeto ou subclasse, muito provavelmente este erro está contido na nova ou nas novas implementações. Isso geralmente é esperado. Mas, e quando isso não acontece? E quando o erro não estiver na nova implementação?

A resposta está em como o conteúdo da classe base está encapsulado, ou seja, não temos acesso a ele, o erro está em seu processo ou concepção. O mais provável é que alguma situação não tenha sido prevista, por exemplo, um envio de mensagens mal feito, ou o não término de um determinado processo, bem como a falta tratamento de erro não previsto. Como uma classe tem que ser o mais auto-sustentável possível, as correções deverão ser feitas na classe base e não nas classes derivadas.

O que está acontecendo, na verdade, é que além de encapsularmos o conteúdo restrito da classe base, estamos também encapsulando todos os possíveis problemas que esta possa causar, fazendo com que as correções sejam mais fáceis de identificar e tratar.

O processo de Herança cuidará para que as correções sejam implementadas nas demais classes derivadas desta.

Criação das classes utilizando herança

Observe a imagem abaixo:

Classes Utilizando Herança

As classes “Conta Poupança” e “Conta Especial” possuem, automaticamente, todos os membros da classe “Conta Simples”, ou seja, “Nome do cliente”, “nº da conta” e “Saldo”, pois herdaram suas características. Não é necessário redefini-los nas novas classes, apenas criam-se os novos membros como “Taxa” e “Limite” nos casos acima.

Não só características podem ser herdadas, mas também os métodos. Nas classes do tipo “Conta”, algumas ações comuns seriam “sacar” e “depositar”. Estas seriam definidas na classe “Conta Simples” para serem herdadas para as demais classes. Em termos de familiarização, chamamos a classe que será herdada como classe-base ou superclasse, e a classe que herda como classe-derivada ou subclasse.

Classes derivadas também podem servir de base para outras classes. Não há limite para derivação de classes.

Existe um ditado que diz, quando herdamos uma classe, ela deve corresponder ao relacionamento “é um” ou “é uma”, ou seja, Conta Especial, é uma Conta Simples assim como um carro é um veículo.

Alguns pontos devem ser considerados quando utilizamos o processo de herança, como é o caso de encapsulamento. Certamente queremos proteger os membros que promovem o funcionamento de um objeto, mas é necessário herdá-los para que as subclasses funcionem corretamente. Estes temas serão abordados em exemplos práticos mais adiante.

Classes e Objetos

Até aqui, procuramos levá-los a um raciocínio sobre o mundo real de forma que seja simples a associação deste com os conceitos da POO.

Na POO, tudo é baseado em classes e objetos. O conceito de classes é universal e imprescindível para a POO, qualquer que seja a linguagem em que esta seja aplicada.

Uma classe é a definição de um objeto. Semanticamente, podemos definir uma classe como sendo um conjunto de características e atributos que definem um objeto. Sendo assim, podemos definir um objeto como algo que é criado, estabelecido, definido e concebido a partir de uma classe.

Um ótimo exemplo para a afirmação acima é imaginar um bolo como sendo um objeto. Qual seria a classe do objeto bolo? O que o define? Como ele é feito ou criado? Neste caso, a classe do objeto bolo seria a sua receita. É nela que está definido o quanto de farinha será gasto no bolo, quanto tempo ele deverá ficar no forno, entre outros atributos e características. Podemos dizer então que a classe molda o objeto.

Outro exemplo simples é o de uma casa e sua planta. A planta seria a classe, pois define todas as características para a construção desta, enquanto que a casa é o nosso objeto.

Na ótica da programação, um objeto é uma instância de uma classe. Não se assuste com esta frase. Ela ficará bem entendida até final deste capítulo.

É bom frisar que a partir de uma classe, é possível construir uma série de objetos. Com uma receita, é possível fazer infinitos bolos assim como com uma planta, é possível construir uma série de casas.

Podemos então começar a definir o que é o conteúdo de uma classe. Ela deverá listar todas as características do objeto, podendo incluir:

  • campos e propriedades – o que define as características (ex: cor, tamanho, etc.);
  • comportamentos, ações ou métodos;
  • reações ou eventos – qual o comportamento deste objeto com base em alguma ação causada;

Isso é dito procurando ainda associar a idéia de objetos reais. Entretanto, na POO alguns aspectos e definições especiais são implementados para garantir o perfeito comportamento na criação,  utilização e descarte dos objetos e classes.  São eles:

  • construtores;
  • destrutores;

Cada um destes elementos citados são membros de uma classe, cada qual com sua aplicação.

Cada linguagem de programação tem sua sintaxe para interpretar, definir e diferenciar cada um destes membros da classe. Algumas estabelecem implementações adicionais a cada uma destas características das classes. No entanto, o padrão conceitual de classe e objeto deve ser seguido.