Posts da Tag: Orientação a objetos - Blog da TreinaWeb

Guias Java

Guia da linguagem Java

O Java é uma poderosa linguagem de programação em constante crescimento, sendo uma das mais populares entre os desenvolvedores. Com um forte desenvolvimento orientado a objetos, ela favorece o desenvolvimento de sistemas flexíveis e extensíveis.

É uma linguagem gratuita, tendo isso como um dos principais fatores que ajudou a popularizá-la entre os programadores e empresas no geral. O objetivo dessa linguagem é ser simples, orientada a objetos e de fácil aprendizagem.

Aprender Java é importante para qualquer profissional da área de desenvolvimento, pois essa é uma das linguagens que mais oferecem oportunidades no mercado de trabalho.

Você pode encontrar Java em diferentes dispositivos, como no acesso a serviços bancários online e até mesmo em aplicativos da Receita Federal, como por exemplo, o programa de declaração de imposto de renda. Empresas como IBM, a rede social LinkedIn e a Netflix também utilizam Java em seus produtos.

O Java é uma das linguagens mais populares e existem várias oportunidades de emprego. Já temos um artigo aqui que apresenta cinco fortes motivos para que você estude a plataforma de desenvolvimento da Oracle. Recomendamos a leitura!

Principais IDEs para desenvolvimento Java

Uma IDE (Ambiente de Desenvolvimento Integrado) é um software que auxilia no desenvolvimento de aplicações. Para o desenvolvedor, é uma forma de criar aplicações de maneira mais rápida, uma vez que estas IDEs auxiliam em todo o processo de desenvolvimento de uma aplicação.

As IDEs provem diversos benefícios, como a análise de todo o código a ser escrito para identificar bugs causados por um erro de digitação, autocompletam trechos de códigos, e etc.

Algumas das principais utilizadas no desenvolvimento em Java, são o Eclipse, NetBeans, VSCode e IntelliJ. Você pode conferir mais sobre elas no artigo “Principais IDEs para desenvolvimento Java”.

Como instalar o Java e nosso primeiro exemplo

Como o Java é multiplataforma, você pode instalar em qualquer sistema: Windows, Linux e MacOS. Já temos um artigo que aborda todo o passo a passo de instalação, em todos esses sistemas, para que você já deixe sua máquina pronta para começar.

Como qualquer outra linguagem, o Java tem as suas próprias regras de sintaxe e estrutura.

Vamos imprimir o tradicional “Hello World” e você verá como é fácil!

public class TreinaWeb {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

No mesmo artigo onde ensinamos você a instalar o Java em sua máquina, também abordamos com mais profundidade sua sintaxe e estrutura.

Desenvolvedor Java
Formação: Desenvolvedor Java
A formação Desenvolvedor Java da TreinaWeb tem como objetivo apresentar o desenvolvimento através do Java e todo o ecossistema para desenvolvimento da Oracle. Nesta formação, são desde tópicos básicos como o paradigma orientado a objetos, a preparação do ambiente de desenvolvimento para o Java através do Eclipse e o controle de versão de código através do Git e do GitHub. Até aspectos mais avançados como acesso a bancos de dados relacionais e o desenvolvimento de aplicações web com o Java.
CONHEÇA A FORMAÇÃO

Conhecendo variáveis e constantes

O Java é uma linguagem de tipagem estática, ou seja, existe um sistema de tipos bem definido e que é validado durante o tempo de compilação do código. Para isso, é necessário definir tipos explícitos para as variáveis que são criadas.

Por padrão, o Java possui em sua biblioteca padrão alguns tipos de dados:

• Byte: um inteiro com valor 8 bits, onde seu intervalo de valores vai do -128 até o 127. É um dado leve, que ocupa pouco espaço em memória;
• Char: trata-se de um único caractere unicode de 16 bits. Seu intervalo de valores vai do ‘\u0000’ ou 0 até o valor máximo ‘\uffff’ ou 65535. É utilizado para armazenar caracteres de maneira isolada;
• Short: trata-se de um valor inteiro de 16 bits. Seu intervalo de valores vai do -32.768 até o 32.767;
• Int: trata-se de um valor inteiro de 32 bits. Suporta valores de -2.147.483.648 a 2.147.483.647;
• Long: define um valor inteiro de 64 bits. Suporta valores de -9.223.372.036.854.775.808 a 9.223.372.036.854.775.807;
• Boolean: é um dos tipos de dados mais simples, pois ele pode armazenar apenas dois valores: true (verdadeiro) ou false (falso). É empregado para realizar testes lógicos, dentro de estruturas de decisão e repetição, por exemplo.
• Float: trata-se de um tipo de dado de ponto flutuante de 32 bits. Isso quer dizer que ele permite armazenar valores decimais entre 6 e 7 casas. Porém, o armazenamento em memória pode não representar necessariamente o valor repassado. Por exemplo, um float definido com “0,001” pode acabar sendo armazenado como “0,00100001”, e isso não somente no Java, já que isso é decorrência dos pontos flutuantes em geral. Por isso, o tipo float não deve ser usado para operações que precisam passar por várias operações aritméticas, como no caso de sistemas financeiros. Isso porque, a primeira vista, essa imprecisão pode não impactar, mas na sequência de operações realizadas, estas imprecisões podem começar a aparecer nos resultados;
• Double: representa um ponto flutuante de 64 bits. Por isso, ele permite armazenar números de até cerca de 15 casas decimais. A observação para o tipo float também é válida para o tipo double: por ser um tipo de dado que é armazenado por aproximação de valores, ele não deve ser utilizado em operações que demandam precisão absoluta.

Depois de vermos os tipos de dados, podemos criar nossas variáveis. Elas têm algumas regras, como por exemplo, devem ser claras e auto-explicativas.

String nomeDoAluno = “”;
int idade = 0;

Também temos as constantes, que sob um ponto de vista prático são como variáveis, só que com o ponto de que seu valor não pode ser modificado. Isso quer dizer que, se você declara uma constante chamada MAIORIDADE_PENAL com o valor 18, esse valor não poderá ser jamais modificado durante a execução da aplicação.

O Java não possui formalmente o conceito de constantes, assim como acontece em outras linguagens como C# e C++. Embora a palavra-chave “const” seja uma palavra reservada na especificação do Java (ou seja, você não pode nomear nenhuma estrutura como “const”), ela não tem nenhum significado para o compilador.

No artigo sobre variáveis e constantes no Java, podemos nos aprofundar no uso de constantes e ver mais exemplos e explicações sobre o uso de variáveis.

Estruturas condicionais e estruturas de repetição

As estruturas condicionais e de repetição são fundamentais e vem para nos ajudar no reaproveitamento de código.

As estruturas condicionais possibilitam ao programa tomar decisões e alterar o seu fluxo de execução. Isso possibilita ao desenvolvedor o poder de controlar quais são as tarefas e trechos de código executados de acordo com diferentes situações, como os valores de variáveis. Algumas estruturas condicionais no Java são o if…else e o switch/case.

Abaixo temos um exemplo simples da utilização do if…else. Nele verificamos se a pessoa é maior ou menor de idade, dependendo do valor que contiver na variável idade.

package br.com.treinaweb;

public class Exemplo {

    public static void main(String[] args) {
        int idade = 10;
        if (idade >= 18) { 
            System.out.println(“Maior de idade!”);
        } else {
            System.out.println(“Menor de idade!”);
        }
    }

}

As estruturas de repetição, também conhecidas como loops (laços), são utilizadas para executar repetidamente uma instrução ou bloco de instrução enquanto determinada condição estiver sendo satisfeita. As principais estruturas de repetição são o for e o while.

Veremos um exemplo abaixo utilizando o for. Aqui teremos uma variável começando em zero, onde, a cada rodada, ela irá somar +1. Isso irá se repetir até que a variável chegue até 10, encerrando assim esse bloco.

package br.com.treinaweb;

public class Exemplo {

    public static void main(String[] args) {
        for (int i = 0; i <= 10; i++) {
            System.out.println(“A variável i agora vale “ + i);
        }
    }
}

Confira a execução desse código acima e outros exemplos com muitos mais detalhes em nosso artigo sobre estruturas de decisão e repetição.

Orientação a objetos

Como vimos na introdução deste artigo, o Java é orientado a objetos. O paradigma orientado a objetos traz boas vantagens, como a reutilização de código, a legibilidade e manutenibilidade do código, a natural modularização e a produção de código mais acessível, já que as estruturas criadas geralmente representam aspectos também existentes no mundo real.

Quando lidamos com o paradigma orientado a objetos, acabamos criando diversas unidades de software através de estruturas chamadas classes. A partir destas classes, podemos criar estruturas chamadas objetos, estruturas estas que interagem entre si. Esse é o motivo pelo qual o paradigma é chamado de orientação a objetos: todas as interações necessárias para que o software funcione ocorrem através de mensagens e comandos trocados entre estes objetos.

Uma classe nada mais é que um “molde” para definição de outras estruturas. Um exemplo seria uma classe chamada Pessoa. Todas as pessoas possuem características em comum, como nome, idade, sexo, etc, por isso elas podem fazer parte de uma mesma classe.

package br.com.treinaweb;

public class Pessoa {

    // Atributos ou Características de uma pessoa
    public String nome;
    public int idade;
    public char sexo;
}

Como o nome diz, na orientação a objetos temos os objetos. Os objetos são estruturas que são criadas a partir das classes. Por exemplo, os objetos criados a partir da classe Pessoa, podem usufruir dos mesmos atributos e métodos definidos pela Classe.

package br.com.treinaweb;

public class Cadastro {

    public static void main(String args[]) {
      Pessoa maria = new Pessoa();
      maria.nome = "Maria";
      maria.idade = 20;
      maria.sexo = ‘F’;
    }
}

Também vimos que ela tem acesso aos métodos, que são comportamentos e ações que os objetos podem ter. Nesse nosso exemplo, poderíamos colocar que toda Pessoa pode falar, andar, comer, etc.

      maria.falar();
      maria.andar();
      maria.comer();

Para ver mais exemplos e conceitos bem mais aprofundados, acesse o artigo “Orientação a objetos em Java”.

Maven: o que é, o que podemos fazer com ele e como encontrar dependências

É um problema comum durante o desenvolvimento de projetos com a linguagem Java termos que baixar arquivos .jar para serem incluídos dentro de nossos projetos e assim podermos utilizar alguma biblioteca que vai nos auxiliar ao longo do desenvolvimento do mesmo.

A boa notícia é que existem diversas ferramentas para nos ajudar a gerenciar as bibliotecas e automatizar certas tarefas dentro de projetos Java. Uma delas é o Maven.

O Maven é uma ferramenta de código aberto mantida pela Apache. Trata-se de uma ferramenta de gestão de dependências e um task runner. Em outras palavras, o Maven automatiza os processos de obtenção de dependências e de compilação de projetos Java. Quando criamos um projeto Maven, este projeto fica atrelado a um arquivo principal: o pom.xml. Neste arquivo POM (Project Object Model), nós descrevemos as dependências de nosso projeto e a maneira como este deve ser compilado.

Para gerenciar as dependências do nosso projeto utilizamos o arquivo pom.xml nele colocamos as informações das bibliotecas que o nosso projeto necessita para funcionar e o Maven se encarrega do download dessas bibliotecas e de adicioná-las no Build Path/Classpath.

O Maven irá buscar por essas dependências em locais chamados de repositórios, existem basicamente dois repositórios, o repositório local que está localizado na pasta .m2/repository e o repositório remoto que está localizado no repositório público do Maven.

Ao adicionar uma nova dependência em nosso projeto, o Maven primeiro realiza a busca em nosso repositório local, caso não encontre irá buscar no repositório remoto e então fazer o download da biblioteca e disponibilizá-la no repositório local, dessa maneira caso você necessite utilizar a mesma biblioteca em outro projeto não será necessário realizar o download novamente.

Quando falamos das dependências do repositório padrão do Maven, elas podem ser buscadas através de interfaces mais amigáveis. Nesse caso, entram em cena as ferramentas de gestão de repositórios. A mais comum é o Nexus Sonatype, que é utilizado inclusive pelo repositório padrão do Maven.

Indicamos a leitura do nosso artigo “Introdução ao Maven, aprenda como criar e gerenciar projetos Java”. Lá você irá aprender mais a fundo como instalar o Maven, como funciona o pom.xml e ainda fazer seu primeiro exemplo. Já no artigo “Como instalar uma dependência com Maven e usar em seu projeto” vemos mais a fundo como funciona o Sonatype.


Guias Python

Guia da linguagem Python

O que é Python

Criado por Guido Van Rossum em meados dos anos 90, o Python é uma linguagem de programação de alto nível, multiplataforma e open source, que ganhou destaque nos últimos anos por possuir uma ótima curva de aprendizagem, ser uma linguagem leve e ter se tornado uma das principais linguagens para o desenvolvimento de IA, Machine Learning e Big Data, áreas em grande crescimento nos últimos anos.

Python é uma linguagem de programação multiparadigma, ou seja, suporta diversos paradigmas de desenvolvimento, como, orientado a objetos, funcional, imperativo, entre outros. Desta forma, com o Python um mesmo programa pode ser feito utilizando diferentes paradigmas ou um único programa poderá ser desenvolvido utilizando mais de um paradigma.

Suas demais características, aplicações, vantagens e sintaxe podem ser vistas em nosso artigo “O que é Python”.

Desenvolvedor Python
Formação: Desenvolvedor Python
Aprenda os principais conceitos do Python (uso de variáveis, estruturas condicionais e estruturas de decisão), como trabalhar com orientação à objetos (métodos, construtores, destrutores, classes, herança, polimorfismo e duck-typing), estruturas de dados (Listas, Filas, Pilhas, Árvores Binárias, Dicionários, Conjuntos, Tabelas de Espalhamento e Mapas), banco de dados relacionais (DB API e SQLAlchemy) e como criar aplicações desktop com o Kivy.
CONHEÇA A FORMAÇÃO

Afinal, por que devo aprender Python?

O Python foi apontado pelo Stack Overflow como uma das principais linguagens de programação para o ano de 2019, principalmente por possuir uma grande demanda de trabalho, o que acarretou em diversas vagas de emprego em todo o mundo, e o manteve como uma ótima alternativa para o ano de 2020 e para este novo ano de 2021.

Dentre suas principais aplicações podemos citar Data Science, Machine Learning, Big Data, Desenvolvimento Web (Django e Flask) e muitas outras.

Tudo isso, sem dúvidas, a torna uma excelente escolha de linguagem como podemos ver em nosso artigo “Abordando os motivos para aprender Python“.

Como Instalar o Python e executar seu primeiro exemplo

Agora que já sabemos o que é o Python e suas principais características, estamos prontos para realizar a instalação e executar o nosso primeiro exemplo com a linguagem como podemos acompanhar em nosso artigo “Instalação do Python e nosso primeiro Olá Mundo

Principais IDEs para desenvolvimento

Para facilitar o desenvolvimento de programas com o Python, a melhor maneira é utilizando IDEs. São elas que proporcionam mecanismos que facilitam toda a escrita, testes e execução do nosso código.

IDE ou Integrated Development Environment (Ambiente de Desenvolvimento Integrado) é um software que auxilia no desenvolvimento de aplicações, muito utilizado por desenvolvedores, com o objetivo de facilitar diversos processos (ligados ao desenvolvimento), que combinam ferramentas comuns em uma única interface gráfica do usuário (GUI). Desta forma, como principais IDEs para desenvolvimento Python podemos citar:

Eclipse

O Eclipse é uma excelente IDE, muito utilizada no mercado. Seu uso facilita a criação de aplicações Python tanto para Desktop ou Web. O download do Eclipse poderá ser realizado em seu próprio site.

PyCharm

PyCharm conta com desenvolvimento multitecnologias, onde, além do Python, oferece suporte para CoffeeScript, TypeScript, Cython, JavaScript, SQL, HTML/CSS, linguagens de modelo, AngularJS, Node.js e muitas outras.

O download do PyCharm é feito em seu próprio site, onde é possível acompanhar todas as suas novidades, recursos, suporte e muito mais.

Jupyter Notebook

Criada em 2014, derivado do IPython, o Jupyter Notebook é baseada na estrutura servidor-cliente, que permite a manipulação de documentos. O Jupyter Notebook independe de linguagem e suporta diversos ambientes de execução, entre elas: Julia, R, Haskell, Ruby, e o próprio Python.

Para instalar o Jupyter Notebook basta acessar o seu site, onde você também encontrará toda a sua documentação, blog, novidades e muito mais.

Spyder

O Spyder é outra opção para desenvolvedores Python, muito utilizado principalmente por cientistas de dados, já que possui integração com as principais bibliotecas como NumPy, SciPy, Matplotlib e IPython.

O download do Spyder poderá ser feito em seu site, onde também é possível verificar seus plugins, componentes e muito mais.

Em nosso blog possuímos o artigo “Principais IDEs para desenvolvimento em Python” onde citamos as características de cada ferramenta, que poderá te auxiliar na escolha.

Conhecendo variáveis e constantes

O Python é uma das principais linguagens que possui tipagem dinâmica. A tipagem dinâmica é a característica que muitas linguagens de programação possuem por não exigirem que os tipos de dados sejam declarados, pois são capazes de realizar esta escolha dinamicamente. Desta forma, durante a execução do programa ou até mesmo durante a sua compilação, o tipo de uma variável poderá ser alterado.

Além disso, o Python possui como característica a tipagem forte, ou seja, a linguagem pode realizar conversões automaticamente entre todos os tipos suportados.

No Python os tipos suportados são:

  • Inteiro: Representado por toda e qualquer informação numérica que pertença ao conjunto de números inteiros relativos (números positivos, negativos ou o zero).
  • Float: Representado por números decimais, ou seja, números que possuem partes fracionadas.
  • String: É uma cadeia de caracteres que representam textos.
  • Booleano: Variáveis booleanas armazenam valores lógicos que podem ser verdadeiro ou falso.
  • Tipo complexo: armazenam dados com formato misto, ou seja, dados de diferentes tipos em uma mesma sentença.

Além disso, o Python possui características marcantes de regras de tipos e nomeação de variáveis, como podemos ver no artigo “Conhecendo variáveis e constantes no Python”.

Estruturas condicionais e estruturas de repetição em Python

Estruturas de condição são artifícios das linguagens de programação para determinar qual bloco de código será executado a partir de uma determinada condição. No Python, assim como em outras linguagens, podemos trabalhar com as seguintes estruturas de condição:

  • if: Visa verificar se determinada ação é verdadeira e executar o bloco de código contido em seu escopo;
  • if/else: Fará com que uma das ações sejam executadas, já que se a condição dentro do if não for verdadeira, será executado o código contido no else;
  • if/elif/else: Serve para verificar mais de uma condição no bloco de execução de um programa;
  • while: É uma estrutura de repetição que permite executar um determinado bloco de código enquanto uma condição for verdadeira;
  • for…in: O for executará um determinado bloco de código por um número definido de vezes. Esta estrutura é muito útil quando já sabemos a quantidade de vezes que precisamos executar determinado bloco de código.
  • for else e while else: O Python também permite adicionar o comando else depois de uma estrutura de repetição, seja ela um for ou um while. Este else serve para executar um determinado bloco de código imediatamente após a estrutura de repetição finalizar.

Aqui em nosso blog possuímos o artigo “Estruturas condicionais e de repetição no Python” onde exemplificamos cada uma das estruturas condicionais e de repetição para sua melhor visualização.

Orientação a objetos em Python

O paradigma de programação orientado a objetos é um dos principais paradigmas das linguagens de programação. Muito utilizado no mercado, entender como funciona e como implementar este paradigma é essencial para todo desenvolvedor de software.

No Python, este paradigma é amplamente utilizado e no artigo “Orientação a Objetos no Python” veremos como implementá-lo na linguagem.

Desenvolvedor Django Full-Stack
Formação: Desenvolvedor Django Full-Stack
Aprenda nesta formação como desenvolver aplicações complexas utilizando o Django, principal framework para desenvolvimento web de todo o ecossistema Python. Para isso, você verá desde os conceitos mais básicos, até conceitos mais avançados.
CONHEÇA A FORMAÇÃO

Principais Estruturas de Dados no Python

No Python, podemos utilizar diversos tipos de estruturas de dados. Estas estruturas resolvem um tipo de problema e podem ser úteis em diversas situações. As principais estruturas são as Listas, Sets, Dicionários e Tuplas.

  • Listas: Uma lista é a estrutura de dados mais básica do Python e armazena os dados em sequência, onde cada elemento possui sua posição na lista, denominada de índice;
  • Tupla: É uma estrutura bastante similar a uma lista, com apenas uma diferença: os elementos inseridos em uma tupla não podem ser alterados, diferente de uma lista onde podem ser alterados livremente;
  • Sets: São uma coleção de itens desordenada, parcialmente imutável e que não podem conter elementos duplicados. Por ser parcialmente imutável, os sets possuem permissão de adição e remoção de elementos;
  • Dicionários: São coleções de itens desordenados com uma diferença bem grande quando comparados às outras coleções (lists, sets, tuples, etc): um elemento dentro de um dicionário possui uma chave atrelada a ele, uma espécie de identificador.

Em nosso artigo “Principais estruturas de dados no Python” abordamos todos os conceitos listados acima e disponibilizamos exemplos para o melhor entendimento. Vale a pena a leitura.

Como instalar uma dependência com PIP e usar em seu projeto

O PIP é uma ferramenta para gerenciamento de pacotes de software escrito em Python. Todos os pacotes disponíveis para sua instalação podem ser encontrados no site do PIP.

Todo o processo de instalação de pacotes em projetos Python pode ser visto no artigo “Como instalar um pacote com PIP e utilizá-lo em seu projeto”.

Gerenciando pacotes em projetos Python com o PIP

Agora que já sabemos como instalar uma dependência com o PIP, precisamos entender como gerenciar várias dependências instaladas em nossa máquina.

Todo esse processo é visto em nosso artigo “Gerenciando pacotes em projetos Python com o PIP”.

Criando ambientes virtuais para projetos Python com o Virtualenv

É comum desenvolvermos mais de um projeto Python em nossa máquina e cada um desses projetos utilizem diferentes versões de um mesmo pacote.

Para manter as diferentes versões de um mesmo pacote instalados sem qualquer conflito em nossa máquina, precisamos criar ambientes virtuais para cada projeto e, assim, isolar os pacotes e suas versões. Para isso, utilizamos ambientes virtuais e todo processo de criação e gerenciamento podem ser vistos no artigo “Criando ambientes virtuais para projetos Python com o virtualenv”.


Java

Orientação a objetos em Java

O Java atualmente pode ser considerada uma linguagem multiparadigma, mas é perceptível a sua vocação para estruturas orientadas a objetos. O paradigma orientado a objetos traz boas vantagens, como a reutilização de código, a legibilidade e manutenibilidade do código, a natural modularização e a produção de código mais acessível, já que as estruturas criadas geralmente representam aspectos também existentes no mundo real.

Quando lidamos com o paradigma orientado a objetos, acabamos criando diversas unidades de software através de estruturas chamadas classes. A partir destas classes, podemos criar estruturas chamadas objetos, estruturas estas que interagem entre si. Esse é o motivo pelo qual o paradigma é chamado de orientação a objetos: todas as interações necessárias para que o software funcione ocorrem através de mensagens e comandos trocados entre estes objetos.

Caso você esteja iniciando e não conheça muita coisa sobre orientação a objetos, indico primeiramente a leitura do artigo “Os pilares da orientação a objetos”.

Classe

Quando falamos sobre orientação a objetos, as classes são uma das estruturas essenciais.

Uma classe funciona como um “molde” para definição de outras estruturas. Classes geralmente são compostas pelo agrupamento de atributos ou características, e métodos ou ações. Uma classe define agrupamentos de atributos e métodos que são correlacionados e podem ser reaproveitados.

Por exemplo, imagine que você precisa criar uma aplicação para fazer a gestão de uma frota de veículos. Nessa aplicação, com certeza será necessário manipular informações de carros. E todos os carros geralmente possuem um “molde” padrão com características e ações que são comuns a todos os carros.

Todos os carros, por exemplo, possuem características como:

• Modelo;
• Marca;
• Fabricante;
• Chassi.

E possuem ações em comum, como:

• Ligar;
• Acelerar;
• Frear;
• Desligar.

Dessa maneira, poderíamos criar uma estrutura de código que representasse esse formato padrão para todos os veículos, agrupando justamente estas características e ações. E é exatamente essa estrutura que é uma classe. Nesse caso, poderíamos ter uma classe Carro, que define as características e ações que são comuns para todos os carros.

Declarando classes

No Java, podemos declarar uma classe da seguinte maneira:

package br.com.treinaweb;

public class Carro {

    // Atributos ou Características de um carro
    public String modelo;
    public String marca;
    public String chassi;
    public String fabricante;

    // Métodos ou ações de um carro
    public void ligar() {
        System.out.println("Carro ligado!");
    }

    public void desligar() {
        System.out.println("Carro desligado!");
    }

    public void acelerar() {
        System.out.println("Carro acelerando...");
    }

    public void frear() {
        System.out.println("Carro freando...");
    }

}

Neste momento, caso você tenha visto possíveis palavras desconhecidas ou que você não entenda o funcionamento, como public, por exemplo, seu funcionamento é abordado no artigo “Os pilares da orientação a objetos” que citamos no começo deste artigo.

Como no exemplo acima, classes geralmente possuem duas sessões distintas: uma primeira parte, onde são declaradas as características (que tecnicamente são chamadas de atributos); e uma segunda parte onde são declaradas as ações previstas por aquela classe (que tecnicamente são chamadas de métodos).

Uma classe não é obrigada a ter as duas sessões: podemos ter classes que possuem somente atributos, podemos ter classes que possuem somente métodos e podemos ter classes que possuem ambas as sessões. Tudo vai depender da necessidade.

Objetos

Classes existem para definirmos os nossos moldes, ou seja, para definirmos o formato de estruturas que nosso código irá manipular. Mas, elas servem somente para ser moldes. Se quisermos as utilizar, precisamos colocar algo e criar uma estrutura que seja semelhante a esse molde. E é nesse momento que entram em cena os objetos.

Os objetos são estruturas que são criadas a partir das classes. Um objeto, quando criado a partir de uma determinada classe, assume que irá possuir os mesmos atributos e os mesmos métodos definidos pela classe. Uma classe pode dar origem a vários objetos distintos entre si que compartilham o mesmo molde.

Declarando objetos

No Java, objetos podem ser declarados da seguinte forma:

package br.com.treinaweb;

public class Aplicacao {

    public static void main(String args[]) {
      Carro corsa = new Carro();
      corsa.modelo = "Corsa";
      corsa.marca = "Chevrolet";
      corsa.chassi = "ABC123";
      corsa.fabricante = "Chevrolet";
      corsa.ligar();
      corsa.acelerar();
      corsa.frear();
      corsa.desligar();
      System.out.println("Nome do carro: " + corsa.modelo);

      Carro gol = new Carro();
      gol.modelo = "Gol";
      gol.marca = "Volkswagen";
      gol.chassi = "DEF456";
      gol.fabricante = "Volkswagen";
      gol.ligar();
      gol.acelerar();
      gol.frear();
      gol.desligar();
      System.out.println("Nome do carro: " + gol.modelo);
    }
}

Neste exemplo, usamos a nossa classe Carro para criar dois objetos a partir dela: os objetos gol e corsa.

No exemplo acima, ainda podemos afirmar que temos duas variáveis do tipo Carro: gol e corsa; embora, em termos técnicos, o correto é dizer que temos duas instâncias da classe Carro: gol e corsa, objetos também são chamados de instâncias de uma classe. Porém, em termos práticos, não há problema em dizer que temos uma variável do tipo de uma classe.

Objetos são importantes por algumas razões, logo aqui, podemos ver algumas características importantes:

• Objetos definem instâncias de classes, o que nos permite usufruir do molde de atributos e métodos definidos por uma classe;
• Objetos guardam estado de maneira segregada: veja que o objeto corsa tem suas características definidas de maneira própria, como modelo, marca, chassi e fabricante. O objeto gol também tem exatamente as mesmas características, mas definidas de acordo com o objeto gol. Embora ambos os objetos venham da mesma classe, eles têm seus próprios valores de atributos, valores estes atrelados a cada instância (no caso, as instâncias gol e corsa).

Métodos

Partindo do mesmo código acima, temos os métodos. Os métodos são comportamentos e ações que os objetos podem ter.

No código acima, os objetos gol e corsa, por virem do “molde” Carro, adquirem os atributos e métodos definidos pela classe Carro e, por isso, passam a ter as características com modelo, marca e chassi; e ganham as ações definidas pela classe Carro, como ligar, desligar e acelerar.

    public void ligar() {
        System.out.println("Carro ligado!");
    }

Quando temos objetos, seus atributos (ou características) e métodos (ou ações) são acessados através do “.”. Por exemplo: se eu tenho um Carro chamado corsa e quero acessar seu atributo modelo, devo escrever um código como corsa.modelo. A mesma coisa vale para métodos: se eu quiser acessar o método ligar do objeto corsa, devo escrever o código corsa.ligar().

Se você quer conhecer mais sobre essa linguagem, não se esqueça de acessar nosso Guia da linguagem Java. Te esperamos lá 🙂


Python

Orientação a objetos em Python

O paradigma de programação orientado à objetos é um dos principais paradigmas das linguagens de programação. Muito utilizado no mercado, entender como funciona e como implementar este paradigma é essencial para todo desenvolvedor de software.

Python - Orientação a objetos
Curso de Python - Orientação a objetos
CONHEÇA O CURSO

No Python, o paradigma orientado à objetos funciona de forma similar às outras linguagens, porém com algumas mudanças em sua implementação, como veremos ao longo deste artigo.

Declarando classes

No paradigma orientado à objetos, uma classe é a representação de algo do mundo real. No Python, o uso de classes é algo constante no desenvolvimento de programas.

Sendo assim, para declarar uma classe no Python é bem simples, como podemos ver abaixo:

class Pessoa():
        # Atributos e métodos da classe

Como vimos acima, para declarar uma classe no Python, utilizamos a palavra reservada class seguido do nome desta classe.

No Python, todas as classes devem, por boas práticas, possuir nomes que comecem com letra maiúscula e, caso sejam compostos, a primeira letra de cada palavra deve ser maiúscula, o que chamamos de formato CamelCase:

class PessoaFisica():
    # Atributos e métodos da classe

Criando construtor da classe

Uma classe é representada por atributos e métodos. Os atributos de uma classe representam as características que esta classe possui, já os métodos representam o comportamento da classe.

Para declarar um atributo em uma classe no Python é bem simples, basta definir o nome do atributo no método especial chamado __init__, este método define o construtor da classe, ou seja, é onde definimos como uma nova pessoa será criada em nosso programa.

Para definir os atributos de uma classe em seu construtor, basta passá-los como parâmetro, como podemos ver abaixo:

class Pessoa:
    def __init__(self, nome, sexo, cpf):
        self.nome = nome
        self.sexo = sexo
        self.cpf = cpf

Agora, estamos indicando que toda pessoa que for criada em nosso programa e que utilize como base a classe Pessoa deverá possuir um nome, sexo e cpf.

Instanciando objetos

Como vimos anteriormente, as classes representam a estrutura de um elemento no mundo real, porém ela é apenas o modelo destes elementos.

Sempre que precisamos criar “algo” com base em uma classe, dizemos que estamos “instanciando objetos”. O ato de instanciar um objeto significa que estamos criando a representação de uma classe em nosso programa.

Para instanciar um objeto no Python com base em uma classe previamente declarada, basta indicar a classe que desejamos utilizar como base e, caso possua, informar os valores referentes aos seus atributos, como podemos ver abaixo:

class Pessoa:
    def __init__(self, nome, sexo, cpf):
        self.nome = nome
        self.sexo = sexo
        self.cpf = cpf

if __name__ == "__main__":
    pessoa1 = Pessoa("João", "M", "123456")
    print(pessoa1.nome)

Ao executar a linha pessoa1 = Pessoa("João", "M", "123456")estamos criando um objeto do tipo pessoa com nome “João”, sexo “M” e cpf “123456”.

Com isso, agora possuímos uma forma de criar diversas pessoas utilizando a mesma base, a classe Pessoa.

Ao executar o código acima e imprimir o nome dessa pessoa print(pessoa1.nome), teremos o seguinte retorno:

Instanciando objetos em Python

Declarando métodos

Como vimos anteriormente, uma classe possui atributos (que definem suas características) e métodos (que definem seus comportamentos).

Imagine que possuímos um atributo ativo na classe Pessoa. Toda pessoa criada em nosso sistema é inicializado como ativo, porém, imagine que queremos alterar o valor deste atributo e, assim, “desativar” a pessoa em nosso sistema e, além disso, exibir uma mensagem de que a pessoa foi “desativada com sucesso”.

Para isso, precisamos definir um comportamento para essa pessoa, assim, agora, ela poderá ser “desativada”.

Sendo assim, precisamos definir um método chamado “desativar” para criar este comportamento na classe Pessoa, como podemos ver abaixo:

class Pessoa:
    def __init__(self, nome, sexo, cpf, ativo):
        self.nome = nome
        self.sexo = sexo
        self.cpf = cpf
        self.ativo = ativo

    def desativar(self):
        self.ativo = False
        print("A pessoa foi desativada com sucesso")

if __name__ == "__main__":
    pessoa1 = Pessoa("João", "M", "123456", True)
    pessoa1.desativar()

Para criarmos este “comportamento” na classe Pessoa, utilizamos a palavra reservada def, que indica que estamos criando um método da classe, além do nome do método e seus atributos, caso possuam.

Depois disso, é só definir o comportamento que este método irá realizar. Neste caso, o método vai alterar o valor do atributo “ativo” para “False” e imprimir a mensagem “A pessoa foi desativada com sucesso”, como podemos ver abaixo:

Declarando métodos em Python

Declarando propriedades

Aparentemente o código acima funciona normalmente. Porém, temos um pequeno problema com o atributo “ativo”: ele é acessível para todo mundo. Ou seja, mesmo possuindo o método “desativar”, é possível alterar o valor do atributo “ativo” sem qualquer problema:

Declarando propriedades em Python

Este comportamento do nosso programa dá brechas para que um usuário possa ser ativado ou desativado sem passar pelo método responsável por isso. Por isso, para corrigir este problema, devemos recorrer a um pilar importantíssimo da Orientação à Objetos: o encapsulamento.

Basicamente, o encapsulamento visa definir o que pode ou não ser acessado de forma externa da classe.

Existem três tipos de atributos de visibilidade nas linguagens orientadas a objetos, que são:

  • Public: Atributos e métodos definidos como públicos poderão ser invocados, acessados e modificados através de qualquer lugar do projeto;
  • Private: Atributos e métodos definidos como privados só poderão ser invocados, acessados e modificados somente por seu próprio objeto.
  • Protected: Atributos e métodos definidos como protegidos só poderão ser invocados, acessados e modificados por classes que herdam de outras classes através do conceito de Herança, visto na última aula. Sendo assim, apenas classes “filhas” poderão acessar métodos e atributos protegidos.

No Python, diferente das linguagens completamente voltadas ao paradigma da orientação à objetos (Java, C#, etc.), estes atributos existem, mas não da forma “convencional”.

Python - Fundamentos
Curso de Python - Fundamentos
CONHEÇA O CURSO

Para definir um atributo público, não há necessidade de realizar nenhuma alteração, por padrão, todos os atributos e métodos criados no Python são definidos com este nível de visibilidade.

Já se precisarmos definir um atributo como privado, adicionamos dois underlines (__) antes do nome do atributo ou método:

class Pessoa:
    def __init__(self, nome, sexo, cpf, ativo):
        self.nome = nome
        self.sexo = sexo
        self.cpf = cpf
        self.__ativo = ativo

    def desativar(self):
        self.__ativo = False
        print("A pessoa foi desativada com sucesso")

if __name__ == "__main__":
    pessoa1 = Pessoa("João", "M", "123456", True)
    pessoa1.desativar()
    pessoa1.ativo = True
    print(pessoa1.ativo)

Porém, isso é apenas uma “convenção” do Python, ou seja, mesmo definindo o atributo com visibilidade privada (utilizando dois underlines antes de seu nome), ele poderá ser acessado de fora da classe:

Declarando propriedades em Python

Isso ocorre porquê estamos falando de “convenções”, ou seja, padrões que devem ser seguidos por desenvolvedores Python.

Porém, caso precisemos acessar os atributos privados de uma classe, o Python oferece um mecanismo para construção de propriedades em uma classe e, dessa forma, melhorar a forma de encapsulamento dos atributos presentes. É comum que, quando queremos obter ou alterar os valores de um atributo, criamos métodos getters e setters para este atributo:

class Pessoa:
    def __init__(self, nome, sexo, cpf, ativo):
        self.__nome = nome
        self.__sexo = sexo
        self.__cpf = cpf
        self.__ativo = ativo

    def desativar(self):
        self.__ativo = False
        print("A pessoa foi desativada com sucesso")

    def get_nome(self):
        return self.__nome

    def set_nome(self, nome):
        self.__nome = nome

if __name__ == "__main__":
    pessoa1 = Pessoa("João", "M", "123456", True)
    pessoa1.desativar()
    pessoa1.ativo = True
    print(pessoa1.ativo)

    # Utilizando geters e setters
    pessoa1.set_nome("José")
    print(pessoa1.get_nome())

Porém, ao tentar acessar o valor do atributo nome presente na classe, fica evidente que estamos obtendo esse dado através de um método. Pensando nisso, o time de desenvolvimento criou as Properties para prover um meio mais “elegante” para obter e enviar novos dados aos atributos de uma classe:

class Pessoa:
    def __init__(self, nome, sexo, cpf, ativo):
        self.__nome = nome
        self.__sexo = sexo
        self.__cpf = cpf
        self.__ativo = ativo

    def desativar(self):
        self.__ativo = False
        print("A pessoa foi desativada com sucesso")

        def get_nome(self):
        return self.__nome

    def set_nome(self, nome):
        self.__nome = nome

    @property
    def nome(self):
        return self.__nome

    @nome.setter
    def nome(self, nome):
        self.__nome = nome

if __name__ == "__main__":
    pessoa1 = Pessoa("João", "M", "123456", True)
    pessoa1.desativar()
    pessoa1.ativo = True
    print(pessoa1.ativo)

        # Utilizando geters e setters
    pessoa1.set_nome("José")
    print(pessoa1.get_nome())

    # Utilizando properties
    pessoa1.nome = "José"
    print(pessoa1.nome)
Python - Estrutura de dados - Parte 1
Curso de Python - Estrutura de dados - Parte 1
CONHEÇA O CURSO

Com isso, podemos ver o quão mais “limpo” é o uso das properties para acessar ou alterar o valor de uma propriedade privada das classes no Python.

Se você quer conhecer mais sobre Python, acesse nosso guia da linguagem.


Desenvolvimento Orientação a Objetos

Classes Abstratas vs Interfaces

No paradigma da orientação a objetos, há dois termos que são frequentemente confundidos: as classes abstratas e as interfaces. Apesar de serem utilizadas para propósitos diferentes, possuem aspectos similares, como veremos durante este artigo.

Classes Abstratas

É um tipo de classe especial que não pode ser instanciada, apenas herdada. Sendo assim, uma classe abstrata não pode ter um objeto criado a partir de sua instanciação. Essas classes são muito importantes quando não queremos criar um objeto a partir de uma classe “geral”, apenas de suas “subclasses”.

Imagine que possuímos três classes (Conta, Conta Corrente e Conta Poupança), sendo a classe Conta uma classe “geral” (comumente chamada de classe “pai”). Ao ir em um banco, nós não criamos uma nova Conta, mas sim uma Conta Corrente ou uma Conta Poupança.

Sendo assim, não faz sentido que a classe Conta possa ser instanciada, já que é um erro na regra de negócio caso isso ocorra. É aí que entra o termo “abstrato” desse tipo de classe, por não haver a necessidade de criar objetos com base em uma classe “pai”, não há porquê ela permitir a instanciação de novos objetos.

Agora, ao invés de criarmos um objeto do tipo Conta, só será permitido a criação de objetos do tipo Conta Corrente ou Conta Poupança, o que faz mais sentido.

Por não permitir a instanciação de novos objetos com base em uma classe abstrata, este tipo de classe é utilizada para implementar o conceito de Herança da OO. Ou seja, as classes abstratas irão armazenar atributos e métodos comuns às classes que a irão herdar, permitindo um maior reaproveitamento de código.

Além disso, as classes abstratas permitem que criemos métodos abstratos (mas nem todos). Basicamente, um método abstrato é um método especial que não possui implementação, apenas assinatura (tipo de retorno, nome e parâmetros), e obriga as classes que “herdarem” da classe abstrata à implementar estes métodos, algo muito parecido com o assunto do nosso próximo tópico, as interfaces.

Python - Orientação a objetos
Curso de Python - Orientação a objetos
CONHEÇA O CURSO

Interfaces

Considerada uma Entidade, as interfaces têm papel fundamental no desenvolvimento de software orientado à objetos. Por várias vezes, nós precisamos, de alguma forma, especificar um conjunto de métodos que um grupo de classes deverá, obrigatoriamente, implementar. Para atingir este efeito, utilizamos as interfaces.

Basicamente, uma interface define um “contrato” a ser seguido por outras classes. Este contrato é composto por cláusulas, que descrevem determinados comportamentos que este grupo de classes deverá seguir.

Imagine que você irá alugar um novo imóvel, para isso, você deve assinar o contrato de locação, que impõe regras que deverão ser seguidas, caso você queira assiná-lo. Estas regras são os métodos que as classes deverão implementar para que o contrato seja válido.

Por “obrigar” que as classes que implementem uma interface, também implementem seus métodos, as interfaces apenas definem as assinaturas dos métodos (tipo de retorno, nome e parâmetros). A partir daí, é obrigação da classe que quer implementar esta interface, utilize esta assinatura e implemente seus métodos. Estes métodos, assim como nas classes abstratas, devem ser abstratos.

Além disso, por não ser considerada uma classe, as interfaces não possuem construtores e, por isso, não podem ser instanciadas, como as classes abstratas.

Classes Abstratas vs Interfaces

Vimos então que as classes abstratas e interfaces possuem algumas características semelhantes (não podem ser instanciadas, possuem métodos abstratos que obrigam as outras classes a implementá-los), porém elas não servem para o mesmo propósito.

Quando utilizamos as interfaces, estamos definindo um conjunto de assinatura de métodos que outras classes devem implementar. Com isso, apenas definimos o comportamento base de um conjunto de classes que, por ventura, implementem esta interface.

Já as classes abstratas servem para prover uma base para que as classes que “herdem” desta não precisem se preocupar com o comportamento padrão, apenas com suas características e comportamentos pessoais.

Sendo assim, sempre que precisarmos definir um conjunto de métodos que devem ser implementados por um grupo de classes, utilizamos as interfaces. Se precisarmos determinar uma classe base para outras classes, que herdarão seus atributos e métodos e esta classe não deva ser instanciada, utilizamos as classes abstratas.


Dart Flutter

O que são mixins e qual sua importância no Dart

Quando estamos desenvolvendo aplicações, é comum o uso de herança para evitar a reescrita de código. Basicamente, a herança é uma forma de uma classe obter atributos e métodos de outra classe, evitando a duplicidade e melhorando o reaproveitamento de código.

Porém, a herança por si só, não resolve todos os problemas. Imagine a seguinte situação:

Possuímos em nosso software, uma classe Pessoa , com seus atributos e métodos e três classes que herdam de Pessoa, as classes Funcionario, Cliente e Fornecedor. Estas três classes são filhas de Pessoa e, por isso, herdam seus atributos e métodos:

Diagrama exemplificando uma herança

Porém, imagine que queremos adicionar um método que é comum a diversas classes que herdam de Pessoa, mas nem todas. A primeira opção seria desenvolver estes métodos apenas nas classes que iriam utilizá-las, porém, desta forma, há a duplicidade de código, já que mais de uma classe irá utilizar o mesmo método.

A segunda opção seria utilizar um recurso presente no Dart chamado de Mixins, como veremos abaixo.

O que são Mixins e como eles funcionam?

Basicamente, os Mixins são recursos presentes no Dart que nos permitem adicionar um conjunto de “características” a uma classe sem a necessidade de utilizar uma herança.

Voltando ao exemplo anterior, imagine as classes abaixo:

abstract class Pessoa {
  String _nome;
  int _idade;
  String _sexo;
  String _email;

  Pessoa(String nome, int idade, String sexo, String email) {
    this._nome = nome;
    this._idade = idade;
    this._sexo = sexo;
    this._email = email;
  }

// getters e setters
// métodos
}
class Funcionario extends Pessoa {
  String _cargo;

  Funcionario(String nome, int idade, String sexo, String email, String cargo)
      : this._cargo = cargo,
        super(nome, idade, sexo, email);

// getters e setters
// métodos
}
class Cliente extends Pessoa {
  bool _ativo;

  Cliente(String nome, int idade, String sexo, String email, bool ativo)
      : this._ativo = ativo,
        super(nome, idade, sexo, email);

// getters e setters
// métodos
}
class Fornecedor extends Pessoa {
  String _empresa;

  Fornecedor(String nome, int idade, String sexo, String email, String empresa)
      : this._empresa = empresa,
        super(nome, idade, sexo, email);

// getters e setters
// métodos
}
Dart - Fundamentos
Curso de Dart - Fundamentos
CONHEÇA O CURSO

Agora precisamos implementar o método abastecer() que servirá para um funcionário ou fornecedor abastecer a prateleira de um supermercado. Onde seria o melhor local para isso? Se implementarmos na classe Pessoa, o Cliente também conseguirá acesso a este método, o que não é correto para o que queremos. E é aí que entram os Mixins \o

Com o uso dos Mixins, podemos desenvolver o método abastecer() de forma “isolada” e apenas permitir que determinadas classes possuam acesso a ele. Para isso, criamos o Mixin da seguinte forma:

mixin Abastecer {
  void abastecer() {
    print("Prateleira abstecida");
  }
}

Por ser uma classe especial, os mixins não precisam de construtores.

Agora, para definirmos as classes que poderão utilizar o método abastecer(), utilizamos a palavra with, como podemos ver abaixo:

class Funcionario extends Pessoa with Abastecer{
  String _cargo;

  Funcionario(String nome, int idade, String sexo, String email, String cargo)
      : this._cargo = cargo,
        super(nome, idade, sexo, email);

// getters e setters
// métodos
}
class Fornecedor extends Pessoa with Abastecer{
  String _empresa;

  Fornecedor(String nome, int idade, String sexo, String email, String empresa)
      : this._empresa = empresa,
        super(nome, idade, sexo, email);

// getters e setters
// métodos
}

Com isso, as classes Fornecedor e Funcionario poderão utilizar o método abastecer(), sem que a classe Cliente o faça:

main() {
cliente.abastecer(); //The method 'abastecer' isn't defined for the class 'Cliente'.
funcionario.abastecer(); //Prateleira abstecida
fornecedor.abastecer(); //Prateleira abstecida
}

Isso faz com que o reaproveitamento do nosso código seja ainda mais eficiente, já que o método abastecer() só será implementado uma vez, mas poderá ser utilizado quantas vezes forem necessárias. Outras linguagens possuem recursos parecidos com os mixins no Dart, como o PHP e as Traits.


PHP

Estrutura interna de valor, referências e gerenciamento de memória no PHP

A forma com o que o PHP trabalha com referências e gerenciamento de memória não é um assunto muito popular e é compreensível que não seja, se levarmos em conta que na maior parte do tempo como desenvolvedores não precisamos nos preocupar com isso, ademais, o PHP implementa um garbage collector que cuida da contagem das referências e decide o melhor momento de liberar a memória, sem impactar na experiência do desenvolvedor.

Quando falo em “experiência do desenvolvedor”, me refiro ao fato de não precisarmos a todo momento usar unset() para destruir variáveis não mais utilizadas, tampouco passar estruturas mais complexas como objetos ou arrays por referência a todo momento etc. Há raros e específicos casos em que usar referência faz sentido. Como também há raros e específicos casos em que liberar a memória manualmente (sem aguardar o trabalho do garbage collector) faz sentido como, por exemplo, a liberação de um resource para alguma operação subsequente importante que vai consumir um tanto considerável de memória.

A ideia aqui é tentar ao máximo ter uma abordagem leve, de desenvolvedor para desenvolvedor, no sentido de usuários da linguagem. Não tenho a pretenção e nem o conhecimento necessário para explicar o funcionamento interno do PHP e, além do mais, existe gente muito boa e mais preparada pra isso (compartilharei links no final do artigo).

Em suma, esse artigo se propõe a responder as seguintes questões:

  • Como um valor é representado internamente no PHP?
  • Valores em PHP possuem que semântica? De valor ou de referência?
  • Referências? O que são?
  • Objetos são passados por referência?
  • Se tenho um array com milhões de registros e preciso passá-lo para o argumento de uma função, e agora? Vou dobrar o tanto de memória que estou usando?
PHP - Fundamentos
Curso de PHP - Fundamentos
CONHEÇA O CURSO

A estrutura de dado que representa os valores do PHP

No PHP existe uma estrutura de dado elementar chamada zval (zend value), ela é uma estrutura container para qualquer valor arbitrário que se trabalha no PHP:

struct _zval_struct {
    zend_value value;
    union {
        // ...
        uint32_t type_info;
    } u1;
    // ...
};

Para a nossa noção, o mais importante é notarmos que essa estrutura armazena o valor e o tipo desse valor. E, pelo fato de o PHP ser uma linguagem de tipagem dinâmica, o tipo é conhecido apenas em tempo de execução e não em tempo de compilação. Além disso, esse tipo pode mudar durante a execução, por exemplo, um inteiro pode simplesmente virar uma string em um dado momento.

O valor propriamente dito é armazenado na union zend_value (uma union define múltiplos membros de diferentes tipos mas só um deles pode ser ativo por vez):

typedef union _zend_value {
    zend_long         lval;             /* long value */
    double            dval;             /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

Por essa estrutura já podemos inferir os tipos internos das variáveis que declaramos. Objetos são armazenados numa estrutura separada do tipo zend_object, arrays são do tipo zend_array numa implementação de HashTable e assim por diante. Valores simples como inteiros e float são armazenados diretamente na zval, enquanto que valores complexos como strings e objetos são armazenados em estruturas separadas e representados na zval por ponteiros (como pode ser visto em zend_string *str;).

Outra noção refere-se ao fato de que internamente o PHP faz a contagem da quantidade de vezes que um valor é usado por outras variáveis (symbols) e isso é essencial para o gerenciamento da memória por parte do garbage collector, uma vez que esse contador estiver zerado, indica pro GC que a memória desse valor pode ser liberada.

A semântica dos valores

No PHP os valores possuem a semântica de valor, desde que explicitamente não peçamos por uma referência. Ou seja, quando passamos um valor para um argumento de uma função, ou quando atribuímos uma variável a outra, estamos sempre trabalhando em cópias do mesmo valor. Vamos esclarecer esse conceito com exemplos, para que depois possamos adentrar no assunto de referências.

Exemplo:

<?php

$foo = 1;
$bar = $foo;
$foo++;

var_dump($foo, $bar); // int(2), int(1)

$bar aponta para uma zval diferente, apenas com a cópia do valor de $foo. $foo foi incrementada e isso não refletiu em $bar.

Outro exemplo:

<?php

function increment(int $value) : void {
    $value++;
}

$value = 1;

increment($value);

echo $value; // 1

O valor de $value dentro do escopo da função é um e fora dela, é outro. Por esse motivo, o que está sendo incrementado é o zval do escopo da função.

Mas, veja bem, cópias não são feitas arbitrariamente sem necessidade. Para valores comuns como inteiros, números de ponto flutuante etc, elas são imediatamente feitas. No entanto, o mesmo não acontece com valores complexos como strings e arrays, por exemplo. O PHP usa o princípio copy-on-write, onde uma deep-copy (duplicação) dos valores é feita apenas antes desses dados serem alterados.

O melhor exemplo para ilustrar isso:

<?php

// Etapa 1: Consumo atual de memória
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2: Criando um grande array dinâmicamente
$foo = [];
for ($i = 0; $i < 1000000; $i++) {
    $foo[] = random_int(1, 100);
}
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3: Atribuindo o array a duas novas variáveis
$bar = $foo;
$baz = $foo;
echo sprintf("3) Mem: %s \n", memory_get_usage());

// Etapa 4: Fizemos uma alteração de escrita no array $bar
$bar[] = 1;
echo sprintf("4) Mem: %s \n", memory_get_usage());

// Etapa 5: Zerando os arrays previamente criados
$foo = [];
$bar = [];
$baz = [];
echo sprintf("5) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 395280 
2) Mem: 33953920 
3) Mem: 33953920 
4) Mem: 67512528 
5) Mem: 395312 

Veja que nas etapas 2 e 3 o consumo de memória permaneceu o mesmo. O senso comum seria imaginar que ele fosse triplicar, uma vez que atribuímos dois novos arrays com o valor de $foo. No entanto, o consumo de memória duplicou apenas na etapa 4, pois alteramos o array $bar, essa operação de escrita fez com que o PHP duplicasse o array para esse zval.

O mesmo comportamento pode ser visto com strings:

<?php

// Etapa 1
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2
$string = str_repeat('FOO', 100000);
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3
$string2 = $string;
$string3 = $string;
echo sprintf("3) Mem: %s \n", memory_get_usage());

// Etapa 4
$string2 .= 'BAR';
$string3 .= 'BAR';
echo sprintf("4) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 393504 
2) Mem: 696640 
3) Mem: 696640 
4) Mem: 1302848

Apenas na etapa 4 tivemos aumento da memória, pois as strings foram modificadas, forçando deep-copy.

Outro exemplo:

<?php

// Etapa 1
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2
$string = str_repeat('FOO', 100000);
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3
function foo(string $string) {
    $string2 = $string;
}

foo($string);

echo sprintf("3) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 393400 
2) Mem: 696536 
3) Mem: 696536
PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO

Referências no PHP

Referência é um mecanismo que permite que múltiplas variáveis (símbolos) apontem pro mesmo container interno de valor (zval). Referências não são ponteiros como vemos em C ou em Go, por exemplo.

Voltando ao exemplo anterior de $foo e $bar, como podemos fazer para $bar apontar para o mesmo zval que $foo de tal forma que o incremento em $foo reflita em $bar? Aí que entra o operador de referência &amp; na atribuição:

<?php

$foo = 1;
$bar =& $foo;
$foo++;

var_dump($foo, $bar); // int(2), int(2)

Agora, na prática, temos dois símbolos ($foo e $bar) que apontam para o mesmo valor, alterações em um ou em outro refletirão no mesmo container interno de valor. Por esse motivo o incremento de $foo agora refletiu em $bar.

Esse é o exemplo mais elementar para distinguir a semântica de valor da de referência.

Objetos são passados por referência?

Existe a noção entre desenvolvedores e artigos na web de que objetos são passados por referência, mas isso não reflete a realidade. Eles também são passados pela semântica de valor, como qualquer outro valor. Eles apenas possuem um comportamento parecido com “referência” mas que internamente não se trata de referência.

Vejamos esse exemplo:

<?php

class Node
{
    // Todo
}

$node1 = new Node();
$node2 = $node1;

xdebug_debug_zval('node1'); // node1: (refcount=2, is_ref=0)=class Node {  }
xdebug_debug_zval('node2'); // node2: (refcount=2, is_ref=0)=class Node {  }

Nota: A função xdebug_debug_zval() retorna detalhes internos da zval e para ela funcionar, é preciso ter a extensão xdebug instalada.

Na prática, o que temos nesse exemplo, é que as duas variáveis são estruturas (zval) distintas, mas internamente apontam pro mesmo objeto.

O que é diferente de:

<?php

class Node
{
    // Todo
}

$node1 = new Node();
$node2 = &$node1;

xdebug_debug_zval('node1'); // node1: (refcount=2, is_ref=1)=class Node {  }
xdebug_debug_zval('node2'); // node2: (refcount=2, is_ref=1)=class Node {  }

Nesse exemplo, além de ambas as variáveis apontarem pro mesmo objeto, temos explicitamente uma referência, observe que o valor da flag is_ref agora é 1. Isso quer dizer que, $node1 e $node2 apontam para o mesmo zval.

A diferença, a princípio, parece sutil, mas tentemos ver isso na prática. Primeiro um exemplo onde explicitamente pedimos para receber um valor por referência:

<?php

$a = 1;

function change(&$a) {
    $a = 2;
}

change($a);

echo $a; // 2

A alteração dentro do escopo da função refletiu no mesmo container interno de valor (zval) da variável $a do escopo do script. Como era esperado.

Agora a dúvida é: se objetos são sempre passados por referência (como é o senso comum), não precisamos usar o operador de referência para receber um objeto nessa mesma função change(), certo?

Vejamos:

<?php

class Node
{
    // Todo
}

$node = new Node();

function change($node) {
    $node = 10;
}

change($node);

var_dump($node); // class Node#1 (0) { }

Pois bem, se os objetos realmente fossem passados por referência, o valor da variável $node deveria ser 10 e não uma instância da classe Node. Ou seja, quando um objeto é passado para o argumento de uma função/método ou quando é atribuído em outra variável, não temos passagem por referência, o que fica caracterizado é que essas variáveis apenas estão apontando para a mesma estrutura de dado referente ao objeto, o que dá a sensação que está acontecendo passagem por referência.

O que causa essa confusão é esse comportamento:

<?php

class Node
{
    public int $count;
}

$node = new Node();
$node->count = 1;

function change(Node $node) {
    $node->count = 2;
}

change($node);

echo $node->count; // 2

O resultado é 2, mesmo sem passagem por referência, por causa do comportamento de estarmos trabalhando com o mesmo objeto em memória.

O exemplo abaixo demonstra que estamos lidando com duas estruturas (zval) diferentes, mas que ambas referenciam a mesma estrutura de dado do objeto criado:

<?php

class Node
{
    // TODO
}

$node = new Node();

function change(Node $node) {
    $nodeFoo = &$node;

    xdebug_debug_zval('node'); // node: (refcount=2, is_ref=1)=class Node {  }
}

change($node);

xdebug_debug_zval('node'); // node: (refcount=1, is_ref=0)=class Node {  }

Veja que dentro do escopo da função já ficou caracterizado que existe referência, mas fora dela, não. Ou seja, se $nodeFoo dentro de change() fosse alterada para 10, refletiria no mesmo zval de $node. Veja:

<?php

class Node
{
    // TODO
}

$node = new Node();

function change(Node $node) {
    $nodeFoo = &$node;

    $nodeFoo = 10; // Reflete no mesmo zval de $node

    var_dump($node); // int(10)
}

change($node);

var_dump($node); // class Node#1 (0) { }

Agora, a história muda quando explicitamente recebemos a variável por referência:

<?php

class Node
{
    // Todo
}

$node = new Node();

function change(&$node) {
    $node = 10; // Reflete no mesmo zval da variável $node fora desse escopo
}

change($node);

var_dump($node); // int(10)

Por fim, um exemplo de variáveis que não se referenciam, mas que apontam para o mesmo valor interno em memória até que uma operação de escrita seja realizada, aí é feita uma deep-copy:

<?php

$string = str_repeat('FOO ', 4);

xdebug_debug_zval('string'); // string: (refcount=1, is_ref=0)='FOO FOO FOO FOO '

$string2 = $string;

xdebug_debug_zval('string'); // string: (refcount=2, is_ref=0)='FOO FOO FOO FOO '

$string3 = $string;

xdebug_debug_zval('string'); // string: (refcount=3, is_ref=0)='FOO FOO FOO FOO '

$string3 .= 'BAR';

xdebug_debug_zval('string'); // string: (refcount=2, is_ref=0)='FOO FOO FOO FOO '

Observe que depois que $string3 fez uma operação de escrita o refcount do valor de $string foi decrementado.

Outro exemplo genérico:

<?php

class Foo {
    public $index;
}

$a = new Foo();
$b = $a;
$c = &$b;

$c->index = 10;

echo "({$a->index}, {$b->index}, {$c->index})" . PHP_EOL; // (10, 10, 10)

$c = "Hello";

echo "({$a->index}, {$b}, {$c})"; // (10, Hello, Hello)

Na prática temos:

Diagrama de referência

Todos os símbolos apontam pro mesmo objeto, no entanto, $c é referência de $b, alterações feitas em $c refletirão na mesma zval de $b, mantendo $a intacta.

Em que momento devo usar referências?

As funções de sorting de arrays do PHP os recebem por referência, por exemplo:

sort ( array &$array [, int $sort_flags = SORT_REGULAR ] ) : bool

Sempre que você ver na assinatura de uma função &amp;$var, significa que está recebendo por referência e que, portanto, vai trabalhar na mesma zval da variável informada.

E usar referências não é sobre performance, dependendo do uso, pode piorá-la. Usar referências é mais sobre lidar melhor com a memória, como no caso da função acima, não faria sentido duplicar o array inteiro só para aplicar o algoritmo de sorting.

Mas, no geral, a regra é que você dificilmente vai usar com frequência operações com referências. Usa-se referências quando realmente existe um objetivo muito bem definido, portanto, não se preocupe em querer “otimizar” o seu código pra usar referências, pois é bem possível que você não precisa delas.

O que mais posso fazer com referências?

Você pode ver todas as sintaxe possíveis para se usar referências no PHP através desse link da documentação oficial.

Palavras finais

Sumarizando o que foi visto nesse artigo:

  • PHP usa a semântica de valor;
  • Objetos não são passados por referência;
  • Valores complexos como arrays e strings são apenas duplicados quando alterados (usa-se o princípio copy-on-write);
  • Referências não deixam o código mais performático, se mal utilizadas, podem é deixar mais lento, devido aos vários apontamentos que precisam ser feitos. Referências são usadas em casos bem específicos em que o desenvolvedor tem a exata noção do comportamento que ele espera ter;
  • O desenvolvedor não precisa na maior parte do tempo se preocupar com memória (a não ser que vá trabalhar com grandes data sets, mas aí pode-se estudar o uso de generators no PHP ou alguma outra estratégia);

Não é nada fácil entender a figura completa desse conjunto de pecinhas, mas caso você tenha interesse em se aprofundar, recomendo a leitura dos artigos:

Até a próxima!

Desenvolvedor PHP
Formação: Desenvolvedor PHP
Nesta formação você aprenderá todos os conceitos da linguagem PHP, uma das mais utilizadas no mercado. Desde de conceitos de base, até características mais avançadas, como orientação a objetos, práticas de mercado, integração com banco de dados. Ao final, você terá conhecimento para desenvolver aplicações PHP usando as práticas mais modernas do mercado.
CONHEÇA A FORMAÇÃO

PHP

Autowiring em Container de Injeção de Dependência

No artigo Entendendo Injeção de Dependência vimos sobre o que é injeção de dependência, seu funcionamento e como se dá a sua aplicação. No artigo Container De Injeção De Dependência (DI Container) vimos como funciona um container para gerenciar o mapa de dependências.

Nesse artigo veremos uma funcionalidade relativamente comum nos containers de dependência de frameworks que é a habilidade de resolver as dependências de um construtor (principalmente) automaticamente, chamada de autowiring.

Symfony - Fundamentos
Curso de Symfony - Fundamentos
CONHEÇA O CURSO

No artigo Container De Injeção De Dependência (DI Container) implementamos um protótipo (simples, porém funcional) de um container e, em determinado momento, chegamos nesse tipo de caso de uso:

$indexController = new IndexController(
    $container->get('user.repository')
);

$userController = new UserController(
    $container->get('user.repository')
);

$registerController = new RegisterController(
    $container->get('user.repository')
);

Alguns controladores precisavam receber a instância de UserRepository e tivemos que manualmente passá-la para cada um deles. Agora, imagine se cada um desses controladores tivesse que receber outras dependências? Nenhum problema, só que ficaria meio chato e improdutivo ter que ficar repetindo essas construções na instanciação deles, concorda?

E se desenvolvermos uma forma do nosso container automaticamente resolver essas dependências? Desse modo, tudo o que faríamos seria pedir para o Container uma instância desses controladores, sem a necessidade de nos preocuparmos em passar manualmente para o construtor de cada um deles as suas dependências. Teríamos como resultado um código assim:

$indexController = $container->get(IndexController::class);

$userController = $container->get(UserController::class);

$registerController = $container->get(RegisterController::class);

Estamos pedindo as instâncias desses controladores sem nos preocuparmos em alimentá-los de suas dependências, deixamos esse trabalho para o Container.

Estrutura inicial do projeto

Vamos criar o protótipo de uma aplicação para que possamos testar a nossa implementação de Container. A nossa aplicação terá essa estrutura:

- [blog-artigo-di]
- - - [app]
- - - - [Http]
- - - - - - [Controller]
- - - - - - - UserController.php
- - - - [Repositories]
- - - - - LeadRepository.php
- - - - - TagRepository.php
- - - - - UserRepository.php
- - - Container.php
- index.php

O repositório dessa estrutura no GitHub você encontra clicando aqui. Se desejar, você pode fazer download direto dela clicando nesse link: sem-autowiring.zip

Depois de baixar e colocar no local onde normalmente você executa seus projetos, basta que você execute composer install pois o projeto faz uso da PSR-4 para autoloading:

$ ~/D/w/blog-artigo-di&gt; composer install

O nosso Container atualmente possui uma implementação simples e que ainda não suporta a resolução automática de dependências. Abra app/Container.php:

<?php
declare(strict_types=1);

namespace App;

use Closure;

final class Container
{
    private $instances = [];

    public function set(string $id, Closure $closure) : void
    {
        $this->instances[$id] = $closure;
    }

    public function get(string $id) : object
    {
        return $this->instances[$id]($this);
    }

    public function singleton(string $id, Closure $closure) : void
    {
        $this->instances[$id] = function () use ($closure) {
            static $resolvedInstance;

            if (null !== $resolvedInstance) {
                $resolvedInstance = $closure($this);
            }

            return $resolvedInstance;
        };
    }
}

Esse container já foi explicado na indicação de leitura no início desse artigo. Portanto, não vamos entrar novamente em seus pormenores.

O arquivo index.php na raiz do projeto é o responsável pelo bootstrap da nossa aplicação protótipo:

<?php

// Carrega o autoload do Composer
require './vendor/autoload.php';

use App\Repositories\TagRepository;
use App\Repositories\LeadRepository;
use App\Repositories\UserRepository;
use App\Http\Controller\UserController;

// Instancia o container
$container = new App\Container();

// Adiciona referências ao container
$container->set(TagRepository::class, function() {
    return new TagRepository();
});

$container->set(LeadRepository::class, function() {
    return new LeadRepository();
});

$container->set(UserRepository::class, function() {
    return new UserRepository();
});

// Instancia o UserController passando para ele as dependências necessárias
$userController = new UserController(
    $container->get(TagRepository::class),
    $container->get(UserRepository::class),
    $container->get(LeadRepository::class)
);

$userController->index();

Para testar o exemplo, na raiz do projeto execute:

$ ~/D/w/blog-artigo-di&gt; php -S localhost:8000

Isso executará o servidor embutido do PHP em cima da raiz do nosso projeto, permitindo que o acessemos pelo navegador através da URL: http://localhost:8000/

Silex - Framework PHP
Curso de Silex - Framework PHP
CONHEÇA O CURSO

O resultado será:

/Users/kennedytedesco/Documents/www/blog-artigo-di/app/Http/Controller/UserController.php:31:
array (size=2)
  'id' =&gt; int 1
  'name' =&gt; string 'tag' (length=3)

/Users/kennedytedesco/Documents/www/blog-artigo-di/app/Http/Controller/UserController.php:31:
array (size=2)
  'id' =&gt; int 2
  'name' =&gt; string 'user' (length=4)

/Users/kennedytedesco/Documents/www/blog-artigo-di/app/Http/Controller/UserController.php:31:
array (size=2)
  'id' =&gt; int 3
  'name' =&gt; string 'lead' (length=4)

Indica que o método index() do UserController foi executado com sucesso. Ou seja, as dependências, os repositórios necessários para o funcionamento dessa classe foram injetados com sucesso. Até aqui tudo bem, ademais, injetamos tais dependências manualmente:

$userController = new UserController(
    $container->get(TagRepository::class),
    $container->get(UserRepository::class),
    $container->get(LeadRepository::class)
);

Implementando a resolução automática de dependências

Para que possamos implementar a resolução automática das dependências utilizaremos a API de reflexão do PHP. Essa API nos fornece meios para que façamos engenharia reversa nas classes, extraindo muitas de suas informações internas como quantos métodos possui, quais são públicos, protegidos ou privados, se implementa um construtor, quais são os parâmetros do construtor, se são opcionais ou exigidos, entre outras informações.

Dessa forma, se o usuário pedir para o Container a instanciação do UserController:

$userController = $container->get(UserController::class);

Que tal a gente usar reflexão para obter quais dependências (classes) ele necessita para ser instanciado e então resolver essas dependências automaticamente usando o próprio Container e retornar a instância dele?

É exatamente isso que a nossa implementação de autowiring fará. Para tanto, começaremos alterando o código do nosso container app\Container.php para:

<?php
declare(strict_types=1);

namespace App;

use Closure;
use ReflectionClass;
use ReflectionParameter;

final class Container
{
    private $instances = [];

    public function set(string $id, Closure $closure) : void
    {
        $this->instances[$id] = $closure;
    }

    public function get(string $id) : object
    {
        // Se essa referência existe no mapa do container, então a retorna diretamente.
        if ($this->has($id)) {
            return $this->instances[$id]($this);
        }

        // Se a referência não existe no container, então foi passado uma classe para ser instanciada
        // Façamos então a reflexão dela para obter os parâmetros do método construtor
        $reflector = new ReflectionClass($id);
        $constructor = $reflector->getConstructor();

        // Se a classe não implementa um método construtor, então vamos apenas retornar uma instância dela.
        if (null === $constructor) {
            return new $id();
        }

        // Itera sobre os parâmetros do construtor para realizar a resolução das dependências que ele exige.
        // O método "newInstanceArgs()" cria uma nova instância da classe usando os novos argumentos passados.
        // Usamos "array_map()" para iterar os parâmetros atuais, resolvê-los junto ao container e retornar um array das instâncias já resolvidas pelo container.
        return $reflector->newInstanceArgs(array_map(
            function (ReflectionParameter $dependency) {
                // Busca no container a referência da classe desse parâmetro
                return $this->get(
                    $dependency->getClass()->getName()
                );
            },
            $constructor->getParameters()
        ));
    }

    public function singleton(string $id, Closure $closure) : void
    {
        $this->instances[$id] = function () use ($closure) {
            static $resolvedInstance;

            if (null !== $resolvedInstance) {
                $resolvedInstance = $closure($this);
            }

            return $resolvedInstance;
        };
    }

    public function has(string $id) : bool
    {
        return isset($this->instances[$id]);
    }
}

(Através do GitHub você pode visualizar o que foi adicionado/removido. Basta visualizar esse commit aqui).

A partir do momento que o nosso Container consegue resolver as dependências automaticamente, podemos alterar no index.php a forma de instanciar, que era uma instanciação direta da classe usando new UserController:

$userController = new UserController(
    $container->get(TagRepository::class),
    $container->get(UserRepository::class),
    $container->get(LeadRepository::class)
);

Para uma instanciação que usa o Container (para que ele possa resolver as dependências para nós):

$userController = $container->get(UserController::class);
$userController->index();

(O código completo do exemplo com autowiring encontra-se no branch master do repositório desse exemplo).

Você pode testar o exemplo e terá o mesmo resultado obtido anteriormente quando não usávamos autowiring. O UserController continuará sendo instanciado com sucesso.

Concluindo

Vimos nesse artigo a fundação sobre como a resolução automática de dependências é feita nos containers de injeção de dependência. É por esse caminho que Symfony, Laravel entre outros frameworks (inclusive de outras linguagens) fazem. Obviamente o nosso Container é simples, didático e bem direto ao ponto, não estando 100% pronto para uso em projetos reais. Algumas verificações de segurança (se a classe existe, senão lançar uma exceção etc) precisariam ser implementadas. Na realidade, existem boas implementações de containers por aí e, se você usa um Web Framework, não vai precisar criar a sua própria. No entanto, saber como funciona, é essencial. Essa foi a intenção desse artigo.

Até a próxima!

Desenvolvedor Symfony Full-Stack
Formação: Desenvolvedor Symfony Full-Stack
Nesta formação você aprenderá desenvolver aplicações PHP usando o framework Symfony com desenvoltura. Ao final desta formação, terá condições de trabalhar em grandes aplicações web ou APIs integradas com diversos serviços, tudo isso usando as melhores práticas do mercado.
CONHEÇA A FORMAÇÃO

Orientação a Objetos

Os pilares da orientação a objetos

O paradigma orientado a objetos é um dos paradigmas mais utilizados no mercado de trabalho. Além de ser um dos primeiros paradigmas com o qual nós temos contato quando começamos a estudar desenvolvimento de software, a maioria das linguagens utilzadas pela indústria em geral possui uma forte base orientada a objetos, o que faz com que seja essencial o domínio deste paradigma. Neste artigo, vamos verificar quais são os pontos principais do paradigma orientado a objetos.

Caso você esteja iniciando na área de desenvolvimento, indico também a leitura de nosso guia de introdução à programação.

Lógica Orientada a Objetos Completo
Curso de Lógica Orientada a Objetos Completo
CONHEÇA O CURSO

Classes e objetos

Em linguagens orientadas a objeto, nós organizamos a maior parte do nosso código em estruturas chamadas classes.

Você pode entender uma classe como sendo inicialmente um molde, molde este que geralmente representa alguma estrutura do mundo real com a qual nosso código terá que lidar. Vamos imaginar que estejamos trabalhando com uma aplicação que lida com carros…

Provavelmente, nossa aplicação irá lidar com carros e, por causa disso, precisaremos de um molde para definirmos o que é um carro dentro do nosso código. Esse molde seria responsável por estabelecer o que é um carro e o que pode fazer um carro.

Se pararmos para pensar, nós podemos falar que um carro pode ser caracterizado pelos seguintes itens:

  • Marca;
  • Modelo;
  • Cor;
  • Placa;
  • E várias outras características…

Também podemos dizer que um carro pode ter as seguintes ações:

  • Ligar;
  • Acelerar;
  • Desligar;
  • E várias outras ações…

Veja que, quando falamos do que um carro é, estamos falando de suas características, ou seja: falamos do que caracteriza e define um determinado carro. Já quando falamos do que um carro pode fazer, estamos falando das ações que um carro pode desempenhar.

Trazendo para termos técnicos, nós podemos chamar as características do carro de atributos, euquanto nós chamamos as ações de métodos. No final, os métodos e atributos ficam agrupados em uma classe.

Nós podemos representar estes atributos e métodos através de uma linguagem de modelagem chamada UML (Unified Modeling Language). A UML prevê alguns diagramas que visam auxiliar o processo de modelagem de um software. Entre estes diagramas, nós temos justamente o diagrama de classes.

A classe Carro, se fosse representada pelo diagrama de classes UML, poderia ficar da seguinte maneira:

Diagrama da classe carro

Perceba que a partir deste molde, nós podemos especificar vários carros… Poderíamos ter um Fiat Línea prata com a placa ABC-1234, um Volswagen Gol preto com a placa DEF-4567 ou mesmo um Hyundai HB20 branco com a placa GHI-8901… Todos eles são carros, já que derivam do mesmo molde: todos eles têm marca, modelo, cor e placa, além de poderem ser ligados, desligados, freados e acelerados, atributos e métodos todos estabelecidos pela classe Carro. Nesse caso, o Fiat Línea, o Volkswagen Gol e o Hyundai HB20 são objetos, pois foram “fabricados” a partir do molde que definimos, que é a classe Carro. Objetos são como variáveis que criamos para utilizar as nossas classes, seja para definir seus atributos, como também para invocar seus métodos. É o encadeamento coordenado entre escritas e leituras de atributos com a invocação de métodos que dá a tônica de uma aplicação escrita com uma linguagem orientada a objetos.

Se fôssemos utilizar o Java para representar nossa classe e nossos objetos de uma maneira primitiva, teríamos o código abaixo.

// Pacotes e demais estruturas omitidas para clareza...
public class Carro {
    public String modelo;
    public String marca;
    public String cor;
    public String placa;

    public void ligar() {
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        System.out.println("O veículo desligou!");
    }
}

// ...
Carro gol = new Carro();
gol.modelo = "Gol";
gol.marca = "Volkswagen";

Carro linea = new Carro();
linea.modelo = "Línea";
linea.marca = "Volkswagen";

gol.ligar();
gol.desligar();
linea.ligar();
linea.desligar();

No exemplo acima, temos a classe Carro fazendo o papel de nosso molde. As variáveis gol e linea são objetos que são do tipo Carro ou, utilizando os termos técnicos corretos, gol e linea são instâncias da classe Carro. Por derivarem da classe Carro, estes objetos têm todos os atributos e métodos previstos pela classe Carro.

Encapsulamento

Ainda levando em consideração o exemplo com a classe Carro, poderíamos imaginar um indicador para verificarmos se o carro está ligado ou não. Isso até daria mais “qualidade” à nossa classe Carro: nós poderíamos, por exemplo, garantir que o carro só pudesse ser acelerado ou freado caso estivesse ligado. Esse indicador ainda poderia ser modificado pelos métodos ligar()e desligar()

Partindo dessa idéia, nossa classe Carro ficaria da seguinte maneira:

public class Carro {

    public String modelo;
    public String marca;
    // ...
    public boolean ligado;  

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }
}

Veja que, além de adicionarmos um atributo chamado ligado do tipo booleano (truepara ligado ou false para desligado), nós temos ainda o trecho de código abaixo:

public Carro() {
    ligado = false;
}

Esse trecho de código é um construtor. O construtor é invocado quando inicializamos um objeto a partir de uma classe. Nós, de maneira geral, invocamos o construtor quando chamamos a criação da instância com a palavra-chave new. Sendo assim, quando temos o código abaixo…

Carro gol = new Carro();

… nós estamos justamente chamando este método construtor. Ao reescrevermos este método, estamos impondo uma “personalização” na inicialização dos objetos a partir da classe Carro: todo carro será criado já possuindo o indicador ligado como false, ou seja, o carro já começa como desligado por padrão.

Agora, poderíamos definir, por exemplo, um método chamado acelerar() em nosso Carro. Nós podemos verificar o atributo ligado para “controlar” melhor este método: um carro, obviamente, só pode ser acelerado caso esteja ligado.

Nosso código de exemplo ficaria da seguinte maneira:

public class Carro {

    public String modelo;
    public String marca;
    // ...
    public boolean ligado;  

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }
}

Assim, poderíamos ter a nossa classe Carro sendo utilizada da seguinte maneira:

Carro gol = new Carro();
System.out.println(gol.ligado); // Vai imprimir "false" por causa do construtor personalizado
gol.modelo = "Gol";
gol.marca = "Volkswagen";
// gol.acelerar(); Se essa linha for descomentada, o código gerará um erro, pois estamos tentando acelerar um carro desligado
gol.ligar();
System.out.println(gol.ligado); // Vai imprimir "true", pois o método ligar() foi chamado
gol.acelerar();
gol.desligar();
System.out.println(gol.ligado); // Vai imprimir "false", pois o método desligar() foi chamado

Aparentemente, nossa classe Carro está funcionando corretamente. Mas, temos um problema: o atributo ligado é acessível para todo mundo, da mesma maneira que os atributos modelo e marca por exemplo. Isso quer dizer que nós podemos “deturpar” o comportamento da classe Carro… Nós poderíamos, por exemplo, alterar manualmente o conteúdo do atributo ligado antes de chamarmos o método ligar(), permitindo acelerar um carro que estivesse em tese desligado:

Carro gol = new Carro();
System.out.println(gol.ligado); // Vai imprimir "false" por causa do construtor personalizado
gol.ligado = true; // Atributo que define se o carro está ligado ou não alterado "na mão"
gol.acelerar(); // Agora a linha não causará erro, mesmo que o método ligar() não tenha sido chamado

Isso certamente é uma situação problemática, pois agora nosso carro dá uma brecha para funcionar de maneira diferente de como ele foi planejado.

Para corrigirmos isso, precisamos recorrer a um pilar da orientação a objetos: o encapsulamento. O encapsulamento visa esconder atributos e métodos de nossas classes que não deveriam ser acessados por outras estruturas. É exatamente o que precisamos: o atributo ligado deveria ser acessível em tese só pela própria classe Carro, o que permitiria somente aos métodos ligar() e desligar() alterarem o indicador de funcionamento do carro da maneira correta. Isso evitaria que nós acessássemos o atributo do lado de fora, causando a falha no código que estamos discutindo aqui.

O encapsulamento nas linguagens orientadas a objetos é definido através de algo que chamamos de atributo de visibilidade. Estes atributos de visibilidade estabelecem justamente o quão acessível nossos atributos e métodos são com relação às demais estruturas do nosos código. De maneira geral, temos três atributos de visibilidade básicos e comuns às linguagens orientadas a objeto em geral:

  • public: a estrutura é visível a partir de qualquer lugar no código, inclusive em outras classes fora a classe que define o atributo ou método em si;
  • private: a estrutura é visível somente pela classe que define a estrutura em si. Estruturas externas, como outras classes, não conseguem acessar o método ou atributo que esteja marcado com este atributo de visibilidade;
  • protected: a estrutura é visível somente na classe-mãe e nas classes-filhas.

Se considerarmos a nossa classe Carro, podemos ver que o problema relatado acontece porque o atributo ligado está definido como public, o tornando acessível em qualquer lugar. Vimos que esse é o problema, pois o atributo ligado não poderia ser acessível a partir de qualquer lugar: ele deveria ser acessível somente dentro dos métodos ligar() e desligar(), ambos dentro da classe Carro. O nosso atributo ligado não está encapsulado.

Lógica de Programação Completo
Curso de Lógica de Programação Completo
CONHEÇA O CURSO

Poderíamos o encapsular se o tornássemos private, fazendo com que ele fosse acessível somente dentro da classe Carro.

public class Carro {

    public String modelo;
    public String marca;
    // ...
    private boolean ligado; 

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }
}

Assim, o erro que víamos antes não acontecerá mais, pois o atributo ligado agora só é acessível dentro da própria classe Carro.

Carro gol = new Carro();
gol.ligado = true; // Essa linha causará um erro de compilação, pois o atributo "ligado" não é mais acessível externamente

Agora, podemos dizer que o atributo ligado da classe Carro está encapsulado.

Se quiséssemos pelo menos ler o valor do atributo ligado externamente (já que alterá-lo externamente estaria completamente errado), poderíamos criar um método que devolvesse o valor do atributo ligado.

public class Carro {

    public String modelo;
    public String marca;
    // ...
    public boolean ligado;  

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }

    public boolean estaLigado() {
        return ligado;
    }
}

Com o método acima, poderíamos pelo menos verificar externamente se o carro está ligado ou desligado.

Carro gol = new Carro();
System.out.println(gol.estaLigado()); // Vai imprimir "false" por causa do construtor personalizado
gol.modelo = "Gol";
gol.marca = "Volkswagen";
gol.ligar();
System.out.println(gol.estaLigado()); // Vai imprimir "true", pois o método ligar() foi chamado
gol.acelerar();
gol.desligar();
System.out.println(gol.estaLigado()); // Vai imprimir "false", pois o método desligar() foi chamado

Este tipo de método é geralmente chamado de método de acesso, já que ele provê um tipo de acesso indireto a um atributo encapsulado. Com relação ao encapsulamento, nós temos dois tipos de métodos de acesso basicamente:

  • get: métodos que permitem ver o valor de um atributo;
  • set: métodos que permitem alterar o valor de um atributo.

Nós poderíamos falar que o método estaLigado() é um método get, pois ele permite a nós lermos algo que está encapsulado dentro da classe Carro.

É uma prática recorrente em linguagens orientadas a objeto (principalmente no Java) envolver todos os atributos com métodos de acesso do tipo get e set, evitando o acesso direto aos atributos. Apesar de ser uma prática comum, é importante dizer que só o fato de utilizarmos métodos de acesso não garante o encapsulamento de nenhuma estrutura. O que garante este encapsulamento é a definição de visibilidade correta de cada um dos atributos e métodos e o estabelecimento dos métodos de acesso de maneira correta. Por exemplo: não criamos um método set para o atributo ligado, pois ele só pode ser alterado pela própria classe.

Se fôssemos aplicar esta regra a nossa classe Carro, nós teríamos o seguinte código:

public class Carro {

    private String modelo;
    private String marca;
    // ...
    public boolean ligado;  

    public void setModelo(String modelo) {
        this.modelo = modelo;
    }

    public String getModelo() {
        return modelo;
    }

    public void setMarca(String marca) {
        this.marca = marca;
    }

    public String getMarca() {
        return marca;
    }

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }

    public boolean estaLigado() {
        return ligado;
    }
}

Herança

O reaproveitamento de código e a possibilidade de se evitar código duplicado são objetivos das linguagens orientadas a objetos. Vamos imaginar que agora nós precisamos criar uma classe para definir um outro tipo de veículo, como uma bicicleta. Uma bicicleta possui atributos em comum com um carro: ambos possuem marca e modelo, por exemplo. Além disso, ambos não deixam de ser um tipo de veículo.

Se fôssemos escrever o código para definição da classe Bicicleta sem considerar a classe Carro, nós teríamos o seguinte código:

public class Bicicleta {

    private String modelo;
    private String marca;

    public void setModelo(String modelo) {
        this.modelo = modelo;
    }

    public String getModelo() {
        return modelo;
    }

    public void setMarca(String marca) {
        this.marca = marca;
    }

    public String getMarca() {
        return marca;
    }

    public void acelerar() {
        System.out.println("A bicicleta foi acelerada.");
    }
}

Veja que acabamos duplicando todo o código relativo aos atributos marca e modelo nas classes Carro e Bicicleta, o que pode ser bem ruim em termos de manutenibilidade do código a longo prazo. Mas, nós temos uma maneira de evitar essa duplicidade: nós podemos utilizar o conceito de herança.

Nesse exemplo, se fôssemos aplicar o conceito de herança, nós teríamos três classes:

  • A classe Carro, com tudo que um carro possui de características e ações;
  • A classe Bicicleta, com tudo que uma bicicleta possui de características e ações;
  • Uma nova classe chamada Veiculo, com tudo que existe de comum entre carros e bicicletas.

No exemplo acima, para que as classes Carro e Bicicleta conseguissem usufruir das estruturas comuns estabelecidas na classe Veiculo, elas precisariam herdar a classe Veiculo.

Poderíamos representar esta relação entre as classes Veiculo, Carro e Bicicleta com a UML da seguinte maneira:

Diagrama de classes: Veiculo, Carro e Bicicleta

Também poderíamos definir as classes Veiculo, Carro e Bicicleta da seguinte maneira:

public class Veiculo {

    private String modelo;
    private String marca;

    public void setModelo(String modelo) {
        this.modelo = modelo;
    }

    public String getModelo() {
        return modelo;
    }

    public void setMarca(String marca) {
        this.marca = marca;
    }

    public String getMarca() {
        return marca;
    }

    public void acelerar() {
        // ???
    }

}

public class Carro extends Veiculo {

    public boolean ligado;  

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }

    public boolean estaLigado() {
        return ligado;
    }
}

public class Bicicleta extends Veiculo {

    public void acelerar() {
        System.out.println("A bicicleta acelerou!");
    }

}

Veja que tudo que é comum entre as classes Carro e Bicicleta foi para a classe Veiculo. As classes Carro e Bicicleta estão agora herdando a classe Veiculo para reaproveitar estas estruturas comuns, além de que um carro e uma bicicleta são tipos de veículos. É importante que, quando formos empregar a herança, exista essa relação de “ser” ou “estar” entre as classes.

Nesse caso, nós podemos fazer com que Carro herde Veiculo porque um carro é um veículo; assim como também podemos fazer com que Bicicleta herde Veiculo, pois uma bicicleta é um tipo de veículo. Com a herança, nós evitamos a duplicidade de código e facilitamos a manutenção, além de manter a coerência, desde que a regra do “ser/estar” seja devidamente implementada.

Quando temos a herança sendo utilizada, as classes podem assumir um dos dois papéis:

  • Classe-mãe, classe-base ou super-classe: é a classe que serve de base para as demais classes. Em nosso exemplo, a classe Veiculo é uma super-classe;
  • Classe-filha ou sub-classe: é a classe que herda outra determinada classe. No nosso exemplo, as classes Carro e Bicicleta são sub-classes.

Abstração

Quando estamos lidando com a orientação a objetos, é muito comum que nós sempre tentemos escrever código baseado em abstrações, pois isso traz flexibilidade ao código.

No exemplo anterior, nós esbarramos em um problema de abstração: todo veículo é capaz de acelerar, independente de ser um carro ou bicicleta. Sendo assim, nós não podemos remover o método acelerar() da nossa classe Veiculo, já que todo veículo tem esse comportamento. Mas, a própria classe Veiculo não “sabe” como acelerar.

Quem sabe como acelerar é a classe Carro(que sabe como um carro acelera) e a classe Bicicleta (que sabe como uma bicicleta acelera). Para resolver esta situação, poderíamos tornar o método acelerar() abstrato: isso vai desobrigar a classe Veiculo a definir uma implementação para este método (já que a classe Veiculo não sabe como acelerar), mas obriga as classes-filha (no caso, as classes Carro e Bicicleta) a definirem seus comportamentos de aceleração.

Além disso, se pararmos para analisar, não faz sentido nós instanciarmos objetos a partir da classe Veiculo, já que ela serve somente como uma super-classe em nosso cenário. Nós poderíamos instanciar objetos a partir das classes Carro e Bicicleta, mas não a partir da classe Veiculo. Como queremos evitar que objetos sejam instanciados a partir da classe Veiculo, já que ela deve ser somente uma classe-base, também podemos definir a classe Veiculo como sendo uma classe abstrata.

O código ficaria da seguinte maneira:

// Classe abstrata (não pode ser instanciada)
public abstract class Veiculo {

    private String modelo;
    private String marca;

    public void setModelo(String modelo) {
        this.modelo = modelo;
    }

    public String getModelo() {
        return modelo;
    }

    public void setMarca(String marca) {
        this.marca = marca;
    }

    public String getMarca() {
        return marca;
    }

    // Método abstrato: a classe Veiculo sabe que ela tem que acelerar, mas não sabe como fazer isso.
    // A responsabilidade passa a ser das classes-filha
    public abstract void acelerar();

}

public class Carro extends Veiculo {

    public boolean ligado;  

    public Carro() {
        ligado = false;
    }

    public void ligar() {
        ligado = true;
        System.out.println("O veículo ligou!");
    }

    public void desligar() {
        ligado = false;
        System.out.println("O veículo desligou!");
    }

    // Aqui, a classe Carro define como ela deve exercer o ato de acelerar
    @Override
    public void acelerar() {
        if (!ligado){
            throw new Exception("O carro não pode ser acelerado, pois ele está desligado."); // Erro: o carro está desligado!
        }
        System.out.println("O carro foi acelerado");
    }

    public boolean estaLigado() {
        return ligado;
    }
}

public class Bicicleta extends Veiculo {

    // Aqui, a classe Bicicleta define como ela deve exercer o ato de acelerar
    @Override
    public void acelerar() {
        System.out.println("A bicicleta acelerou!");
    }

}

Com a utilização da abstração nesse caso, sabemos que qualquer novo tipo de veículo que for criado a partir da classe Veiculo terá que obrigatoriamente implementar o comportamento de aceleração. Isso faz muito sentido, já que todo veículo tem que ser capaz de acelerar.

Polimorfismo

Linguagens orientadas a objetos ainda prevêem o suporte para criação de estruturas polimórficas. Estruturas polimórficas são estruturas que conseguem mudar seu comportamento interno em determinadas circunstâncias. Essa variação comportamental pode acontecer por algumas formas, como através da sobescrita de métodos e através do LSP (Liskov Substitution Principle).

No exemplo anterior, nós temos um exemplo de polimorfismo através da sobrescrita de métodos: nós temos a classe Veiculo, que possui o método abstrato acelerar(). O método é abstrato porque a classe Veiculo só sabe que tem que conter este comportamento, mas não sabe como esse comportamento deve ocorrer.

Mas, as classes Carro e Bicicleta foram obrigadas a implementar o método acelerar() por herdarem a classe Veiculo. Cada uma dessas classes implementou o método acelerar() da maneira mais adequada para cada um dos tipos de veículos. Aqui, já temos um exemplo de polimorfismo: as classes Carro e Bicicleta são polimórficas, pois possuem um ancestral comum (a classe Veiculo) que as obriga a implementar o método acelerar(), mas cada uma delas implementa o mesmo método da maneira mais correta para cada tipo de veículo.

Essa mudança de implementação não exigiu a mudança do código da classe Veiculo, além de que a implementação dos métodos acelerar() em cada uma das classes é isolada: a implementação do método acelerar() na classe Carro não afeta a implementação do mesmo método na classe Bicicleta, e vice-versa.

Outro exemplo de polimorfismo seria através da aplicação do Princípio da Substituição de Liskov (também conhecido como LSP – Liskov Substitution Principle). O LSP é parte de um conjunto de cinco práticas de codificação conhecido como SOLID. Estes princípios visam a produção de código com alta qualidade e alinhado com os princípios das linguagens orientadas a objeto.

Para entender o LSP, considere o código abaixo:

Carro veiculo = new Carro();
// ...
veiculo.acelerar(); // Vai escrever "O carro foi acelerado"
// ...

O código não aparenta nada de diferente: temos um objeto chamado veiculo do tipo Carro. Mas, pelo fato de Carro herdar a classe Veiculo, nós podemos deduzir que um objeto do tipo Carro também pode ser considerado como sendo do tipo Veiculo; afinal, todo Carro agora é também um Veiculo por causa da herança. Sendo assim, podemos escrever o código acima da seguinte maneira:

Veiculo veiculo = new Carro();
// ...
veiculo.acelerar(); // Vai escrever "O carro foi acelerado"
// ...

Nós podemos definir o objeto veiculo como sendo do tipo Veiculo, mas o criar com base em um Carro. Nesse momento, o objeto veiculo vai se comportar como um Carro. No caso acima, dizemos que Veiculo é a abstração, enquanto Carro é a concretização.
Se quiséssemos trocar o tipo do nosso veículo para uma bicicleta, bastaria trocar a concretização.

Todo o código abaixo continuaria funcionando normalmente, já que uma Bicicleta também é um Veiculo. Isso torna nosso código muito mais flexível (se quisermos alterar o comportamento do nosso código, basta trocar as concretizaçãoes) e a prova de falhas de escrita de código (já que a abstração vai garantir que o código que vier logo abaixo da definição da concretização vai continuar funcionando de maneira transparente).

Veiculo veiculo = new Bicicleta();
// ...
veiculo.acelerar(); // Vai escrever "A bicicleta foi acelerada"
// ...

Neste exemplo, além de todas as vantagens que podemos notar no que diz respeito à qualidade e manutenibilidade do código, podemos dizer que o objeto veiculo é um objeto polimórfico, pois a concretização está sendo capaz de alterar seu comportamento. Porém, todo o código subsequente não é afetado por essa troca.

Conclusão

As definições de classes, objetos, encapsulamento, herança, abstração e polimorfismo constituem os principais pilares do paradigma orientado a objetos. Estes são considerados temas pilares pois, através dele, podemos começar a obter as vantagens que o paradigma orientado a objetos visa oferecer da maneira mais correta possível. É imprescindível que nós, como desenvolvedores, conheçamos estes pilares para que consigamos escrever código orientado a objetos flexível, conciso e com um grau de qualidade superior.

Aqui, vale uma ressalva: cada linguagem oferece estes pilares de uma maneira própria, a depender da filosofia da linguagem. Existem linguagens orientadas a objeto que talvez não ofereçam o conceito de visibilidade de maneira tão explítica; algumas linguagens permitem herança múltipla, outras linguagens oferecem apenas herança simples… De fato, para o total aproveitamento dos conceitos da orientação a objetos, é imprescindível que, além de conhecer estes pilares, o desenvolvedor também conheça a filosofia e a sintaxe da linguagem que está sendo utilizada.

Desenvolvedor Java
Formação: Desenvolvedor Java
A formação Desenvolvedor Java da TreinaWeb tem como objetivo apresentar o desenvolvimento através do Java e todo o ecossistema para desenvolvimento da Oracle. Nesta formação, são desde tópicos básicos como o paradigma orientado a objetos, a preparação do ambiente de desenvolvimento para o Java através do Eclipse e o controle de versão de código através do Git e do GitHub. Até aspectos mais avançados como acesso a bancos de dados relacionais e o desenvolvimento de aplicações web com o Java.
CONHEÇA A FORMAÇÃO

Python

Utilizando herança no Python

Desenvolvida originalmente sob o paradigma funcional, o Python precisou evoluir com o tempo para se adequar ao mercado competitivo e, assim, se tornar uma das linguagens mais utilizadas do mundo. Para isso, um dos principais recursos que foram implementados foi o suporte ao paradigma orientado ao objetos, porém, sem perder a essencia da linguagem, fazendo com que muita coisa seja implementada de forma diferente quando comparado com o Java ou o C#, por exemplo. Uma dessas mudanças foi a adição do recurso de herança na linguagem e que veremos como utilizá-la neste artigo.

Python - Orientação a objetos
Curso de Python - Orientação a objetos
CONHEÇA O CURSO

O que é a Herança?

A Herança é um conceito do paradigma da orientação à objetos que determina que uma classe (filha) pode herdar atributos e métodos de uma outra classe (pai) e, assim, evitar que haja muita repetição de código.

Um exemplo do conceito de herança pode ser visto no diagrama a seguir:

Nele temos que as classes filhas Gato, Coelho e Cachorro herdam os atributos e métodos da classe pai Animal e, assim caracterizando a herança.

Não vamos entrar em muitos detalhes sobre o conceito da herança, mas se você quiser estudar um pouco mais a fundo esse conceito, recomendo um outro artigo aqui do blog: Devo usar herança ou composição?

Como usar a herança no Python?

Para utilizar a herança no Python é bem simples. Assim com vimos no diagrama acima, vamos criar quatro classes para representar as entidades Animal, Gato, Cachorro e Coelho.

Obs: não vamos nos preocupar com a implementação de métodos e atributos, vamos criar apenas o básico para demonstrar o conceito da herança.

class Animal():
    def __init__(self, nome, cor):
        self.__nome = nome
        self.__cor = cor

    def comer(self):
        print(f"O {self.__nome} está comendo")

No código acima definimos a classe pai que irá possuir todos os atributos e métodos comuns às classes filhas (Gato, Cachorro e Coelho). Nela, criamos apenas o construtor que irá receber o nome e a cor do animal, além do método comer que vai exibir a mensagem com o nome do animal que está comendo.

Após isso, criamos as três classes “filhas” da classe Animal. Para definir que estas classes são herdeiras da classe Animal, declaramos o nome da classe pai nos parenteses logo após definir o nome da classe, como podemos ver abaixo:

import animal

class Gato(animal.Animal):
    def __init__(self, nome, cor):
        super().__init__(nome, cor)
import animal

class Cachorro(animal.Animal):
    def __init__(self, nome, cor):
        super().__init__(nome, cor)
import animal

class Coelho(animal.Animal):
    def __init__(self, nome, cor):
        super().__init__(nome, cor)

Note que as classes filhas só estão repassando seus dados de nome e cor para a classe Pai através do super() e que nenhum método foi implementado dentro dessas classes.

Agora, por herdar da classe Animal, as classes Gato, Cachorro e Coelho podem, sem nenhuma alteração, utilizar o método comer(), definido na classe Animal pois elas herdam dessa classe, logo elas possuem a permissão de invocar este método:

import gato, cachorro, coelho

gato = gato.Gato("Bichano", "Branco")
cachorro = cachorro.Cachorro("Totó", "Preto")
coelho = coelho.Coelho("Pernalonga", "Cinza")

gato.comer()
cachorro.comer()
coelho.comer()

Ao executar o código acima, obtemos o seguinte retorno no terminal:

O Bichano está comendo
O Totó está comendo
O Pernalonga está comendo

E é dessa forma que é implementado o conceito de herança no Python. Há muitos outros conceitos, dentro da herança, a serem abordados, como a sobrescrita de métodos, herança múltipla… Mas não se preocupem, estes tópicos serão abordados em breve. 🙂

Ah, claro, muito te recomendo o nosso completo curso sobre Orientacão a Objetos com Python:

Python - Orientação a objetos
Curso de Python - Orientação a objetos
CONHEÇA O CURSO

Espero vocês lá. Abraço!


Desenvolvimento Back-end PHP

Object Calisthenics em PHP – Parte 2

Este artigo é uma continuação do anterior. Para relembrarmos, abaixo a lista dos assuntos e em negrito os que serão abordados nesse artigo:

  1. Um nível de indentação por método
  2. Não use ELSE
  3. Envolva seus tipos primitivos
  4. Envolva suas collections em classes
  5. Uma chamada de método por linha
  6. Não abrevie
  7. Mantenha as classes pequenas
  8. Não tenha classes com mais de duas variáveis de instância
Windows - Fundamentos para desenvolvedores
Curso de Windows - Fundamentos para desenvolvedores
CONHEÇA O CURSO

3. Envolva seus tipos primitivos

Podemos definir esse exercício para os tipos escalares em PHP que são: int, float, bool e string. “Envolver” vem de um significado da programação orientada a objetos, que quer dizer, colocar o tipo “envolta” de uma classe, a fim de trazer mais resultados e funcionalidades do que um tipo comum/escalar.

Essa técnica vêm de uma aplicação do DDD (Domain-Driven Design) chamada de Value Object, onde temos um objeto-valor pequeno, que irá cuidar de um tipo de dado específico. Como o PHP é fracamente tipado, a melhor aplicação será em passagens de parâmetros de métodos ou funções. Veja o código abaixo:

class Customer
{
    protected $name;
    protected $birthday;

    public function __construct(string $name, string $birthday)
    {
        // Validar aqui???
        $this->name = $name;
        $this->birthday = $birthday;
    }
}

Ambos parâmetros são validáveis e não é legal validarmos no construtor da classe, pelo fato de não podermos reaproveitar as validações. Já o fato de forçarmos os atributos como string na entrada, algo errado pode acontecer se o desenvolvedor que está usando a classe não souber, por exemplo, qual padrão de data utilizado para a entrada $birthday, o que pode gerar um problema lá na frente, possivelmente no banco de dados.

Abaixo um exemplo de bom e outro de mau uso da classe:

// Programador que conhece a classe
$customer = new Customer('John Doe', '1983-02-10');

// Programador que não conhece a classe
// pode gerar um 0000-00-00 no Database
$customer = new Customer('John Doe', '10/02/1983'); 

Poderíamos então usar duas classes que farão envolvimento no tipo string, por exemplo:

  • CustomerName: que cuidará de validação de nome de cliente, pode verificar tamanho, fazer trim, e até limpeza.
  • CustomerBithday: Mais importante que o nome, ela vai validar o formato da data ou até mesmo formatar a entrada como, por exemplo, converter 10/02/1983 para 1983-02-10, evitando assim um problema de inconsistência, lembrando que não precisa ser necessariamente uma classe e, seguindo o princípio de inversão de dependência, podemos facilmente trabalhar com interfaces seguindo estratégias.

Como ficaria após a implementação dessas classes:

class Customer
{
    protected $name;
    protected $birthday;

    public function __construct(CustomerName $name, CustomerBirthday $birthday)
    {
        $this->name = $name;
        $this->birthday = $birthday;
    }
}

Usando:

// Programador que conhece a classe
$customer = new Customer(
     new CustomerName('John Doe'), 
     new CustomerBirthday('1983-02-10')
);

// A data será formatada internamente
$customer = new Customer(
    new CustomerName('John Doe'), 
    new CustomerBirthday('10/02/1983')
);

Um possível problema dessa abordagem é que ela adiciona complexidade à base de código. Na tradução dos Object Calisthenics, é colocado que todos os tipos primitivos devem ser envolvidos em classes, porém, sabemos que em PHP isso pode se tornar improdutivo e desnecessário, portanto, analise o quanto aquela entrada ou tipo pode sofrer mudança, se precisa de validação, normalização etc, só aplique-o se tiver uma real justificativa.

4. Envolva suas collections em classes

Semelhante ao exercício anterior, devemos envolver nossas coleções. Isso significa que trabalhar com um CustomerList é melhor do que com um array, neste caso, o uso dará uma melhor flexibilidade para o tratamento da coleção.

Abaixo uma usabilidade com e outra sem coleção em classe:

// A lógica fica fora, o que pode trazer problemas futuros
foreach ($customers as $customer) {
    if ($customer->isGoldAccount()) {
        $customer->addBonus(new Money('R$ 50,00'));
    }
}

Nesse exemplo queremos adicionar um bônus aos clientes do tipo gold. $customers é um array e por isso para modificar a coleção, precisamos iterá-la com um foreach e ainda internamente verificar se o tipo do cliente é gold.

Se usarmos uma classe que envolve a coleção, ou seja, uma CustomerCollection ela poderá ter 2 métodos:

  • Para filtragem de tipos de clientes: filterGoldAccounts.
  • Para adicionar aos clientes o bônus: addBonus.

A usabilidade ficaria assim:

$customersCollection = new CustomersCollection; // Classe com Lazy Loading

// Filtramos os clientes de conta Gold
$goldCustomers = $customersCollection->filterGoldAccounts();

// Adicionamos pela collection o bonus de R$ 50,00
// filtrado pela classe de coleção
$goldCustomers->addBonus(new Money('R$ 50,00'));

// Por fim persistimos
$goldCustomers->persists();

Assim, temos coleções que são específicas e inteligentes o suficiente para melhorar a usabilidade e evitar erros de programação. No PHP temos um conjunto de classes padrão para lidar com listas, a SPL(Standard PHP Library) que tem uma sessão dedicada a iteradores.

5. Uma chamada de método por linha

Devemos sempre fazer uma chamada de método por linha, não se aplicando à bibliotecas que usam do padrão Method Chaining ou DSL(Domain Specific Language).

Para seguir esse exercício não devemos, por exemplo, ao desenvolver um conjunto de Models, relacioná-los com métodos em cadeia, isso pode ser uma péssima ideia, segue um exemplo:

$customer->getById(55988)
         ->getPurchase(18376)
         ->getProducts()
         ->filterById(234);

Queremos resgatar um produto do pedido de um cliente, pode-se parecer muito prático, porém, alguns problemas poderão ocorrer e é muito difícil testar um bloco desses. Como saber se getPurchase encontrou o pedido? E se não encontrou, o que acontece? Nesse caso, vem outra problemática: e se o pedido não contém itens ainda? E temos um retorno null, certamente teremos um erro de método não encontrado.

Por isso, para garantir que tudo ocorreu certo, podemos seguir a Lei de Demeter, ela diz que devemos somente conversar com classes próximas, então, criamos um método para conversar e filtrar o que precisamos, ao invés de percorrer pelos Models que estão distantes. Não focaremos na implementação, porém, o conceito de uso abaixo pode ilustrar essa aproximação:

// Resgatando o model isoladamente
$customer = $customerModel->getById(55988);

// Aproximação: método que pertence a Customer
// sua implementação cuidará de retornos nulls
$product = $customer->getPuchasedProduct(18376, 234);

O principal objetivo desse exercício é não sair percorrendo por objetos retornados em chamadas de métodos, usar chamadas de vários métodos em linha pode gerar muitos problemas de manutenção, dificuldade de entendimento e testes mal escritos. Costuma-se dizer que gera um código que “cheira mal”.

Conclusão

Esses exercícios são mais aprofundados e devem ser estudados com calma, não devemos seguí-los somente por que parece ser o certo, devemos entender a motivação por trás deles, lembrando que nenhuma dessas técnicas são balas de prata e vão servir para toda modelagem, o bom senso é a melhor direção.

Até o próximo artigo da série!

Slim - Microframework PHP
Curso de Slim - Microframework PHP
CONHEÇA O CURSO

PHP

Object Calisthenics em PHP – Parte 1

O conceito de object calisthenic foi criado pelo desenvolvedor Jeff Bay, que teve a visão de que praticar código reutilizável, limpo e de boa leitura é uma prática que necessita de muito exercício, ou seja, muita prática e constante melhoria e evolução através de ações e práticas.

Ele é composto por nove exercícios que constituem a aplicação dos mesmos ao se escrever código em orientação a objetos.

Object vem da programação orientada a objetos, e calisthenics do termo grego, Kales, simplificando, seria a forma de se obter um físico ou, no caso, um resultado a partir da prática de exercícios que deixarão seu código em “forma”.

Ao todo são nove regras básicas, veja abaixo:

  1. Um nível de indentação por método;
  2. Não use ELSE;
  3. Envolva seus tipos primitivos;
  4. Envolva suas collections em classes;
  5. Uma chamada de método por linha;
  6. Não abrevie;
  7. Mantenha as classes pequenas;
  8. Não tenha classes com mais de duas variáveis de instancia;
  9. Sem getters e setters;

Veremos neste artigo as duas primeiras que são exercícios fortíssimos que fazem já muita diferença quando praticados.

Slim - Microframework PHP
Curso de Slim - Microframework PHP
CONHEÇA O CURSO

1. Um nível de indentação por método

Como já especificado devemos internamente dentro de uma função usar somente um nível de indentação, quanto mais níveis de comandos de decisão ou estruturas de decisão, mais complexo o método fica, minando com a simplicidade do projeto.

Abaixo podemos ver um exemplo disso:

Código sem a regra de indentação:

class Customer
{
    public function getPromoCode(string $promoName)
    {
        // 1.
        if ($this->promoCode) {
            // 2.
            if (false === $this->promoCodeExpired()) {
                // 3.
                if ($this->promoName == $promoname) {
                    return $this->promoCode;
                } else {
                    throw new Exception('Promoção não existe mais');
                }
            } else {
                throw new Exception('Promoção Expirada');
            }      
        } else {
            throw new Exception('Cliente sem código de promoção');
        }
    }
}

Uma forma de “treinar” esse exercício é criando métodos protegidos auxiliares que tenham um motivo e reaproveitamento, atuando na facilitação da escrita e colocando os blocos dentro das funções de apoio.

Veja como resolvemos o problema acima:

class Customer
{
    public function getPromoCode(string $promoName)
    {
        if ($this->promoCode) {
            return $this->getValidPromoCode($promoName);
        } else {
            throw new Exception('Cliente sem código de promoção');
        }
    }

    protected function getValidPromoCode(string $promoName)
    {
        if (false === $this->promoCodeExpired()) {
            return $this->getPromoExists($promoName);
        } else {
            throw new Exception('Promoção Expirada');
        }    
    }

    protected function getPromoExists(string $promoName)
    {
        if ($this->promoName == $promoName) {
            return $this->promoCode;
        } else {
            throw new Exception('Promoção não existe mais');
        }
    }
}

2. Não use ELSE

Essa regra parece ser muito estranha, mas ela funciona muito bem com o conceito early return, que emprega o uso do “retorne seu valor o quanto antes”, ação que só é facilmente implementada dentro de funções, métodos ou loops.

A base deste exercício é sempre trabalhar com o return (ou continue), sabemos que ao cair em um return/continue o código abaixo não será executado o que ajuda na remoção dos “elses” ao inverter ou até modificar a validação antes usada.

Abaixo o código anterior com early return:

class Customer
{
    public function getPromoCode(string $promoName)
    {
        if ($this->promoCode) {
            return $this->getValidPromoCode($promoName);
        } 
        throw new Exception('Cliente sem código de promoção');
    }

    public function getValidPromoCode(string $promoName)
    {
        if (false === $this->promoCodeExpired()) {
            return $this->getPromoExists($promoName);
        }
        throw new Exception('Promoção Expirada'); 
    }

    public function getPromoExists(string $promoName)
    {
        if ($this->promoName == $promoName) {
            return $this->promoCode;
        }
        throw new Exception('Promoção não existe mais');
    }
}

Como trocamos os comandos de decisões para que fique um código limpo removemos os “else’s”, e simplificamos a lógica que passa a ser melhor compreendida e limpa, esse conceito também pode ser aplicador para loops, onde o intuito é usar o continue no lugar do “else”.

Vejamos abaixo um exemplo:

Antes:

// Exibir a lista de membros
// que pagaram a mensalidade
foreach ($members as $member) {
    if ($member->paid()) {
        $report[] = [$member->name => 'Paid'];
    } else {
        $report[] = [$member->name => 'Not Paid'];
    }
}

Depois:

// Sem Else
foreach ($members as $member) {
    if ($member->paid()) {
        $report[] = [$member->name => 'Paid'];
        continue;
    }
    $report[] = [$member->name => 'Not Paid'];
}

Ou até com um pouco mais de limpeza:

// Um pouco mais de limpeza
foreach ($members as $member) {
    $report[] = $member->paid() ? 
                    [$member->name => 'Paid'] : 
                    [$member->name => 'Not Paid'];
}

Conclusão

Veremos em breve as outras regras numa série de posts. O uso do object calisthenics trás muitos benefícios que em grande escala fazem uma grande diferença, comece esses exercícios e terá seu código elogiado e muito mais elegante.

Obviamente nem sempre é possível de imediato “atacar” seu código com os calisthenics, mas com refatorações isso começa a ficar natural e quanto mais prática mais natural fica a aplicação desses exercícios.

Espero que tenham gostado e nos vemos nos próximos posts. Bons estudos!

PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO