A importância da POO em .NET

A importância da POO é simples e direta. Tudo em .NET é objeto. Mesmo os tipos de dados mais simples são considerados objetos, já estes também contém métodos e propriedades.

Implicitamente, todo e qualquer tipo ou objeto em .NET possui um ancestral comum, o tipo object. Mesmo os tipos mais simples e os tipos (classes) criados posteriormente, herdam as características deste tipo. As vantagens de se ter um ancestral comum serão entendidas ao longo do curso. Uma delas já é explícita, pois atende completamente aos conceitos da POO.

Sem entrar em muitos detalhes neste ponto, considere o programa abaixo:

1
2
3
4
5
6
7
8
9
10
11
namespace ConsoleExemplo1
{
	class Class1
	{
		static void Main(string[] args)
		{
			string teste = 10.ToString();
			System.Console.WriteLine("{0}", teste);
		}
	}
}

O resultado deste algoritmo é o número 10 escrito na tela do Console.

Vamos dar uma atenção especial à linha 7. Veja o número 10 que está sendo atribuído à variável “teste” que é do tipo string. O interessante é que a variável 10 não foi definida em lugar algum. Por definição ela é uma constante e por interpretação, ele passa a ser considerada durante a compilação como um inteiro de 32 bits (Int32). Isso pode ser facilmente verificado usando o método GetType que retorna o nome do tipo de uma determinada variável:. Veja o código complementado abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
namespace ConsoleExemplo1
{
	class Class1
	{
		static void Main(string[] args)
		{
			string teste = 10.ToString();
			System.Console.WriteLine("{0}", teste);
			System.Console.WriteLine("{0}", 10.GetType().ToString());
		}
	}
}

A linha 9 imprime na tela do Console o tipo de variável atribuído em tempo de compilação à constante 10. Veja o resultado da execução deste código:

Saída no Console - Método ToString aplicado a um Inteiro

O compilador converteu automaticamente a constante “10” para o tipo “System.Int32”. Como dito anteriormente, tudo em .NET é objeto e todos herdam do ancestral object, e como tal, possuem métodos, propriedades e tudo o que se aplica a este classe. No código acima, a constante 10 está executando dois métodos:

  • Na linha 7 executa o método ToString() que converte a variável para uma representação de string para que possa ser visualizada em modo texto.
  • Na linha 9 executa tanto o método ToString() quanto o método GetType() que retorna o tipo de dado da variável em que é aplicado.

Estando claro que tudo em .NET é objeto, seja ele um aplicativo console, um aplicativo Windows, enfim, podemos seguir com os estudos sabendo da importância dos objetos para tirar o máximo proveito dos recursos do .NET.

Introdução

Com os conceitos acima aprendidos, faz-se necessário alguma implementação de código para o melhor entendimento dos conceitos. O que foi descrito até aqui, não engloba tudo o que a POO tem a oferecer, porém, seria extremamente complexo e obscuro continuar sem vincular tudo o que já foi visto e o que veremos sem nenhum exemplo prático.

A intenção deste capítulo até aqui, foi induzi-los a uma linha de pensamento Orientada a Objetos. Tudo que foi explicado até agora, poderia ter sido sustentado com trechos de código fonte, mas muito se perderia, seja por dar atenção à linguagem e a sua sintaxe, seja por acompanhamento do raciocínio.

É um grande desafio fazer uso pleno da POO nos programas desenvolvidos. Mesmo os mais experientes e talentosos especialistas não conseguiriam implementar todos os seus conceitos por maior que seja a complexidade do problema. Programar Orientado a Objetos passa ser uma arte de certo ponto de vista. De qualquer forma, 90% dos conceitos serão aqui abordados com exemplos práticos para que numa futura necessidade, seja possível extrair o máximo do que a POO oferece.

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.

Herança

Este pode ser considerado o conceito mais importante da POO, pois é ele quem possibilita obter a maior parte das vantagens oferecidas por esta.

Herança é o processo de reaproveitamento das características com base em um ancestral dos quais estas querem se aproveitar.

Pela palavra em si, já podemos admitir que este conceito nos leva a uma idéia de aproveitamento, e é isso mesmo que deve ser considerado. Podemos assimilar a situação em que o filho herda bens do pai, mas para termos de estudo da POO, vamos assumir uma herança mais “biológica”.

Imaginemos que o filho herda os traços do pai, ou seja, cor dos cabelos, da pele, dos olhos e outras não só estéticas, mas comportamentais como o caráter, temperamento e outras coisas. Devemos considerar também que nem sempre tudo é herdado. Além do mais, não é só do pai que a herança é feita, mas da mãe também. É possível também que algumas outras sejam herdadas dos avôs ou ancestrais mais distantes, dos quais os pais herdaram tais características.

Em resumo, este conceito nos leva ao reaproveitamento de características de nossas classes. Assumindo o caso acima, para criar um filho, precisamos de um pai, que precisa de outro pai, que precisa de outro, enfim. Obviamente não vamos chegar ao infinito, mas precisamos admitir que a construção de um filho, torna-se bem mais simples com a existência de um pai.

Voltando agora ao conceito de classes, tomemos o exemplo do carro. Se construíssemos o carro do zero, seria algo extremamente penoso, pois teríamos que conceituar todo o seu funcionamento, desde o motor, toda a parte elétrica e por aí vai. Mas obviamente não faríamos isso, mas sim aproveitaríamos muito do que já foi feito, por exemplo, o próprio motor, todo o câmbio, toda a parte elétrica, e mudaríamos apenas aquilo que nos fosse de interesse. Estamos então herdando vários conceitos de certa classe e reaproveitando-os para construir uma nova, implementando novas, e até alterando algumas de suas características. Isso significa uma economia de trabalho significativa, dependendo do que se quer construir ou herdar.

Vamos tentar exemplificar um caso real. Suponhamos que na construção de um sistema bancário tivéssemos que construir um cadastro de clientes e este cadastro fosse dos seguintes tipos:

  • Conta Simples
  • Conta Especial
  • Conta Poupança

Cada um destes tem as seguintes características:

  • Conta Simples
    • Nome do cliente
    • nº da conta
    • Saldo
  • Conta Especial
    • Nome do cliente
    • nº da conta
    • Saldo
    • Limite
  • Conta Poupança
    • Nome do cliente
    • nº da conta
    • Saldo
    • Taxa

Note que para os casos acima, todos têm os membros “Nome do cliente”, “nº da conta” e “Saldo” em comum. Não haveria motivo para tanta redundância. É aí que entra o conceito de herança. Poderíamos então tomar a classe Conta Simples que possui os três membros comuns a todas as outras, como sendo a classe Base, e, simplesmente “herdá-la”, implementando somente as modificações necessárias. O desenho abaixo demonstra as duas situações:

Encapsulamento

Encapsulamento é a característica de uma classe que a torna a mais independente possível.

Isso é desejável por que, como dito anteriormente, é conveniente que quando disponibilizamos uma classe, o usuário possa fazer uso desta de maneira fácil e intuitiva. Mas outro fator que é importante considerar, é que não devemos possibilitar ao usuário interferir no funcionamento de nossa classe, ou seja, devemos proteger a parte do conteúdo que é vital para o funcionamento do objeto a ser criado.

Tomando como exemplo a classe carro, é importante “liberar” ao usuário algumas opções como: acelerar, mudar a marcha, trocar o óleo e colocar combustível. Mais importante ainda é proteger a parte que trata do funcionamento do motor, o sistema de freios e todo o restante que é importante para funcionamento do objeto carro, mas não precisa estar acessível ao usuário. As razões são inúmeras e a mais importante, é proteger e garantir o perfeito funcionamento do objeto.

O conceito de encapsulamento deve ser empregado quando se quer proteger o conteúdo da classe que, por segurança ou conveniência, não deve estar acessível aos usuários desta.

Pode se perceber o emprego desse conceito em várias situações da vida real e mesmo na informática, por exemplo, quando fazemos uso de vários recursos, de várias ferramentas, sem sabermos como estas realmente funcionam internamente, e às vezes sem sequer termos acesso a essas informações.