Programação Orientada a Objetos – Conceitos

Objetivo

O objetivo deste artigo é descrever as diferenças entre linguagens orientadas a objetos (com foco em C#) em relação a outras linguagens, e os benefícios que a programação orientada a objetos pode trazer no dia-a-dia.

Muitas pessoas acreditam que se simplesmente usam “classes” em suas aplicações, programam orientado a objetivo. A idéia central desse artigo é mostrar outros recursos, principalmente herança, polimorfismo e abstração e como aplicá-los na prática.

Esse artigo também mostra como utilizando técnicas de OOP (Object-oriented programming ou programação orientada a objetos) é possível minimizar o acoplamento entre classes, tornando o software mais fácil de manter.

Linguagens Não-estruturadas, Estruturadas e Orientadas a Objeto

Introdução

O objetivo deste tópico é voltar um pouquinho na história das linguagens de programação e apresentar as diferenças entre os 3 tipos de linguagens. Baseadas nessas diferenças, dá pra entender porque as linguagens orientadas a objeto representam uma evolução enorme em relação às suas velhas ancestrais.

Linguagens Não-Estruturadas

As primeiras linguagens existentes (não sei exatamente a data, mas chuto ser lá pela década de 70) eram não-estruturadas.

Na prática, pra gente tentar entender de uma forma bem simples, caracteriza-se pelo não modularização (não dá pra quebrar um programa em functions, procedures, etc) e o uso do “GO TO” para conseguir controlar todo o fluxo de controle do programa.

Alguns exemplos seriam o basic de MS-DOS (sim, eu vivi esse tempo), a linguagem “batch” do MS-DOS (arquivos .bat), e o BASIC. Pra gente tentar entender na prática, vai um exemplo em batch:

@ECHO OFF
GOTO :Start
:Erro
ECHO Ocorreu um Erro!
GOTO End

:Start
TESTE
IF ERRORLEVEL 10
GOTO Erro

:End
@ECHO ON

No exemplo acima, o programa roda de modo “macarrônico” ou segue, começando e executando linha a linha. Ele começa e já manda um goto para :Start. Executa a linha TESTE (tenta rodar um executável ou .bat de nome TESTE) e caso receba um errorlevel 10 (código de retorno de erro do executável), vai para a seção :Erro (ou seja, começo do programa). Mostra a mensagem de erro e vai para a seção :End (final do programa).

Em resumo… pra debugar esse tipo de programa é necessário bastante coragem, e ele não é intuituivo. Depende da boa vontade do programador estruturá-lo.

Linguagens Estruturadas

As linguagens estruturadas caracterizam-se pela possibilidade de estruturar o programa em procedures e functions, o que já resolve grande parte do problema de estruturação do código.

Esse é o caso de linguagens como ANSI C (não C++), Pascal (não Object Pascal ou Delphi) e outras.

Muitas dessas linguagens permitem o uso de structs (C) ou records (pascal) que já permitem a definição de tipos complexos, muito similares às classes nas linguagens orientadas a objeto que vemos hoje.

Por isso existe uma polêmica enorme sobre VB6 ou mesmo ASP sobre a linguagem ser orientada a objeto ou não. O caso é que o VB permite a criação de “classes”, porém, não possui outros conceitos que caracterizam linguagens orientadas a objeto como herança e polimorfismo. O VB.Net sim é orientado a objeto.

Linguagens de programação Orientadas a Objeto

O que caracteriza uma linguagem orientada a objeto basicamente são os recursos de herança e polimorfismo. Nessas linguagens entram C#, VB.Net, Java, C++, SmallTalk, Object Pascal (Delphi) entre outras.

Mas o que são esses recursos que caracterizam linguagens orientadas a objeto?

  • Classes e Objetos: Basicamente esse conceito simplifica muito os problemas gerados principalmente no C por usar estruturas (structs) e ponteiros para estruturas. Quem já programou em C sabe do que estou falando. Basicamente, sempre que declaramos classes, montamos tipos complexos (que possuem vários atributos, métodos etc) passados sempre por referência. Estruturas são sempre passadas por valor (por default). Para entender isso melhor, ver o artigo Tipos Escalares e Tipos Complexos ou Tipos por Valor e Referência
  • Herança: É a possibilidade de definir uma classe que “herda” todas as características de alguma outra classe e implementa novas. Isso faz com que o desenvolvedor passe a pensar melhor em “abstrações” e em reuso de código.
  • Polimorfismo: Para você nunca mais errar a forma de pronunciar essa palavra difícil é só pensar em Poli + morfos. Poli = muitas, morfos = formas. O conceito significa “um objeto podendo ser visto de várias formas diferentes”.

Existem vários outros conceitos dentro de linguagens orientadas a objetos como sobrecarga, encapsulamento, interfaces e outros. Basicamente se os 3 acima forem “dominados” é muito mais fácil entender os demais.

Acoplamento

Como os conceitos de orientação a objetos podem minimizar o acoplamento no meu código?

Vamos pensar num código para “gravar” um pedido. E vamos pensar que temos mais de uma regra para validar o desconto dado pelo vendedor no pedido. Por exemplo: Um vendedor “A” pode dar até 10% do valor total do pedido. Um vendedor “B”, pode dar 20% do valor do total do pedido desde que não ultrapasse um valor de R$10000.

Num modelo tradicional de desenvolvimento, teríamos algo parecido com:


private void gravarPedido(PedidoVO pedido){
  if (!validarDesconto(pedido))
    throw new Exception("Desconto não permitido");
  gravarNaBase(pedido);
}

private bool validarDescontoPedido(PedidoVO pedido){
  if (pedido.vendedor.descontoPorPercentual){
    float valorDescontoMaximo = pedido.valorTotal * (pedido.vendedor.percentualMaximoDesconto / 100);
    if (pedido.valorDesconto > valorDescontoMaximo)
      return false; //Não pode!
  }
  if (pedido.vendedor.descontoPorPercentualEValorMaximo){
    float valorDescontoMaximo = pedido.valorTotal * (pedido.vendedor.percentualMaximoDesconto / 100);
    if (pedido.valorDesconto > valorDescontoMaximo || pedido.valorDesconto > pedido.vendedor.valorMaximoDesconto)
      return false; //Não pode!
  }
  return true;
}

No exemplo acima, o código de validação de desconto está “acoplado” ao código de gravação do pedido, ou seja, sempre que entrar uma regra nova de cálculo de desconto, eu tenho que mexer na gravação de pedido. Sempre que eu mexo na gravação do pedido, eu corro o risco de quebrar toda a funcionalidade de pedido, só por causa da implementação de uma nova regra de desconto.

No caso de uma implementação orientada a objetos, poderia pensar numa interface “IValidadorDesconto” que possui um único método validarDesconto(PedidoVO vo). Cada vendedor, possui a sua implementação para validar desconto, e o código ficaria com algo parecido com:

private void gravarPedido(PedidoVO pedido){
  if (!pedido.vendedor.validarDesconto(pedido))
    throw new Exception("Desconto não permitido");
  gravarNaBase(pedido);
}

Não se preocupe por enquanto em entender o que é a interface, ou como a implementação foi parar ali. A idéia aqui é apenas exemplificar uma mesma versão do código em que a regra de validação de desconto NÃO está acoplada com o processo de gravação do pedido.

Nesse caso, existem várias implementações de regra de validação de desconto, que implementam essa interface IValidadorDesconto. E nesse caso, se eu implementar uma nova regra, eu crio outra classe que implementa IValidadorDesconto e não preciso mexer no código de gravação do pedido.

Esse processo, diminui a complexidade e o tanto de coisas que as classes precisam fazer, consequentemente tornando-as mais simples, de fácil manutenção e menos sujeitas a erro.

Entendendo os principais conceitos de OOP

Daqui pra frente, é muito importante que o conteúdo do artigo Tipos Escalares e Tipos Complexos ou Tipos por Valor e Referência esteja bem dominado, senão fica um pouco complicado entender os conceitos de abstração/polimorfismo na prática.

Não vou “esmiuçar” os conceitos de visibilidade (public, private, protected, sealed), nem overloads e outras coisas. Existem toneladas de documentos sobre isso na internet e é bem fácil de entender desde que o conceito de herança e Polimorfismo esteja bem fixo (estes eu pretendo detalhar aqui). Se sentirem necessidade de complementar esse artigo com essas informações, deixem um comentário na página e assim que possível o farei.

Herança

O conceito de herança é bem simples. Aproveitar todo o comportamento de um ancestral e adicionar novo comportamento.

Ex.:

public class ClasseBase{
  private int _AtributoInt;
  public int AtributoInt{
    get { return _AtributoInt;}
    set { _AtributoInt = value; }
  }

  public virtual void fazerAlgo(){
    Console.WriteLine("ClasseBase: FizAlgo " + _AtributoInt.ToString());
  }

  public void outroMetodo(){
  }
}

public class ClasseFilha: ClasseBase{
  private string _AtributoString;
  public string AtributoString{
    get { return _AtributoString;}
    set { _AtributoString = value;}
  } 

  public override void fazerAlgo(){
    base.fazerAlgo();
    Console.WriteLine("ClasseFilha: FizAlgo " + _AtributoString);
  }
}

No exemplo acima, quando declaramos : ClasseBase é aqui que eu estou descrevendo “herança”. Lê-se ClasseFilha herda ClasseBase. Se eu for usar essa classe dentro de um método num botão, obterei o seguinte comportamento:

private void button1_Click(object sender, EventArgs e) {
  ClasseFilha c = new ClasseFilha(); 
  c.AtributoInt = 10; //Posso usar AtributoInt aqui porque herdei essa característica da ClasseBase
  c.AtributoString = "teste"; 
  c.outroMetodo(); //Esse método pode ser usado aqui porque herdei essa característica da ClasseBase.
  c.fazerAlgo()
  // O resultado dessa chamada será ClasseBase: FizAlgo 10 E ClasseFilha: FizAlgo teste, porque tanto o código
  // da classe base quanto da classe filha são executados devido à chamada de base.fazerAlgo().
}

Se tiver dúvidas do porque do “new”, veja o artigo: Tipos Escalares e Tipos Complexos ou Tipos por Valor e Referência.

Sempre que definimos um método “virtual” numa classe ancestral, significa que “esperamos” que os mesmos sejam herdados e tenham comportamento diferente em classes filhas, que podem ser implementados com um “override”. Esse recurso é justamente o que permite que as classes filhas funcionem como uma espécie de “plugin”.

Interfaces

Vou pegar o gancho pra falar um pouquinho de interfaces, justamente pq o comportamento dela é muito semelhante a uma classe que é “herdada”.

A interface pode ser entendida como um “contrato” entre uma ou mais classes. Quando uma determinada classe “implementa” uma interface (esse é o termo correto), automaticamente a classe é obrigada a implementar os métodos definidos na interface, passando a ser compatível com ela.

Uma interface não possui código, implementação, apenas definições de métodos e seus parâmetros. Ex.:

public interface ITeste{
  public int AtributoInt{
    get;
    set;
  }

  public void fazerAlgo();
}

public class TesteImplementacao: ITeste{
  private int _AtributoInt;
  public int AtributoInt{
    get { return _AtributoInt;}
    set { _AtributoInt = value;}
  } 

  public void fazerAlgo(){
    Console.WriteLine("Fiz algo");
  }
}

No exemplo acima, a definição da interface ITeste diz que para algo implementar ITeste, precisa ter um atributo chamado AtributoInt que obrigatoriamente precisa ter um get e um set (poderia especificar somente um dos dois, por exemplo) e também um método fazerAlgo com retorno void e sem parâmetros.

A partir do momento que a classe TesteImplementacao ganhou um “: ITeste” estou dizendo que ela “implementa” ITeste e obrigatoriamente tem que ter o atributo e os métodos especificados na interface.

Tá bom, e pra que serve isso?

Vamos para o conceito de polimorfismo que a coisa se explica melhor.

Polimorfismo

Como já dito anteriormente: Poli = várias, morfos = forma, ou seja, “Um objeto pode ser visto de várias formas”. Essa é a idéia do conceito que nos ajuda a nunca mais esquecer essa palavra feia.

Para simplificar o conceito, vamos pensar nas classes já definidas anteriormente, com o seguinte exemplo:

public class Teste3: Teste2, ITeste {

}

Estou definindo uma classe Teste3 que herda de Teste2 (possui todos seus atributos e métodos) e implementa ITeste.

Agora veremos o seguinte exemplo:

private void button1_Click(object sender, EventArgs e) {
  Teste3 t = new Teste3(); //Criando nova instância de teste3;
  ITeste i = t; //Operação permitida sim! Porque pelo conceito de Polimorfismo, t É ITeste ou seja, estou "vendo ITeste de outra forma".
  Teste2 t2 = t;//Operação permitida também. Mesma coisa, posso ver t como Teste2. Teste3 É teste2.
  object o = t; // Operação também permitida. Estou vendo t como "object". Como todo mundo automaticamente herda de object, o mesmo conceito se aplica.
}

No exemplo acima estou vendo a mesma instância de várias maneiras. No caso, quando vejo ela numa visibilidade menor, por exemplo,
olhando a instância de Teste3 como ITeste, não posso acessar o método “outroMetodo”, porém, qualquer operação que eu faça ali, estou fazendo na mesma instância.

Ex.:

private void button1_Click(object sender, EventArgs e) {
  Teste3 t = new Teste3(); //Criando nova instância de teste3;
  t.AtributoInt = 10;
  t.AtributoString = "teste";
  ITeste it = t;
  it.fazerAlgo();
}

O resultado da chamada acima para “fazerAlgo” será:

ClasseBase: FizAlgo 10
ClasseFilha: FizAlgo teste

Isso ocorre porque estou acessando a mesma instância “Teste3” de outra forma.

É possível também o sentido inverso, ou seja, dada uma instância de “TesteBase”, eu conseguir acessar um método da classe Teste3 (desde que a instância seja de Teste3). Nesse caso entra um “typecast”, ou seja, eu preciso dizer para o compilador “converter” o tipo antes de acessar o método. Ex.:

private void button1_Click(object sender, EventArgs e) {
  Teste3 t = new Teste3(); //Criando nova instância de teste3;
  t.AtributoInt = 10;
  t.AtributoString = "teste";

  ITeste it = t; // Fiz o acesso da instância de Teste3 como ITeste.
  Teste3 teste3 = (Teste3)it; // Agora estou fazendo um "typecast" de ITeste para Teste3.
  teste3.AtributoString = "mudei o valor da mesma instância ainda";
}
[

]

Na verdade, melhor que o “converter” que já coloquei entre aspas de propósito, pode ler-se como “o compilador olha para um espaço de memória de uma forma diferente”.

Nesse caso, quando eu faço o “typecast” na verdade eu estou acessando uma referência atualmente vista como ITeste para Teste3. O compilador aceita se eu passar outro tipo que também implementa ITeste como parâmetro e essa operação “pode” gerar erros em runtime (caso a instância existente na variável it não seja do tipo Teste3).

Tentando ser mais didático

A combinação desses conceitos, herança, interfaces e polimorfismo, pode permitir que a sua aplicação funcione com uma estrutura similar a “plugins”, minimizando bastante o acoplamento. O ganho disso está na manutenção da aplicação. Mexe-se menos nos núcleos, motores e partes críticas e mais nas implementações específicas.

Em diversos lugares na própria framework do .Net vemos isso. As páginas aspx entendem os componentes de uma forma “abstrata”, ou seja, como System.Web.UI.Control. Todo e qualquer server control (mesmo os user controls) herdam dessa classe. Dessa forma, System.Web.UI.Page conhece apenas System.Web.UI.Control. Ela não tem a menor idéia do que seja System.Web.UI.TextBox, System.Web.UI.ImageButton. Em resumo, eu crio novos componentes (plugins) sem precisar mexer na estrutura de WebForms do ASP.Net, desde que eu saiba entender como essa classe abstrata “Control” funciona e como ela chama seus métodos virtuais. Não é mágico?

Pra gente entender na prática como programar algo baseado nesse conceito, ou ainda exemplificar o conceito, vamos fazer uma analogia com dispositivos USB. Pense em USB como uma interface (e ela é na vida real). Uma interface bonita, um cabo pequeno, barato, tem hubs, extensões e um monte de outras coisas que “falam” USB.

Na prática, um computador sabe “entender” dispositivos de armazenamento USB. Qualquer coisa que eu ligue nele que seja um dispositivo de armazenamento, ele automaticamente “entende”. Pode ser um celular, um HD externo, um pen drive, uma máquina fotográfica. Tudo isso porque o computador sabe entender de uma forma “abstrata” o comportamento de um dispositivo de armazenamento. Pegar arquivo, ler arquivo, gravar arquivo, apagar arquivo, são possíveis implementações para essa interface. Desde que o dispositivo saiba “implementar” essa interface, o computador automaticamente sabe conversar com ela, mesmo que seja feito por outro fabricante e que ele não saiba nenhum detalhe da sua implementação.

Eu comprei recentemente um celular que pode ser sincronizado com o computador. Quando eu ligo o celular na porta USB, o mesmo celular tem duas “implementações” para USB. Uma que ele pode ser acessado como um telefone comum, onde eu posso sincronizar meus contatos, compromissos, notas, etc e outra em que ele pode ser visto como um “dispositivo de armazenamento USB”. Dessa forma, podemos dizer que o telefone celular é meio “polimórfico” (acabei de cunhar esse termo, não levem ele a sério). Ele é um dispositivo que implementa uma interface para ser visto como um celular no computador, outro como um dispositivo de armazenamento, da mesma forma que nossas classes podem ser vistas de mais de uma forma.

Olhando desta forma, podemos pensar na nossa aplicação do mesmo jeito. Como um “motor” ou um “núcleo” e as coisas “específicas” como implementações, minimizando completamente o acoplamento, facilitando testes, extensão e mtas outras coisas.

Podemos pensar num sistema de pedidos com uma interface para validar descontos, ou um sistema de integração com uma interface para processar arquivos e diversas implementações para tratar os diferentes layouts. A sacada é pensar: “Que problemas na minha aplicação dependem de abstrações e implementações específicas?”. Automaticamente surgem vários problemas que podem ser resolvidos dessa forma.

Anúncios

4 comentários em “Programação Orientada a Objetos – Conceitos

  1. Parabéns pelo post Eric, estou estudando C# para trabalhar na área e no começo é bem difícil entender os conceitos e aprender. Seu artigo me deu mais uma clareada no aprendizado, obrigado 🙂

    1. Willian,

      Que bom que foi útil. O objetivo é esse mesmo. O assunto é bastante extenso, mas a intenção do artigo é justamente fazer uma introdução bem prática ao assunto.

      Abraço,

      Eric

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s