Encapsulamento, Polimorfismo e Herança em Python
          
          Bernardo Fontes
          
          59º Encontro do PUG-PE
          Recife/PE
          08 de Junho de 2019
        
        
        
        
          ### Roteirinho
          - Encapsulamento
          - Information Hiding
          - Polimorfismo
          - Herança
        
        
          ### Encapsulamento
          - Definição básica: mecanismo nas linguagens de promamação para **restringir o acesso direto aos dados** de um objeto
          - Pelo histórico da nossa indústria, se tornou um conceito muito relacionado ao Java
          - Em Java, o encapsuamento é **obrigatório**
          - Exemplos inspirados no blogpost "[Encapsulation is not information hiding](https://www.javaworld.com/article/2075271/encapsulation-is-not-information-hiding.html)"
        
        
          ### Atributos públicos
          ```java
          public class Position {
            public double latitude;
            public double longitude;
          }
          // em uma outra parte do código
          Position pos = new Position();
          pos.latitude = 10.0;
          pos.longitude = 50.0;
          ```
          - O que acontece se colocarmos 1000.0 como latitude?
        
        
          ### Avaliando estados
          - Como o intervalo de valores válidos para latitude vai de -90 a 90 teremos problemas ao usarmos 1000
          - O problema é o código permitir um **estado inválido** dentro da aplicação
          - O estado é uma leitura **qualitativa do total de dados** de um objeto
          - Se um dos dados do objeto, por exemplo o saldo em uma conta bancária, muda, o estado precisa ser **"reavaliado"**
          - **Estados válidos** configuram os estados com os quais o sistema tem condições de lidar
          - **Estados inválidos** são grandes causadores de bugs e falhas no software
        
        
          ### Atributos privados + métodos de acesso
          ```java
          public class Position {
            private double latitude;
            private double longitude;
            public Position( double latitude, double longitude ) {
              setLatitude( latitude );
              setLongitude( longitude );
            }
            public void setLatitude( double latitude ) {
              // Ensure -90 <= latitude <= 90 using modulo arithmetic.
              // Code not shown.
              // Then set instance variable.
              this.latitude = latitude;
            }
            public void setLongitude( double longitude ) {
              // Ensure -180 < longitude <= 180 using modulo arithmetic.
              // Code not shown.
              // Then set instance variable.
              this.longitude = longitude;
            }
            public double getLatitude() {
              return latitude;
            }
            public double getLongitude() {
              return longitude;
            }
          }
          // em outra parte do código
          // *todas* as partes que usarem latitude e longitude vão quebrar
          // no caso de java, não vamos nem conseguir compilar o código
          pos.latitude = 10.0;
          pos.longitude = 50.0;
          ```
        
        
          ### Tá, mas e no Python?
          - Python provê mecanismos de encapsulamento **mais elegantes e menos impositivos** sobre a interface dos objetos
          - Podemos **postergar ao máximo** qualquer decisão de encapsulamento
        
        
          ### Sem encapsuamento
          ```python
          class Posicao():
              def __init__(self, lat, lng):
                  self.lat, self.lng = lat, lng
          p1 = Posicao(12.0123, 36.12313)
          p1.lat = 20.8
          p1.lng = 36.9
          ```
        
        
          ### Com encapsuamento pythônico
          ```python
          class Posicao():
              def __init__(self, lat, lng):
                  self.lat, self.lng = lat, lng
              @property
              def lat(self):
                return self._lat
              @lat.setter
              def lat(self, value):
                if not -90 <= value <= 90:
                  raise ValueError("Latitude inválida")
                self._lat = value
              @property
              def lng(self):
                return self._lng
              @lng.setter
              def lng(self, value):
                if not -180 <= value <= 180:
                  raise ValueError("Longitude inválida")
                self._lng = value
          # O código abaixo continua funcionando sem problemas
          p1 = Posicao(12.0123, 36.12313)
          p1.lat = 20.8
          p1.lng = 36.9
          ```
        
        
          ### Da visão geral de encapsulamento
          - Limitada **aos dados** e a maneira como são expostos
          - Atributos possuem **restrições**:
            - `public`
            - `protected`
            - `private`
          - Construções de **métodos de acesso** (`get` ou `set`) para modificar seus valores
          - **Nem todas as linguagens** viabilizam esses 3 estados
          - "Protected" em Python: `self._varname` como convenção
          - "Private" em Python: `self.__varname`
          - Maaaas.....
        
        
            *Never, ever use two leading underscores. This is annoyingly private. If name clashes are a concern, use explicit name mangling instead (e.g., _MyThing_blahblah). This is essentially the same thing as double-underscore, only it's transparent where double underscore obscures.*
            [Ian Bicking - Paste Style Guide](https://paste.readthedocs.io/en/latest/StyleGuide.html)
        
        
          ### Um outro exemplo...
          ```python
          class CalendarioEventos():
              def __init__(self):
                  self._eventos = []
              @property
              def eventos(self):
                  return self._eventos
          ####### em outra parte do código:
          calendario = CalendarioEventos()
          calendario.evento.append({
              "nome": "59º Encontro PUG-PE",
              "dia": date(2019, 6, 8)
          })
          calendario.evento.append({
              "nome": "5ª Noite de Processing - Recife",
              "dia": date(2019, 6, 26)
          })
          ```
          - O que acontece se mudarmos o atributo `_eventos` de um `list` para um `set`?
        
        
          ### Refinando o que é encapsulamento
          - Só **encapsular o acesso aos dados** não implica necessariamente em ter um bom código OOP
          - Melhorando a definição: *Mecanismo nas linguagens de promamação para **restringir o acesso direto aos dados** de um objeto através de **métodos que operem sobre eles***
          - Mesmo com o encapsulamento sendo usado na classe `CalendarioEventos`, seus clientes sabem de seus **detalhes de implementação** como, por exemplo, de que `eventos` é uma lista.
          - Não só isso, o cliente é quem está determinando **como os dados de um evento serão armazenados** já que ele modifica diretamente a estrutura de dados que os comporta
        
        
          ### Usando Informatino Hiding
          ```python
          class CalendarioEventos():
              def __init__(self):
                  self._eventos = set()
              def adiciona_evento(self, nome, dia):
                  self._eventos.add((nome, dia))
              @property
              def eventos(self):
                  return [{"nome": e[0], "dia": e[1]} for e in self._eventos]  # agora é só uma cópia de _eventos
          ####### em outra parte do código:
          calendario = CalendarioEventos()
          calendario.adiciona_evento(
              nome="59º Encontro PUG-PE",
              dia=date(2019, 6, 8)
          )
          calendario.adiciona_evento(
              nome="5ª Noite de Processing - Recife",
              dia=date(2019, 6, 26)
          )
          ```
          - O método `adiciona_evento` escondeu todos os detalhes de implementação de `CalendarioEventos`
        
        
            ### Benefícios de Information Hiding
            - Um código **baseado em dados** é, por definição, mais **rígido** porque suas integrações estão diretamente acoplados às **decisões de implementação** dos seus diversos objetos;
            - Em contrapartida, um código **baseado em comportamento** é, por definição, mais **flexível** porque suas integrações dependem de **abstrações** e não em suas implementações concretas;
            - Portanto, o pote de ouro OOP não é o encapsulamento por si só, mas sim a **troca de mensagens** através de métodos que funcionam como **mecanismos de proteção** da classe;
        
        
          ### Tell, don't ask
          - Pensamento declarativo VS pensamento procedural
          - Dados **não possuem semântica** e dizem respeito somente a **decisões de implementação**
          - Declarativo == troca de **mensagens**
        
        
          ### Alan Kay (criador do termo OOP e da Smalltalk)
          "OOP to me means only **messaging**, **local retention** and **protection** and **hiding of state-process**, and extreme **late-binding** of all things. It can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I'm not aware of them."
          [Referência](http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay_oop_en)
        
        
          ### Late-binding???
          - Mecanismos da linguagem que permitem de que o **método ou objeto** que esteja sendo chamado seja **definido em tempo de execução**
          - **Polimorfismo** é só uma das algumas maneiras atingir late-binding
          - Por ser praticamente **a única maneira viável** de se fazer em Java, nos parece que é a única
          - Felizmente, Python é uma linguagem mais orientada a objetos...
        
        
          ### Python Data model
          ```python
          def print_info(dado):
              print(f"Analisando o objeto {dado}")
              num = len(dado)
              print(f"O objeto possui {num} elementos.\n")
          dados = [
              "meu nome é bernardo",
              ["valor 1", "valor 2"],
              ("eu", "adoro", "tuplas"),
              {"chave": "valor"},
              {1, 2, 3, 4, 5, 5, 5, 5, 6, 1, 1, 1,},
              range(10),
              42  # será que funciona?
          ]
          for dado in dados:
              print_info(dado)
          ```
          - Será que conseguimos usar `len` com o nosso calendário de eventos?
        
        
          ### `__len__`
          ```python
          class CalendarioEventos():
              def __init__(self):
                  self._eventos = set()
              @property
              def eventos(self):
                  return [{"nome": e[0], "dia": e[1]} for e in self._eventos]  # agora é só uma cópia de _eventos
              def adiciona_evento(self, nome, dia):
                  self._eventos.add((nome, dia))
              def __len__(self):
                  return len(self._eventos)
          ####### em outra parte do código:
          calendario = CalendarioEventos()
          calendario.adiciona_evento(
              nome="59º Encontro PUG-PE",
              dia=date(2019, 6, 8)
          )
          calendario.adiciona_evento(
              nome="5ª Noite de Processing - Recife",
              dia=date(2019, 6, 26)
          )
          num = len(calendario)
          print(f"Temos {num} eventos no nosso calendario")
          ```
        
        
          ### Duck typing
          If it walks like a duck and it quacks like a duck, then it must be a duck
          ```python
          def adiciona_elementos(obj, elementos):
              try:
                for elem in elementos:
                  obj.append(elem)
              except AttributeError:
                print(f"Objeto {obj} não implementa um método append")
          class CalendarioEventos():
              def __init__(self):
                  self._eventos = set()
              @property
              def eventos(self):
                  return [{"nome": e[0], "dia": e[1]} for e in self._eventos]  # agora é só uma cópia de _eventos
              def adiciona_evento(self, nome, dia):
                  self._eventos.add((nome, dia))
              def __len__(self):
                  return len(self._eventos)
              def append(self, event_data):
                  self.adiciona_evento(*event_data)
          lista_simples = []
          calendario = CalendarioEventos()
          dict_simples = {}
          eventos = [('Evento 1', date(2019, 1, 20)), ('Evento 2', date(2020, 3, 14))]
          ```
        
        
          ### Mas e o Polimorfismo?
          - No polimorfismo, definimos uma **única classe** que determina uma **interface** com a qual o sistema vai interagir
          - As partes do código devem saber interagir **somente** com os métodos definidos por essa interface
          - Herança é **um mecanismo** para que classes possam implementar interfaces
          - Herança **não é** um mecanismo para reaproveitamento de código
        
        
          ### Polimorfismo em Python
          ```python
          from abc import ABC, abstractmethod
          class DBAccessInterface(ABC):
            @abstractmethod
            def get_by_id(self, id_):
              pass
          class JsonDB(DBAccessInterface):
            def get_by_id(self, id_):
               return self.json_data.get(id_)
          class CsvDB(DBAccessInterface):
            def get_by_id(self, id_):
              for row in self.csv_data:
                if row.id == id_:
                  return row
              return None
          class TxtDB(DBAccessInterface):
             """
             Como essa classe não implementa a interface, não conseguimos instanciá-la
             """
          ```
        
        
          ### Princípio de Substituição de Liskov
          - Proposto por Barbara Liskov, uma cientista da computação americana
          - *Funções que usem ponteiros ou referências **para classes bases** devem ser capazes de utiizar **objetos das classes derivadas** sem precisar saber que está lidando com uma classe derivada*
        
        
          ### Discutindo um pouco de herança
          ```
          class Rect:
            def __init__(self, w, h):
              self.w = w
              self.h = h
            @property
            def area(self):
              return self.w * self.h
            @property
            def perimetro(self):
              return self.w * 2 + self.h * 2
          class Square(Rect):
            def __init__(self, size):
              self.w = size
              self.h = size
            @property
            def h(self):
              return self._h
            @h.setter
            def h(self, value):
              self._h = value
              self._w = value
            @property
            def w(self):
              return self._h
            @w.setter
            def w(self, value):
              self._h = value
              self._w = value
          ```
          - Acham isso justo?
        
        
          ### O que acontece nesse caso?
          ```
          def renderiza_rect(rect):
              max_area = 1200
              if rect.area > max_area:
                rect.w = 30
                rect.h = 40
              # lógica para renderizar
          ```
          - Acham isso justo?
          - Problema causado por usar Herança como mecanismo para **reutilização de código** e não para **implementação de comportamento**
          - Sabem onde rola **muito** isso? Django class based views
        
        
          ### Resumo
          - Encapsulamento nos é útil para **proteger nosso objeto de estados inválidos**
          - Python viabiliza que nos preocupemos com isso só **quando for necessário**
          - Temos que usar **Information Hiding** para criar abstrações que escondam nossas implementações
          - Essas abstrações definem a **interface** de um objeto
          - Todo nosso código deve **confiar na interface dos objetos** e não nos seus detalhes de implementação
          - **Interfaces bem definidas** nos permitem usar bastante **late-binding** em Python
          - Polimorfismo é só **uma das maneiras** de atingir late-binding
          - Cuidado com soluções por heranças e prefira **composição**