[INFO] POO – Programação Orientada a Objetos: uma introdução

Publicado: 29/04/2011 em ASP.NET, C#, Certificações, Formação, Internet, Microsoft, Programação, Segurança da Informação, Softwares, Tutoriais / Info
Tags:, , , , , ,
Um breve relato da história da Programação Orientada a Objetos

O termo Programação Orientada a Objetos foi criado por Alan Kay, autor da linguagem de programação Smalltalk. Mas mesmo antes da criação do Smalltalk, algumas das ideias da POO já eram aplicadas, sendo que a primeira linguagem a realmente utilizar estas idéias foi a linguagem Simula 67, criada por Ole Johan Dahl e Kristen Nygaard em 1967. Note que este paradigma de programação já é bastante antigo, mas só agora vem sendo aceito realmente nas grandes empresas de desenvolvimento de Software. Alguns exemplos de linguagens modernas utilizadas por grandes empresas em todo o mundo que adotaram essas idéias: Java, C#, C++, Object Pascal (Delphi), Ruby, Python, Lisp, …

A maioria delas adota as idéias parcialmente, dando espaço para o antigo modelo procedural de programação, como acontece no C++ por exemplo, onde temos a possibilidade de usar POO, mas a linguagem não força o programador a adotar este paradigma de programação, sendo ainda possível programar da forma procedural tradicional. Este tipo de linguagem segue a idéia de utilizar uma linguagem previamente existente como base e adicionar novas funcionalidades a ela.

Outras são mais “puras”, sendo construidas do zero focando-se sempre nas idéias por trás da orientação a objetos como é o caso das linguagensSmalltalkSelf e IO, onde TUDO é orientado a objetos.

Idéias básicas da POO

A POO foi criada para tentar aproximar o mundo real do mundo virtual: a idéia fundamental é tentar simular o mundo real dentro do computador. Para isso, nada mais natural do que utilizar Objetos, afinal, nosso mundo é composto de objetos, certo?!

Na POO o programador é responsável por moldar o mundo dos objetos, e explicar para estes objetos como eles devem interagir entre si. Os objetos “conversam” uns com os outros através do envio de mensagens, e o papel principal do programador é especificar quais serão as mensagens que cada objeto pode receber, e também qual a ação que aquele objeto deve realizar ao receber aquela mensagem em específico.

Uma mensagem é um pequeno texto que os objetos conseguem entender e, por questões técnicas, não pode conter espaços. Junto com algumas dessas mensagens ainda é possível passar algumas informações para o objeto (parâmetros), dessa forma, dois objetos conseguem trocar informações entre si facilmente.

Ficou confuso? Vamos a um exemplo prático: imagine que você está desenvolvendo um software para uma locadora e esta locadora tem diversos clientes. Como estamos tentando modelar um sistema baseado no sistema real, nada mais obvio do que existirem objetos do tipo Clientes dentro do nosso programa, e esses Clientes dentro do nosso programa nada mais serão do que objetos que “simulam” as características e ações no mundo virtual que um cliente pode realizar no mundo real.

Vamos apresentar exemplo mais detalhadamente mais para frente, mas antes precisamos especificar mais alguns conceitos.

O que é uma Classe, Atributo e Método? E pra que serve o Construtor?

Uma classe é uma abstração que define um tipo de objeto e o que objetos deste determinado tipo tem dentro deles (seus atributos) e também define que tipo de ações esse tipo de objeto é capaz de realizar (métodos).

É normal não entender isso logo de cara, mas os conceitos de classes e subclasses são relativamente simples: Tente pensar no conceito de Classes utilizado na Biologia: um animal é uma classe, e tem suas características: é um ser vivo capaz de pensar, precisa se alimentar para viver, etc, etc. O Ser Humano é uma sub-classe dos animais. Ele tem todas as características de um animal, mas também tem algumas peculiaridades suas, não encontradas nas outras sub-classes de animais. Os pássaros também são animais, mas possuem características próprias.

Note que uma Classe não tem vida, é só um conceito. Mas os Objetos (animais, serem humanos, pássaros, etc) possuem vida. O seu cachorro rex é um Objeto (ou instância) da classe Cachorro. A classe Cachorro não pode latir, não pode fazer xixi no poste, ela apenas especifica e define o que é um cachorro. Mas Objetos do tipo Cachorro, estes sim podem latir, enterrar ossos, ter um nome próprio, etc.

A criação de uma nova Classe é dividida em duas partes: os seus atributos e os seus métodos. Os atributos são variáveis que estarão dentro de cada um dos objetos desta classe, e podem ser de qualquer tipo. Por exemplo, a classe Cachorro poderá ter o atributo nome que será do tipo String. Assim, cada Objeto desta classe terá uma variável própria chamada nome, que poderá ter um valor qualquer (Rex, Frodo, Atila, …).

Métodos serão as ações que a Classe poderá realizar. Quando um objeto desta classe receber uma mensagem de algum outro objeto contendo o nome de um método, a ação correspondente a este método será executada. Por exemplo, caso um objeto da classe Dono envie uma mensagem para um objeto do tipo Cachorro falando “sente”, o cachorro irá interpretar esta mensagem e conseqüentemente irá executar todas as instruções que foram especificadas na classe Cachorro dentro do método sente.

Um construtor tem uma função especial: ele serve para inicializar os atributos e é executado automaticamente sempre que você cria um novo objeto.

Quando você especifica os atributos de uma classe, você apenas diz ao sistema algo como “objetos desta classe Pessoa vão ter uma variável chamada Nome que é do tipo String, uma variável chamada idade que é do tipo inteiro, etc, etc”. Mas estas variáveis não são criadas, elas só serão criadas no construtor. O construtor também pode receber parâmetro, desta forma, você pode passar para o construtor uma String contendo o nome da Pessoa que você está criando, sua idade, etc. Normalmente, a sintaxe é algo parecido com isto:

Pessoa joao := new Pessoa( “João”, 13 );

Este código gera um novo objeto chamado joao que é do tipo Pessoa e contem os valores “João” como nome e 13 como idade.

Caso você tenha entendido o texto acima, você entendeu 9% do que esta por trás da orientação a objetos.

O mais importante é entender como funciona a comunicação entre os objetos, que sempre segue o seguinte fluxo:

-Um objeto A envia uma mensagem para o objeto B.

-Objeto B verifica qual foi a mensagem que recebeu e executa sua ação correspondente. Esta ação está descrita no método que corresponde a mensagem recebida, e está dentro da Classe a qual este objeto pertence.

Exemplo

Voltando ao exemplo da locadora: Você está desenvolvendo um software para uma locadora. Esta locadora terá diversos clientes. Poderímos então criar uma classe explicando para o computador o que é um Cliente: para isso precisaríamos criar uma classe chamada Cliente, e com as seguintes características (atributos):

  • Nome
  • Data de Nascimento
  • Profissão

Mas um cliente é mais do que simples dados. Ele pode realizar ações! E no mundo da POO, ações são descritas através da criação de métodos.

Dessa forma, objetos da nossa classe Cliente poderá por exemplo executar as seguintes ações (métodos):

  • AlugarFilme
  • DevolverFilme
  • ReservarFilme

Note o tempo verbal empregado ao descrever os métodos. Fica fácil perceber que trata-se de ações que um Cliente pode realizar.

É muito importante perceber as diferenças entre Atributo e Método. No começo é normal ficar um pouco confuso, mas tenha sempre em mente:

  • Atributos são dados.
  • Métodos descrevem possíveis ações que os objetos são capazes de realizar.

Assim, nosso sistema pode ter vários Objetos do tipo Cliente. Cada um destes objetos possuirá seu próprio nome, data de nascimento e profissão, e todos eles poderão realizar as mesmas ações (AlugarFilme, RevolverFilme ou ReservarFilme).

Herança

Voltando a idéia das classes na Biologia: um ser humano é um animal. Ele tem todas as características (atributos) e pode realizar todas as ações (métodos) de um animal. Mas além disso, ele tem algumas características e ações que só ele pode realizar.

Em momentos como este, é utilizado a herança. Uma classe pode estender todas as características de outra e adicionar algumas coisas a mais. Desta forma, a classe SerHumano será uma especialização (ou subclasse) da classe Animal. A classe Animal seria a classe pai da serHumano, e logicamente, a classe SerHumano seria a classe filha da Animal.

Uma classe pode sempre ter vários filhos, mas normalmente as linguagens de programação orientadas a objetos exigem que cada classe filha tenha apenas uma classe pai. A linguagem C++ permite que uma classe herde as características de varias classes (herança múltipla), mas C++ não é um bom exemplo quando se está falando sobre conceitos de POO.

Um exemplo um pouco mais próximo da nossa realidade: vamos supor que estamos desenvolvendo um sistema para um banco. Nosso banco possui clientes que são pessoas físicas e pessoas jurídicas.

Poderiamos criar uma classe chamada Pessoa com os seguintes atributos:

  • Nome
  • Idade

Em seguida, criamos 2 classes que são filhas da classe Pessoa, chamadas PessoaFisica e PessoaJuridica. Tanto a classe PessoaFisica como a PessoaJuridica herdariam os atributos da classe Pessoa, mas poderiam ter alguns atributos a mais.

A classe PessoaFisica pode ter por exemplo o atributo RG enquanto a classe PessoaJuridica poderia ter o atributo CNPJ.

Dessa forma, todos os objeto da classe PessoaFisica terá como atributos:

  • Nome
  • Idade
  • RG

E todos os objetos da classe PessoaJuridica terão os seguintes atributos:

  • Nome
  • Idade
  • CNPJ

Os métodos são análogos: poderíamos criar alguns métodos na classe Pessoa e criar mais alguns métodos nas classes PessoaJuridica e PessoaFisica. No final, todos os objetos teriam os métodos especificados na classe Pessoa, mas só os objetos do tipo PessoaJuridica teriam os métodos especificados dentro da classe PessoaJuridica, e objetos do tipo PessoaFisica teriam os métodos especificados na classe PessoaFisica.

Herança X Composição

Há algum tempo, herança era considerada a ferramenta básica de extensão e reuso de funcionalidade. Qualquer linguagem que não possuísse o conceito de herança “não servia para nada”. Atualmente, todos os artigos sobre padrões de projeto desaconselham a utilização de herança.

E agora?

Um princípio básico de padrões de projeto é “dar prioridade à composição”, preferir sempre “tem-um” ao invés de “é-um”. Quanto à herança, deve ser utilizada com muita prudência e em pouquíssimas situações.

Uma das poucas certezas que temos no desenvolvimento de aplicações é que existirão alterações. Portanto, a utilização de herança para fins de reutilização não dá tão certo quando se tratam de manutenções nos códigos já existentes.

Supondo que utilizamos uma classe pai para encapsular o comportamento de algum objeto. Dessa forma, todas as nossas classes filhas herdarão esses comportamentos. E o problema ocorre quando alguma das classes filhas não precisam de algum dos comportamentos que estão encapsulados. Passamos a ter que mudar o código na classe filha para que o método que foi herdado funcione da maneira específica para esse objeto. Ou, pior ainda, quando esse objeto não necessitar desse comportamento, então teríamos que sobrescrever o método para que ele não faça nada.

Alguns dos problemas dessa implementação é que o encapsulamento entre classes e subclasses é fraco, e o acoplamento é forte. Assim, toda vez que uma superclasse for alterada, todas as subclasses podem ser afetadas. Perceba que estamos violando o princípio básico de OO (Alta Coesão, Baixo Acoplamento).

Ainda com herança, a estrutura está presa ao código, e não pode sofrer alterações facilmente em tempo de execução, fazendo diminuir a capacidade de polimorfismo.

Quando utilizamos composição, instanciamos a classe que desejamos dentro de nosso código. Dessa forma, estamos estendendo as responsabilidades pela delegação de trabalho a outros objetos. Em vez de codificar um comportamento estaticamente, definimos pequenos comportamentos padrão e usamos composição para definir comportamentos mais complexos. Ainda na utilização de composição, podemos mudar a associação entre classes em tempo de execução e permitir que um objeto assuma mais de um comportamento.

Ao utilizar a composição, teremos muito mais flexibilidade, além de ser mais comum em muitos padrões de projetos. Porém, na herança, temos uma possibilidade de desenvolver mais rápido, diminuindo o tempo de desenvolvimento, levando em conta que perdemos muito mais tempo mantendo e alterando o código original do que com o desenvolvimento inicial. Portanto, nosso esforço deve ser sempre voltado para a reutilização e para a manutenção.

Aproveito para indicar um artigo interessante que conta com códigos mostrando o motivo de nunca se utilizar herança: veja aqui.

Composição e Herança
  • Composição e herança são dois mecanismos para reutilizar funcionalidade
  • Alguns anos atrás (e na cabeça de alguns programadores ainda!), a herança era considerada a ferramenta básica de extensão e reuso de funcionalidade
  • A composição estende uma classe pela delegação de trabalho para outro objeto
    a herança estende atributos e métodos de uma classe
  • Hoje, considera-se que a composição é muito superior à herança na maioria dos casos
    • A herança deve ser utilizada em alguns (relativamente poucos) contextos
  • Vamos portanto desinflar um pouco a bola da herança …

Um exemplo de composição

  • Use composição para estender as responsabilidades pela delegação de trabalho a outros objetos
  • Um exemplo no domínio de endereços
    • Uma empresa tem um endereço (digamos só um)
    • Uma empresa “tem” um endereço
    • Podemos deixar o objeto empresa responsável pelo objeto endereço e temos agregação composta (composição)

tecnicas11.gif (2407 bytes)

Um exemplo de herança

  • Atributos, conexões a objetos e métodos comuns vão na superclasse (classe de generalização)
  • Adicionamos mais dessas coisas nas subclasses (classes de especialização)
  • Três situações comuns para a herança (figura abaixo)
    • Uma transação é um momento notável ou intervalo de tempo

tecnicas4.gif (4586 bytes)

  • Exemplo no domínio de reserva e compra de passagens de avião

tecnicas5.gif (4521 bytes)

Benefícios da herança

  • Captura o que é comum e o isola daquilo que é diferente
  • A herança é vista diretamente no código

Problemas da herança

  • O encapsulamento entre classes e subclasses é fraco (o acoplamento é forte)
    • Mudar uma superclasse pode afetar todas as subclasses
      • The weak base-class problem
    • Isso viola um dos princípios básicos de projeto O-O (manter fraco acoplamento)
  • Às vezes um objeto precisa ser de uma classe diferente em momentos diferentes
    • Com herança, a estrutura está parafusada no código e não pode sofrer alterações facilmente em tempo de execução
    • A herança é um relacionamento estático que não muda com tempo

O resultado de usar composição

  • Em vez de codificar um comportamento estaticamente, definimos pequenos comportamentos padrão e usamos composição para definir comportamentos mais complexos
  • De forma geral, a composição é melhor do que herança normalmente, pois:
    • Permite mudar a associação entre classes em tempo de execução;
    • Permite que um objeto assuma mais de um comportamento (ex. papel);
    • Herança acopla as classes demais e engessa o programa
5 regras para o uso de herança (Coad)
  • O objeto “é um tipo especial de” e não “um papel assumido por”
  • O objeto nunca tem que mudar para outra classe
  • A subclasse estende a superclasse mas não faz override ou anulação de variáveis e/ou métodos
  • Não é uma subclasse de uma classe “utilitária”
    • Não é uma boa idéia fazer isso porque herdar de, digamos, HashMap deixa a classe vulnerável a mudanças futuras à classe HashMap
    • O objeto original não “é” uma HashMap (mas pode usá-la)
    • Não é uma boa idéia porque enfraquece a encapsulação
      • Clientes poderão supor que a classe é uma subclasse da classe utilitária e não funcionarão se a classe eventualmente mudar sua superclasse
      • Exemplo: x usa y que é subclasse de vector
        • x usa y sabendo que é um Vector
        • Amanhã, y acaba sendo mudada para ser subclasse de HashMap
        • x se lasca!
  • Para classes do domínio do problema, a subclasse expressa tipos especiais de papeis, transações ou dispositivos
Polimorfismo

Um dos conceitos mais complicados de se entender, e também um dos mais importantes, é o Polimorfismo. O termo polimorfismo é originário do grego e significa “muitas formas”.

Na orientação a objetos, isso significa que um mesmo tipo de objeto, sob certas condições, pode realizar ações diferentes ao receber uma mesma mensagem. Ou seja, apenas olhando o código fonte não sabemos exatamente qual será a ação tomada pelo sistema, sendo que o próprio sistema é quem decide qual método será executado, dependendo do contexto durante a execução do programa.

Desta forma, a mensagem “fale” enviada a um objeto da classe Animal pode ser interpretada de formas diferentes, dependendo do objeto em questão. Para que isto ocorra, é preciso que duas condições sejam satisfeitas: exista herança de uma classe abstrata e casting (outras situações também podem resultar em polimorfismo, mas vamos nos centrar neste caso).

Herança nos já definimos previamente, mas o que é uma classe abstrata? E o que é esse tal de casting?

Uma classe abstrata é uma classe que representa uma coleção de características presentes em vários tipos de objetos, mas que não existe e não pode existir isoladamente. Por exemplo, podemos criar uma classe abstrata chamada Animal. Um Animal tem diversas características (atributos) e podem realizar diversas ações (métodos) mas não existe a possibilidade de criarmos objetos do tipo Animal. O que existem são objetos das classes Cachorro, Gato, Papagaio, etc. Essas classes, estendem a classe Animal herdando todas as suas características, e adicionando algumas coisas a mais. “Animal” é só uma entidade abstrata, apenas um conjunto de características em comum, nada mais.

Você pode olhar para um objeto da classe Cachorro e falar “isto é um animal, pois estende a classe Animal”, mas você nunca vai ver um objeto que seja apenas da classe Animal, pois isso não existe! É como eu olhar para você e falar “você é um objeto da classe SerVivo”. Essa afirmação está correta, mas você na verdade é um objeto da classe SerHumano, que por sua vez herda todas as características da classe SerVivo (que por sua vez é uma classe abstrata, já que não podemos criar algo que seja apenas classificado como “SerVivo”, sempre vamos classifica-lo de forma menos genérica).

Resumindo: uma classe abstrata é um conjunto de informações a respeito de uma coleção de outras classes. Uma classe abstrata sozinha é completamente inútil, já que não podemos instanciar um objeto desta classe, podemos apenas instanciar objetos de classes que estendem a classe abstrata inicial. Ela serve apenas para simplificar o sistema, juntando em um único lugar diversas características que são comuns a um grupo de classes.

Nunca esqueça disso: você nunca vai poder criar um objeto do tipo de uma classe abstrata. Sempre crie objetos das classes que estendem esta classe abstrata.

Já o termo casting é utilizado quando nos forçamos o sistema a ver um certo objeto como sendo de um determinado tipo que não é o seu tipo original. Supondo que temos a situação a seguir:

uml


Essa é uma representação na notação UML (Unified Modeling Language) que nos informa que existe a definição de três classes em nosso sistema: existe a classe abstrata Animal (note que ela está em itálico, isso significa na notação UML que trata-se de uma classe abstrata) e existem também outras duas classes chamadas Cachorro e Gato, que são filhas da classe Animal. Ou seja, todo objeto das classes Cachorro e Gato vão ter todas as características (atributos e métodos) presentes na classe Animal, e mais algumas características próprias.

Imagine que vamos criar um sistema de cadastro de animais. Vamos, por questões didáticas, supor que todos os animais de nosso sistema fiquem armazenados em um array. Então existiria um array contendo objetos dos tipos Gato e do tipo Cachorro. Mas armazenar diversos tipos diferentes de objetos em um único array não é uma boa idéia, pois depois, para extrair essas informações de volta é bastante complicado. Mas pare e pense por um instante: objetos do tipo Cachorro e objetos do tipo Gato são também objetos do tipo Animal, correto? Bom, então podemos criar um array capaz de armazenar Animais! Assim nossa vida fica bem mais fácil, bastando atribuir ao array os objetos (do tipo Cachorro e Gato – que estendem a classe Animal) que queremos guardar. Em forma algorítmica, seria mais ou menos:

Animal[] listaDeAnimais = new Animal[100]; // Criamos um array com 100 posições que armazena objetos do tipo Animal.

listaDeAnimais[0] = new Cachorro(“Frodo”); // Criamos um novo objeto do tipo Cachorro com o nome Frodo e armazenamos no array

listaDeAnimais[1] = new Gato(“Alan”); // Criamos um novo objeto do tipo Gato com o nome Alan e armazenamos no array

…….

Certo! Agora temos um array com vários objetos do tipo Animal. Agora vamos fazer um looping por todos esses objetos, enviando para cada um deles a mensagem “fale”. O que iria acontecer?

Inicialmente, vamos supor que a classe abstrata Animal possui o método “fale”, e que ele seja implementado (de forma algorítmica) da seguinte forma:

Classe Animal {
    método fale() {
        imprimaNaTela(" Eu sou mudo! ");
    }
}

Desta forma, todo objeto que de alguma classe que estenda a classe Animal vai ter automaticamente o método “fale”, e isso inclui todos os objetos das classes Cachorro e Gato. Mas todos eles, atualmente, ao receber a mensagem “fale” vão responder imprimindo na tela a mensagem “Eu sou mudo!”. Mas Gatos e Cachorros podem falar! O que podemos fazer é sobreescrever o método fale para cada uma das classes, substituindo então seu conteúdo pelo comportamento que queremos que cada subclasse tenha.

Por exemplo, poderíamos escrever na classe Gato do seguinte método:

Classe Gato {
    Método fale() {
        imprimaNaTela(" Miaaaaaauuuuuu! ");
    }
}

Para a classe Cachorro, poderíamos fazer de forma semelhante:

Classe Cachorro {
    Método fale() {
        imprimaNaTela(" Au au au! ");
    }
}

Agora, se fizermos um looping entre todos os objetos contidos em nosso array criado anteriormente enviando para cada objeto a mensagem “fale”, cada um deles irá ter um comportamento diferente, dependendo se é um Cachorro ou um Gato. Nosso looping entre todos os animais cadastrado no nosso sistema seria mais ou menos assim:

int cont;
    para cont de 0 a 100 faça {
        listaDeAnimais[cont].fale();
    }

Isto é polimorfismo! Uma mesma mensagem é enviada para diferentes objetos da mesma classe (Animal) e o resultado pode ser diferente, para cada caso. 🙂

Vantagens da POO
  • Os sistemas, em geral, possuem uma divisão de código um pouco mais lógica e melhor encapsulada do que a empregada nos sistemas não orientados a objetos. Isto torna a manutenção e extensão do código mais fácil e com menos riscos de inserção de bugs. Também é mais fácil reaproveitar o código.
  • É mais fácil gerenciar o desenvolvimento deste tipo de software quando temos uma equipe grande. Podemos fazer uma especificação UML antes de iniciar o desenvolvimento do software em si, e em seguida dividirmos o sistema em classes e pacotes, e cada membro da equipe pode ficar responsável por desenvolver uma parte do sistema.
Desvantagens da POO
  • Na minha opinião, o aprendizado do paradigma de programação orientada a objetos é bem mais complicado no início do que os velhos sistemas procedurais. Para começar a programar é necessário ter estabelecido uma série de conceitos bastante complexos. Já na programação procedural tradicional, basta decorar meia dúzia de comandos e você já consegue fazer um programa simples.
  • Dificilmente uma linguagem orientada a objetos conseguirá ter um desempenho em tempo de execução superior a linguagens não orientadas a objetos.
Conclusões

Espero que com este texto você tenha ao menos entendido o básico da orientação a objetos. Não se preocupe caso você tenha “viajado” em algumas partes do texto, é perfeitamente normal. Agora siga em frente, continue estudando e leia um bom livro sobre a linguagem de programação que quer aprender. Caso esteja dando seus primeiros passos no mundo orientado a objetos, um bom começo pode ser a linguagem Ruby. Ela é uma linguagem interpretada e bastante fácil de aprender, e ainda assim, possui um bom sistema de orientação a objetos, sendo um ótimo ambiente para começar. Além disso, Ruby é uma linguagens que vem crescendo bastante no mercado, sendo que tem se destacado bastante no desenvolvimento de softwares para web, substituindo linguagens como como PHP, Perl e Java.

~\\|//~
 -(o o)- RODRIGO SILVA

Deixe uma resposta

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