Programação orientada a Objectos

O que é?

Agora vamos abordar um tema que até a cinco anos atrás era considerado como um tópico avançado, mas hoje em dia a programação orientada a objectos tornou-se uma norma . Linguagens como o Java e o Python englobam o conceito de tal forma que muito dificilmente poderás fazer algo, sem cruzar com os objectos. Mas do que realmente se trata?

as melhores introduções acerca do assunto na minha opnião, são as seguintes:

Estes livros que mencionei, aumentam em volume e exactidão do conteúdo, a medida que vais descendo na lista. Para a os que não tem pretensões profissionais na programação, o primeiro dos livros é o mais adequado. Em caso que o interesse já seja mais aprofundado podes tentar o Object Orientaded Programming escrito pelo Timothy Budd (a 2ª edição). Pessoalmente ainda não tive oportunidade de o ler, mas pessoas de quem a opinião respeito têm me dito muito bem acerca do livro. E finalmente para informações em geral acerca de OO podes tentar este link: http://www.cetus.org

Mas assumindo que não tens tempo nem inclinação para ir ver todos esses livros agora, dou aqui uma curta introdução do conceito. (Nota: Certas pessoas consideram o conceito por vezes algo difícil enquanto outras o compreendem logo a primeira Em caso que faças parte do primeiro grupo, não faz mal porque mesmo assim podes trabalhar com objectos sem que tenhas compreendido na totalidade o conceito que esta por detrás).

Uma nota final, é que só iremos utilizar o Python neste capitulo, porque nem o BASIC nem o Tcl trabalha com objectos. É possível a implementação de um design de uma programação orientada a objectos numa linguagem não orientada a objectos. Isso é possível através de convenções de código, mas aconselho a não seguir por esse caminho. Se tens um problema em que uma programação orientada a a objectos é a melhor opção, então escolhe uma linguagem OO para trabalhares.

Dados e Funções de mão dadas.

Objectos são colecções de dados e funções que operam sobre esses mesmos dados. Uma vez juntos podemos passar os objectos de um lado para o outro dentro de um programa, e quando acedemos a esses objectos, não temos acesso aos atributos dos dados mas também aos operadores que trabalham esses dados.

Por exemplo, um objecto de string, não só guardaria os caracteres que formam essa string, mas também guardaria as funções que operam sobre as strings - como, length (calcular o comprimento), mudar de maiúscula para minúscula, etc.

Os objectos usam uma metáfora de transmissão, portanto, um objecto transmite uma determinada mensagem a um outro objecto, e esse outro objecto responde executando uma das operações, um método. Por outras palavras um objecto ao transmitir uma mensagem exige um recibo de entrega, sendo esse recibo a execução de uma das suas funções pelo objecto que recebe. Existem várias formas de aceder as funções de um objecto, sendo a mais popular o ponto. Para melhor vermos, nada melhor que um exemplo:

w = Widget() # cria uma nova instance, w, do widget
w.paint()  # envia a mensagem 'pintar', ao w 

Isto causaria a execução do método pintura do objecto Widget.

Definindo as Classes

da mesma forma que os dados podem ser de vários tipos, também os objectos podem ser de vários tipos. A uma colecção de objectos com características semelhantes dá-se o nome de class. Nós podemos definir uma class e depois criar instances dessa class, são estes instances que são os objectos. Nós depois podemos guardar uma referência destes objectos em variáveis dentro do nosso programa.

Vamos olhar para um exemplo concreto para ver se consigo explicar melhor, vamos criar uma class onde temos uma mensagem e um método para imprimir essa mesma mensagem.

class Message:
    def __init__(self, aString):
        self.text = aString
    def printIt(self):
        print self.text

Nota 1:Um dos métodos utilizados nesta class é o __init__, e é um método chamado constructor (construtor). A razão do nome deve-se ao facto cada vez que este método é utilizado, uma nova instance de um objecto é criado ou construído. Qualquer variável atribuída (e portanto criada no Python) dentro deste método tornasse-a única dessa nova instance. Existe um grande número destes métodos especiais e toda elas distinguem-se através do formato __xxx__ no nome.

Nota 2: Ambos métodos definiram o primeiro parâmetro como sendo o self. Este nome é apenas uma convenção mas indica a instance do objecto. Como iremos ver mais adiante este parâmetro é preenchido pelo interprete enquanto o próprio programa esta a decorrer e não pelo programador. É por isso é que o print pode ser chamado sem argumentos: m.printIt().

Nota 3:Nos chamamos a class de Message com um 'M' maiúsculo. Isto é puramente uma convenção, mas é bastante usada nos meios programacionais estendo-se a outras linguagens de OO. Uma convenção relacionada diz que os nomes do método devem começar com uma letra minúscula mas as palavras seguintes que façam parte do nome devem começar por maiúsculas. Sendo assim um método chamado "calculo balanço bancário" seria escrito da seguinte forma: calculoBalançoBAncário.

Em caso que queiras, podes re-visitar o capitulo sobre a 'Materia-prima' e olhar para a parte de fala "Operadores definidos pelo Utilizador" o exemplo sobre as moradas já deve fazer um pouco mais de sentido. Na realidade o único de tipo de dados consequentemente operadores definidos pelo utilizador no Python é o class. Uma class com atributos mas sem métodos, excepto o __init__ é o equivalente a um record no BASIC.

Para que servem as Classes

Uma vez definida a class, já podemos então criar instances dessa mesma class para que a possamos manipular, ora vejamos o que podemos fazer com a nossa class Message:

m1 = Message("Olá Mundo!")
m2 = Message("Adeus, foi pouco mas soube bem")

note = [m1, m2] # coloca os objectos numa lista
for msg in note:
    msg.printIt() # imprime uma mensagem de cada vez

Então basicamente, nós podemos trabalhar com a class da mesma forma que trabalhamos com qualquer outro tipo de dados no no Python, mas também a era mesmo essa a função deste exercício!

A mesma coisa, um aspecto diferente

Então tudo que aprendemos até agora foi, definir uma class e criar instances dessa class, para mais tarde atribuirmos essas instances a uma variável. Depois nós podemos passar mensagens a esses objectos, mensagens essas que irão iniciam um dos métodos que anteriormente definimos. Mas isso não é tudo, no que diz respeito a OO, ainda existe um outro elemento, que para muitos é o aspecto principal da programação orientada a objectos.

Se tivermos dois objectos de classes diferentes que respondem ao mesmo tipo de mensagens, mas com comportamentos diferentes, ou seja, cada com o seu método, então podemos pegar nesses objectos e trata-lo de uma forma idêntica dentro do nosso programa, sabendo nós que cada objecto irá responder com um método diferente. Esta capacidade de se poderem comportar de uma forma diferente ao mesmo tipo de mensagem é conhecida como polimorfismo.

De uma forma geral podíamos usar esta característica para fazer com que um determinado número de diferentes objectos gráficos mudassem de cor ao receber a mensagem 'pintar'. Apesar de um triângulo e um circulo terem formas geométricas diferentes e serem objectos diferentes, mas ambos responderiam a mensagem 'pintar'. Nós como programadores, neste caso, podemos ignorar as diferenças geométricas entre os objectos e pensar neles como apenas formas geométricas.

Vamos ver um exemplo onde os objectos onde em vez de enviarmos uma mensagem para se pintarem calculamos as áreas:

Primeiro criamos a class Circunferência e a class Quadrado:

class Quadrado:
    def __init__(self, lados):
        self.lados = lados
    def calculaArea(self):
        return self.lados**2

class Circulo:
    def __init__(self, radius):
        self.radius = radius
    def calculaArea(self):
        import math
        return math.pi*(self.radius**2)

Agora criamos uma lista de formas geométricas (quadrados e círculos) e depois imprimimos as suas areas:

list = [Circulo(5),Circulo(7),Quadrado(9),Circulo(3),Quadrado(12)]

for formasgeo in list:
    print "A área é: ", formasgeo.calculateArea()

A combinação das ideias aqui faladas e o uso de módulos, proporciona-nos um poderoso mecanismo para reutilizar-mos o nosso código. Imagina que colocas as definições desta class num módulo chamado 'formasgeo.py' e cada vez que quiseres saber a área de um objecto, simplesmente importas módulo. Isto é exactamente o que foi feito com a larga maioria dos módulos que já vem no Python, é por isso que o modo de aceder aos métodos de objecto se parece muito com a forma como se usam funções de um módulo.

Heranças

A herança é um mecanismo muitas vezes usado para implementar o polimorfismo. Aliás em muitas linguagens de OO é a única forma de implementar o polimorfismo. E funciona da seguinte forma:

Uma class pode herdar os atributos e as operações de uma class mãe ou super class. Mas isto significa que a a nova class, que é idêntica a class progenitora em muitos aspectos, tenha que herdar todos os métodos da class progenitora. O que acontece na realidade é que a uma nova class herda as características de outra class, mas aquelas que características que não são necessárias são por simplesmente descartadas e substituídas por uma novas características criadas.

Mas nada melhor que um exemplo para ilustrar o que foi dito. Vamos usar uma class hierárquica de contas bancárias, onde podemos depositar, levantar e obter um balanço da nossa situação financeira. Mas da mesma maneira que existem contas bancárias diferentes, aqui também vamos ter contas diferentes - num determinado tipo de contas recebemos juros, mas noutro somos cobrados por cada movimento efectuado.

A class contaBancária.

Vamos então por mãos a obra. Primeiro vamos pensar de forma muito geral (e abstracta), de quais devem ser os atributos e operações que uma conta bancária deveria ter:

Por forma a podermos executar estas operações, vamos precisar de um número de identificação bancária (para as operações de transferência) e de deposito inicial.

Então vamos a isso:

BalanceError = "Lamentamos mas a sua conta só tem $%6.2f"

class ContaBancaria:
    def __init__(self, quantiaInicial):
       self.saldo = quantiaInicial
       print "Abriu a sua conta com %5.2f de quantia inicial" % self.saldo

    def deposito(self, quantia):
       self.saldo = self.saldo+ quantia

    def levantamento(self, quantia):
       if self.saldo >= quantia:
          self.saldo = self.saldo - quantia
       else:
          raise BalanceError % self.saldo

    def verificarSaldo(self):
       return self.saldo
       
    def transferencia(self, quantia, conta):
       try: 
          self.levantamento(quantia)
          account.deposito(quantia)
       except BalanceError:
          print BalanceError

Nota 1: Repara como verificamos sempre o saldo antes de efectuar um levantamento, e repara também no uso de excepções para se lidar com os possíveis erros. Claro que não existe nenhum tipo de erro chamado BalanceError, então nós tivemos que criar um, que é simplesmente uma string!

Nota 2: O método da transferencia usa os métodos levantamentos e deposito do BankAccount para efectuar a transferencia. Isto é muito comum em OO e é conhecido como self messaging (passar uma mensagem a si mesmo). Isto significa que as futuras classes derivadas desta podem tem as suas próprias versões de levantamentos/deposito mas o método das transferencias continuará o mesmo.

Uma ContaAJuros

Agora podemos usar a herança para criar uma conta que adiciona juros (vamos dizer 3%) em cada deposito. Esta nova class vai ser idêntica contaBáncaria, exceptuando no método do deposito. O que vamos fazer é simplesmente recriar esse método:

class ContaAJuros(ContaBAncaria):
   def deposito(self, quantia):
       ContaBancaria.deposito(self,quantia)
       self.saldo = self.saldo * 1.03
       

E é tudo. Agora começamos a ver o verdadeiro poder das linguagens OO. Todos os outros métodos foram herdados da ContaBancaria (para isso bastou colocar ContaBancaria entre parêntesis logo depois do nome da nova class). Repara que o método de deposito chamou o método de deposito da superclass em vez de o copiar. Então agora se fizermos uma mudança no método do deposito da ContaBancaria para incluir suponhamos que, alguma função para interceptar erros, a nova sub-class irá imediatamente ganhar esses atributos.

A ContaComTaxas

Mais uma vez esta conta será idêntica a conta mãe ContaBancaria, com a excepção que desta vez cada vez que levantarmos dinheiro ela irá cobrarmos uma determinada quantia. E tal como fizemos na ContaAJuros criamos uma conta que herda todas as características da ContaBancaria, e depois nós mudamos o método do levantamento.

class ContaComTaxas(ContaBAncaria):
    def __init__(self, quantiaInicial):
        ContaBancaria.__init__(self, quantiaInicial)
        self.taxa = 3
        
    def levantamento(self, quantia):
        ContaBAncaria.levantamento(self, quantia+self.taxa)

Nota 1: Repara que nós guardamos a taxa que se cobras como uma variável da instance, isto para permitir que mais tarde caso queiramos se possa mudar. Repara também como podemos herdar o __init__ da mesma forma que herdamos outro método qualquer.

Note a: Para o novo método de levantamento, nós não fizemos mais nada senão adicionar o taxa a cobrar, e depois chamamos o método antigo para fazer todo o trabalho.

Nota 3: Aqui também introduzimos o nosso primeiro efeito secundário, que é, cada vez que uma transferencia é efectuada também se é cobrada uma taxa por isso, mas como também era essa a nossa intenção mesmo, não faz mal.

Testando o nosso sistema

Para testar e ver se tudo isto funciona, tenta executar este código que se segue (pode ser na prompt do Python ou podes criar um ficheiro a parte)

from ContaBancaria import *

# Primeiro uma ContaBancaria standart
a = ContaBancaria(500)
b = Contabancaria(200)
a.levantamento(100)
# a.levantamento(1000)
a.transferencia(100,b)
print "A = ", a.verificarSaldo()
print "B = ", b.verificarSaldo()


# agora vamos testar a ContaAJuros
c = ContaAJuros(1000)
c.deposito(100)
print "C = ", c.verificarSaldo()


# e finalmente a ContaComTaxas
d = ContaComTaxas(300)
d.deposito(200)
print "D = ", d.verificarSaldo()
d.levantamento(50)
print "D = ", d.verificarSaldo()
d.transferencia(100,a)
print "A = ", a.verificarSaldo()
print "D = ", d.verificarSaldo()


# Faz uma transferência entre a ContaComTaxas e a ContaAJuros
# enquanto a ContaComTaxas retira a ContaAJuros 
# adiciona
print "C = ", c.verificarSaldo()
print "D = ", d.verificarSaldo()
d.transferencia(20,c)
print "C = ", c.verificarSaldo()
print "D = ", d.verificarSaldo()
Agora tira a marca do comentário da linha a.levantamento(1000), para veres a excepção a funcionar.

E é tudo. Penso que isto é um exemplo algo simples, mas que consegue explicar como a herança pode ser usada para estender uma framework simples para algo muito mais complexo.

Nós vimos como criar este exemplo por fases e como testar o nosso programa para ver se ele funciona. Os testes que executamos no programa, não estão completos porque podíamos adicionar mais testes - tal como o que fazer quando uma conta é criada com saldo negativo.

Mas espero que tenhas ganho o gosto pela programação orientada a objectos, e agora podes passar para um tutorial mais avançado ou ler um dos livros que mencionei logo ao inicio para poderes ver mais exemplos.


Anterior  Próxima  Índice


Em caso que tenhas alguma dúvida ou queiras comentar esta página envia-me um e-mail para: babyboy@oninet.pt