4 pilares da Orientação a Objetos em Python

A linguagem Python é considerada uma linguagem multiparadigma, ou seja, possibilita que o desenvolvedor programe utilizando diferentes tipos de programação como procedural, funcional, imperativa e orientação a objetos.

A programação orientada a objetos é um dos paradigmas mais utilizados atualmente, porque através dela é possível organizar a arquitetura do programa trazendo uma perspectiva mais perto do mundo real, considerando as coisas (abstratas ou concretas) do mundo real como objetos no sistema, que interagem entre si e dão vida ao fluxo do programa.

E para garantir uma programação orientada a objetos eficiente, existem alguns pilares desse paradigma que podemos utilizar em programas escritos com Python.

1º Pilar: Abstração

Na programação orientada a objetos, a construção dos objetos é baseada em uma Classe que representa as características daquele objeto.

Abstração é o princípio de criar uma classe que contenha atributos e métodos que são comuns a outras classes e que podem servir como base para serem herdados.

Você abstrai características comuns a N classes e fornece uma classe abstrata que pode ser herdada e servir de base para as demais.

Nesse conceito, podemos mencionar também a questão de fornecer uma classe abstrata como um contrato para que quando herdada garanta que as classes filhas irão implementar os métodos necessários, dessa forma, proteger o nosso código dando a certeza da existência e implementação do método.

Para ilustrar a abstração, imagine um sistema bancário, onde uma Conta bancária possa ser de diversos tipos, como, Conta poupança, Conta Corrente, Conta PJ, etc.

Cada tipo de conta, ainda é uma Conta, e as características comuns a todos os tipos de contas podem ser Abstraídas em uma classe abstrata. Por exemplo:

from abc import ABCMeta, abstractmethod

class Conta(metaclass=ABCMeta):
    _numero = "00000"
    _titular = "root"
    saldo = 0

    @abstractmethod
    def __init__(self, numero: str, titular: str, saldo: float):
        self._numero = numero
        self._titular = titular
        self.saldo = saldo

    @abstractmethod
    def sacar(self, value: float):
        pass

    @abstractmethod
    def depositar(self, value: float):
        pass

    @abstractmethod
    def exibir_saldo(self):
        pass

Dessa forma eu crio uma abstração do conceito de Conta e disponibilizo uma classe para ser herdada por outras classes que são do tipo Conta.

Obs: Uma classe abstrata não deve ser utilizada diretamente, deve ser vista e utilizada como uma base para outras classes como ContaCorrente, ContaPoupanca, etc.

2º Pilar: Encapsulamento

O princípio de encapsulamento consiste em "esconder" a parte funcional dos objetos de forma que quem estiver utilizando não tenha que conhecer mais do que o necessário para utiliza-lo.

Por exemplo, ao dirigir um carro, se quisermos que o carro pare de andar, não é preciso, conhecer toda a mecânica do funcionamento por dentro do veículo para que possamos frear. Basta pisar no freio, o freio é o encapsulamento do comportamento de frear de um carro.

Na orientação a objetos, estruturamos as classes de forma a encapsular toda regra de negócio e parte funcional relacionada a classe dentro de métodos e atributos, de forma que quem utilize, apenas diga oque quer fazer.

Por exemplo:

class Biblioteca:

    def __init__(self, livros_disponiveis) -> None:
        self.livros_disponiveis = livros_disponiveis

    def exibir_livros(self):
        for livro in self.livros_disponiveis:
            print(livro)

    def emprestar_livro(self, livro):
        print(f'Você escolheu o livro: {livro}')
        if livro in self.livros_disponiveis:
            self.livros_disponiveis.remove(livro)
        else:
            print('Desculpe o livro não está disponível!')

    def devolver_livro(self, livro):
        self.livros_disponiveis.append(livro)
        print(f'Obrigado pro devolver o livro: {livro}')

Na classe biblioteca, temos os métodos que fornecem as regras de negócio para que o programa que for utilizar não precise conhecer oque está acontecendo ali dentro e sim possa simplesmente dizer:

// biblioteca me mostre os livros:
biblioteca.exibir_livros()

3º Pilar: Herança

A herança consiste em determinar que uma classe existe por si mesma, porém, ela é uma outra classe em sua essência... Por exemplo, uma ContaCorrente existe mas ela é uma Conta.

Quando a herança é utilizada, a classe que herda, automaticamente, possui os atributos e métodos definidos na classe da qual herdou.

Por exemplo, imagine que o sistema bancário tenha uma uma regra geral para deposito e exibição de saldo para todos os tipos de conta somente o saque muda de conta para conta. Nesse caso, podemos criar uma classe Conta com as características comuns das outras Contas, e as outras contas, irão herdar essas características:

class Conta():
    _numero = "00000"
    _titular = "root"
    saldo = 0

    def __init__(self, numero: str, titular: str, saldo: float):
        self._numero = numero
        self._titular = titular
        self.saldo = saldo

    def depositar(self, value: float):
        # Regra para fazer o depósito...

    def exibir_saldo(self):
        # Exiba o saldo...

# Para aplicar a herança em Python, basta referênciar a classe que se quer herdar entre parenteses:
class ContaPoupaca(Conta):

    def __init__(self, numero: str, titular: str, saldo: float):
        super().__init__(numero, titular, saldo)

    def sacar(self, value: float):
        if value <= self._saldo:
            self._saldo -= value
            return True

        return False

    def titular(self):
        return self._titular

    def numero(self):
        return self._numero

A classe ContaPoupaca herda tudo de Conta, ou seja, possui os métodos depositar e exibir_saldo e os atributos _numero, _titular e saldo e ainda pode implementar outros métodos e definir outros atributos que sejam só dela.

4º Pilar: Polimorfismo

O conceito do polimorfismo é permitir que comportamentos comuns a N tipos de classes possam ser definidos de forma especifica para cada classe.

Imagine que um sistema bancário, define que uma ContaCorrente e uma ContaPoupaca devem ter saldo, titular, numero da conta, um método para sacar, depositar e exibir o saldo, mas, cada classe tem suas próprias regras de negócio, por exemplo, na ContaCorrente, o saque vai ter algum tipo de desconto dependendo de onde for sacado e a ContaPoupanca o deposito vai ser salvo em outro tipo de saldo interno, etc.

A questão é, possuem comportamentos diferentes mas devem possuir os comportamentos porque ambas são Contas.

Por exemplo, eu posso criar uma classe abstrata chamada Conta e a partir dela definir quais são os atributos e comportamentos necessários para quem for herdar dela mas as regras de negócio cada classe implementa a sua maneira, com o caso aqui em que ContaPoupanca herda de Conta (classe abstrata) e implementa suas próprias regras de negócio (gerando o comportamento polimórfico das Contas):

from abc import ABCMeta, abstractmethod

class Conta(metaclass=ABCMeta):
    _numero = "00000"
    _titular = "root"
    _saldo = 0

    @abstractmethod
    def __init__(self, numero: str, titular: str, saldo: float):
        self._numero = numero
        self._titular = titular
        self._saldo = saldo

    @abstractmethod
    def sacar(self, value: float):
        pass

    @abstractmethod
    def depositar(self, value: float):
        pass

    @abstractmethod
    def exibir_saldo(self):
        pass


class ContaPoupanca(Conta):

    def __init__(self, numero: str, titular: str, saldo: float):
        super().__init__(numero, titular, saldo)

    def sacar(self, value: float):
        if value <= self._saldo:
            self._saldo -= value
            return True

        return False

    def depositar(self, value: float):
        if value > 0:
            self._saldo += value
            return True

        return False

    def exibir_saldo(self):
        return self._saldo

    def titular(self):
        return self._titular

    def numero(self):
        return self._numero


class BankingSystem:
    __contas = []

    def __gerar_numero_conta(self):
        numero = len(self.__contas) + 1
        return f"{numero:05}"

    def __checar_valor_positivo(self, value):
        if value <= 0:
            return False

        return True

    def criar_conta_poupanca(self, deposito_inicial: float, nome_titular: str):
        if self.__checar_valor_positivo(deposito_inicial) == False:
            print("Deposito Inicial deve ser maior que Zero!")
            return

        conta = Savingsconta(self.__gerar_numero_conta(), nome_titular, deposito_inicial)
        self.__contas.append(conta)
        print("Conta criada com sucesso!")
        print("Titular: ", conta.titular())
        print("Número da conta: ", conta.numero())
        print("Saldo: ", conta.exibir_saldo())

    def total_of_contas(self):
        print("contas: ", str(len(self.__contas)))

    def acessar_conta(self, nome_titular: str, conta_numero: str):
        contas = [acc for acc in self.__contas 
                    if acc.titular() == nome_titular and acc.numero() == conta_numero]
        if len(contas) == 0:
            print("Conta não existe!")
            return

        conta = contas[0]
        print("Digite 1 para sacar")
        print("Digite 2 para depositar")
        print("Digite 3 para exibir saldo")
        escolha_usuario = (int(input()))
        if escolha_usuario == 1:
            print("sacar")
            print("Digite o valor a sacar")
            value = float(input())
            if conta.sacar(value):
                print("Saque realizado com sucesso!")
            else:
                print("Problema ao sacar, verifique o saldo!")
        elif escolha_usuario == 2:
            print("DEPOSITAR")
            print("Digite o valor do depósito")
            value = float(input())
            if conta.depositar(value):
                print("Deposito realizado com sucesso!")
            else:
                print("Problema ao depositar, valor não permitido!")
        elif escolha_usuario == 3:
            print(conta.exibir_saldo())
        else:
            print("Escolha inválida!")

banking_system = BankingSystem()
while True:
    print("Digite 1 para criar uma conta poupança")
    print("Digite 2 para acessar a conta")
    print("Digite 3 para exibir o total de contas")
    print("Digite 0 para sair")
    escolha_usuario = (int(input()))
    if escolha_usuario == 1:
        print("CREATE")
        print("Digite o nome do titular da conta")
        nome_titular = input()
        print("Digite o deposito inicial")
        initial_deposit = float(input())
        banking_system.criar_conta_poupanca(initial_deposit, nome_titular)
    elif escolha_usuario == 2:
        print("ACCESS")
        print("Digite o name do titular da conta")
        nome_titular = input()
        print("Digite o conta numero")
        conta_numero = input()
        banking_system.access_conta(nome_titular, conta_numero)
    elif escolha_usuario == 3:
        banking_system.total_of_contas()
    elif escolha_usuario == 0:
        quit()
    else:
        print("Escolha inválida!")

19