A essência da Orientação a Objetos

Esse artigo traz as bases conceituais associadas ao paradigma Orientado a Objetos, mas não apenas as ideias de abstração, encapsulamento, etc. mas as consequências do seu uso. Por conta disso vemos, de modo geral, conceitos como Entropia de Software, Acoplamento e Coesão, e um pouquinho de código por que ninguém é de ferro.

Vamos falar um pouquinho sobre A essência da Orientação a Objetos. O mais comum é ver os conceitos-chave: abstração, polimorfismo, etc. Vamos falar com pouco sobre eles, com exemplos. Mas usar o paradigma OO tem consequencias que não podem ser desprezadas. Portanto, entenda que quanto mais classes mais complexo tende a ser o sistema. Assim, vamos nos aprofundar em conceitos de design de sistemas para uma melhor consolidação da essência da OO.

Além disso, aqui no blog temos diversos artigos voltados para design de sistemas, em especial voltados para o Domain Driven Design. Esse artigo não entra nessa saara, mas fala um pouco sobre SOLID, coesão e afins.

A essência da Orientação a Objetos

Para que uma linguagem ou plataforma seja considerada orientada a objetos ela precisa suportar 4 importantes conceitos: Abstração, Encapsulamento, Herança e Polimorfismo. Vamos falar um pouco sabre cada um deles, que demonstram a essência da Orientação a Objetos.

Abstração

Essa é a capacidade de uma estrutura de programação ser agrupada de modo a representar estruturas específicas. Quando há a possibilidade de generalizar grupos, como por exemplo, ter um objeto BaseController que representa todas as possíveis controllers (com comportamentos ou atributos gerais), estamos falando de abstração.

Além disso algumas linguagens de programação possuem palavras-reservadas específicas para lidar com esse cenário, tais como abstract, virtual, sealed ou override. Todas elas lidam com a abstração e a concretização dos elementos abstratos em itens específicos. Veja o exemplo.

// Classe abstrata que define um animal
public abstract class Animal
{
    public abstract void EmitirSom();
}

// Classe que herda da classe abstrata Animal e implementa o método EmitirSom()
public class Cachorro : Animal
{
    public override void EmitirSom()
    {
        Console.WriteLine("O cachorro late!");
    }
}

// Classe que herda da classe abstrata Animal e implementa o método EmitirSom()
public class Gato : Animal
{
    public override void EmitirSom()
    {
        Console.WriteLine("O gato mia!");
    }
}

// Classe principal que instancia objetos das classes Cachorro e Gato e chama o método EmitirSom()
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Cachorro(); // Instancia um objeto da classe Cachorro
        animal.EmitirSom(); // Chama o método EmitirSom() da classe Cachorro

        animal = new Gato(); // Instancia um objeto da classe Gato
        animal.EmitirSom(); // Chama o método EmitirSom() da classe Gato
    }
}

Encapsulamento

Quem já programou em linguagens estruturadas consegue entender com grande facilidade o problema que o encapsulamento resolve. Imagine que um código específico lida com uma entidade chamada Livro, que possui nome, autor, quantidade de páginas, sumário e ISBN. Se a aplicação tiver que manipular 10 livros ao mesmo tempo, seria necessário criar uma grande quantidade de variáveis para suportar todos os 10 livros e seus 4 atributos.

É verdade que mesmo linguagens não orientadas a objeto criam soluções de encapsulamento por conta da gravidade do problema citado. Porém as linguagens OO dão maior flexibilidade para esse tema, exatamente por se relacionar com as bases da Orientação a Objetos.

Além disso o encapsulamento dá a capacidade de uma determinada estrutura de programação possuir elementos que só são disponíveis por dentro dela. Isso normalmente é percebido através de modificadores de acesso como public, private, protected, entre outros. Veja um exemplo em C e em C#, comparando um sem OO e outro com OO, respectivamente.

#include <stdio.h>
#include <string.h>

// Definição da estrutura livro
struct livro {
    char nome[50];
    char autor[50];
    int paginas;
    char sumario[100];
    char isbn[13];
};

int main() {
    // Declaração de um livro
    struct livro livro1;
    
    // Preenchimento das informações do livro
    strcpy(livro1.nome, "O Senhor dos Anéis");
    strcpy(livro1.autor, "J.R.R. Tolkien");
    livro1.paginas = 1178;
    strcpy(livro1.sumario, "O livro conta a história de um grupo de hobbits que precisam destruir um anel para salvar a Terra-Média.");
    strcpy(livro1.isbn, "9780007117116");

    // Exibição das informações do livro
    printf("Nome: %s\n", livro1.nome);
    printf("Autor: %s\n", livro1.autor);
    printf("Páginas: %d\n", livro1.paginas);
    printf("Sumário: %s\n", livro1.sumario);
    printf("ISBN: %s\n", livro1.isbn);

    return 0;
}

Esse exemplo simples mostra um código escrito na linguagem C com o uso de struct, que é um formato de encapsulamento que já existe em linguagens mais básicas, mas que passa a ideia do problema. Agora compare com o exemplo a seguir do mesmo código escrito em C#.

using System;

// Classe livro que define as propriedades do livro
public class Livro {
    public string Nome { get; set; }
    public string Autor { get; set; }
    public int Paginas { get; set; }
    public string Sumario { get; set; }
    private string ISBN { get; set; }

    // Método construtor para inicializar as propriedades do livro
    public Livro(string nome, string autor, int paginas, string sumario) {
        Nome = nome;
        Autor = autor;
        Paginas = paginas;
        Sumario = sumario;
        ISBN = GerarISBN();
    }

    // Método privado para gerar o número de ISBN
    private string GerarISBN() {
        Random rnd = new Random();
        string isbn = "";
        for (int i = 0; i < 13; i++) {
            isbn += rnd.Next(0, 10).ToString();
        }
        return isbn;
    }

    // Método ToString() para exibir as informações do livro
    public override string ToString() {
        return $"Nome: {Nome}\nAutor: {Autor}\nPáginas: {Paginas}\nSumário: {Sumario}\nISBN: {ISBN}";
    }
}

class Program {
    static void Main(string[] args) {
        // Instanciação de um livro utilizando o método construtor
        Livro livro1 = new Livro("O Senhor dos Anéis", "J.R.R. Tolkien", 1178, "O livro conta a história de um grupo de hobbits que precisam destruir um anel para salvar a Terra-Média.");

        // Exibição das informações do livro utilizando o método ToString()
        Console.WriteLine(livro1.ToString());
    }
}

Herança

Já essa característica diz respeito a capacidade que determinadas estruturas têm de serem associadas a outras, absorvendo seus comportamentos e atributos. Com esse conceito é possível ter uma classe Animal com o método EmitirSom(); e uma classe Cachorro que herda de Animal, de modo que o método pode ou não ser reescrito pela classe herdeira.

Esse conceito dá um ganho muito grande na escrita de código uma vez que há um aumento na centralização. Os conceitos de abstração e encapsulamento se somam a esse criando um estrutura muito robusta, não podendo ser vista em abordagens não OO.

using System;

// Definição da classe base Animal
class Animal {
    public string Nome { get; set; }
    public int Idade { get; set; }
    public string Som { get; set; }

    // Construtor da classe Animal
    public Animal(string nome, int idade, string som) {
        Nome = nome;
        Idade = idade;
        Som = som;
    }

    // Método da classe Animal
    public void FazerSom() {
        Console.WriteLine($"{Nome} faz {Som}");
    }
}

// Definição da classe derivada Cachorro que herda da classe base Animal
class Cachorro : Animal {
    public string Raca { get; set; }

    // Construtor da classe Cachorro
    public Cachorro(string nome, int idade, string som, string raca) : base(nome, idade, som) {
        Raca = raca;
    }

    // Método da classe Cachorro que sobrescreve o método FazerSom() da classe base
    public new void FazerSom() {
        Console.WriteLine($"{Nome}, o cachorro da raça {Raca}, faz {Som}");
    }
}

// Classe principal Program
class Program {
    static void Main(string[] args) {
        // Criação de um objeto da classe Animal
        Animal animal = new Animal("Animal", 3, "um som qualquer");
        animal.FazerSom(); // Saída: Animal faz um som qualquer

        // Criação de um objeto da classe Cachorro
        Cachorro cachorro = new Cachorro("Totó", 2, "latido", "Vira-lata");
        cachorro.FazerSom(); // Saída: Totó, o cachorro da raça Vira-lata, faz latido
    }
}

Polimorfismo

Por fim o conceito de polimorfismo é o mais sofisticado e elegante da orientação a objetos. Com ele é possível que estruturas distintas possam assumir um padrão. A aplicação principal pode utilizar como referência o padrão ao invés de uma estrutura específica, reduzindo fortemente o acomplamento entre elas.

Mas há também um outro cenário onde estruturas mais abstratas possuem herdeiros. É possível lidar com as estruturas abstratas como se fossem as estruturas herdeiras. Em ambos os cenários temos polimorfismo. Veja exemplos de cada caso.

using System;

// Definição da interface IPagamento
interface IPagamento {
    void Pagar(double valor);
}

// Definição da classe PagamentoCartao que implementa a interface IPagamento
class PagamentoCartao : IPagamento {
    public void Pagar(double valor) {
        Console.WriteLine($"Pagamento de R${valor} realizado com cartão.");
    }
}

// Definição da classe PagamentoBoleto que implementa a interface IPagamento
class PagamentoBoleto : IPagamento {
    public void Pagar(double valor) {
        Console.WriteLine($"Pagamento de R${valor} realizado com boleto.");
    }
}

// Classe principal Program
class Program {
    static void Main(string[] args) {
        // Criação de uma lista de pagamentos, usando a interface IPagamento
        IPagamento[] pagamentos = new IPagamento[2];
        pagamentos[0] = new PagamentoCartao();
        pagamentos[1] = new PagamentoBoleto();

        // Realiza os pagamentos da lista, usando polimorfismo
        foreach (IPagamento p in pagamentos)
            p.Pagar(50);


    }
}

Nesse exemplo há o uso da interface IPagamento. Ela possui um contrato que obriga as classes que a implementam a ter o método void Pagar(double valor); Desse modo, a aplicação pode listar itens do tipo IPagamento e pagar cada um deles, sem se preocupar com quem é a classe em específico.

Agora vamos ver um segundo exemplo que demonstra o polimorfismo por meio de classes abstratas ao invés de interfaces.

using System;

// Definição da classe abstrata Pagamento
abstract class Pagamento {
    public abstract void Pagar(double valor);
}

// Definição da classe concreta PagamentoCartao que herda de Pagamento
class PagamentoCartao : Pagamento {
    public override void Pagar(double valor) {
        Console.WriteLine($"Pagamento de R${valor} realizado com cartão.");
    }
}

// Definição da classe concreta PagamentoBoleto que herda de Pagamento
class PagamentoBoleto : Pagamento {
    public override void Pagar(double valor) {
        Console.WriteLine($"Pagamento de R${valor} realizado com boleto.");
    }
}

// Classe principal Program
class Program {
    static void Main(string[] args) {
        // Criação de uma lista de pagamentos, usando a classe abstrata Pagamento
        Pagamento[] pagamentos = new Pagamento[2];
        pagamentos[0] = new PagamentoCartao();
        pagamentos[1] = new PagamentoBoleto();

        // Realiza os pagamentos da lista, usando polimorfismo
        foreach (Pagamento p in pagamentos)
            p.Pagar(50);

        
    }
}

Coesão e Coerência

Compreendidas as bases da orientação a objetos, note que quando há um conjunto distinto de objetos que interagem, a estruturação deles é algo fundamental para uma construção de um sistema. É possível que objetos estejam bem definidos por dentro mas a relação entre eles não, ou vice-versa.

Entendemos por coesa a estrutura que de maneira micro está bem definida e bem escrita. Consideramos que as partes de um todo fazem do melhor modo o que se propõe. Já, do extremo oposto, há a coerência (uma visão macro), que é a boa relação entre as estruturas micro. Enquanto a coesão diz mais respeito a forma em que a estrutura se apresenta, a coerência diz respeito a intensão. Então que a coerência sustenta que há uma qualidade perceptível em uma definição sistêmica com lógica e harmonia.

Alta coesão baixo acoplamento

Outro ponto relevante é a relação entre a coesão e o acoplamento. Esse conceito está na essência da Orientação a Objetos. A coesão é um elemento fundamental, uma vez que no paradigma OO os elementos são módulos, alguns mais micro e outros mais macro. Entenda que uma classe, por exemplo, deve ter um propósito e uma estrutura que a deixe auto-contida. Mas agrupamentos maiores de classes também, ou mesmo camadas, pacotes, e grupos ainda maiores.

É importante entender que há diferentes níveis ou tipos de coesão. E que um sistema será mais coeso em uma parte e menos em outro. Cabe ao arquiteto decidir onde deixar mais ou menos coeso. Entretanto entenda que sistemas mais coesos geram códigos mais testáveis e manuteníveis. De modo geral isso gera mais valor no curto, médio ou longo prazo do projeto de software.

Por outro lado, um sistema com muitos agrupamentos em níveis de granulação diferentes têm muitas relações. Essas relações criam acoplamento que também se manifestam de modos diferentes. Esteja certo de que o acomplamento também é algo obrigatório e necessário, cabendo ao arquiteto decidir onde e de que modo eles se dão. Entretanto, quanto menor o acoplamento mais simples é manter um sistema, crescer e suportar mudanças, além de dar mais testabilidade a ele.

Entropia de software

O termo entropia vem da física, mais específicamente da segunda lei da termodinâmica. Ela diz que em um sistema fechado, a entropia (ou desordem) sempre aumenta com o tempo. Isso significa que a energia em um sistema fechado nunca é totalmente convertida em trabalho útil, e que sempre haverá perda de energia na forma de calor.

Podemos considerar, para fins didáticos, como entropia o grau de desordem de um dado sistema. Note que como ele aumenta progressivamente com o tempo é possível supor como foi o passado de um sistema físico ao ve-lo num determinado instante.

Como esse é um tópico que extrapola a TI por entrar na física, se for de seu interesse entender melhor o tema, veja o vídeo a seguir.

E por isso, Ivan Jackbsen et al. em 1992 cunharam o termo Entropia de Software. A ideia é que um sistema de computador possui um nível de desordem (nível de acoplamento e coesão nas suas mais diversas formas). E sua desordem fará com que ele progressivamente se desorganize cada vez mais. Nesse cenário adicionar novos requisitos sem refatoração aumentará o seu custo por adquirir débitos técnicos.

Estruturas de dados, patterns, system design e arquiteturas

Falamos das bases, da coesão e coerência, da relação entre alta coesão e baixo acoplamento e sobre a entropia de software. Estamos focando esse artigo em elementos de design de software. Entenda por design um determinado conceito ou conjunto de conceitos que amparam o bom desenvolvimento de sistemas. Já Arquitetura é o conjunto de práticas de codificação utilizados para o bom desenvolvimento de sistemas. Portanto é facil notar que arquitetura herda de design.

Pois bem, a orientação a objetos é um paradigma que define que as estruturas devem ser: abstraíveis, encapsuláveis, herdáveis e polimórficas. Isso se sintetiza nas classes (ou objetos). Esses objetos se dispões para atender um dado problema. Quanto mais classes mais complexo é o sistema e, com isso, o acoplamento tende a ser maior e a coesão tende a ser menor. Além disso, a entropia progressivamente aumenta.

Para mitigar tais problemas alguns princípios de design foram criados. Robert Martin (o uncle Bob) sintetizou práticas através do acrônimo SOLID que ajudam um software a ser menos acoplado e mais coeso. Além disso, aqui no blog temos um artigo sobre Domain Driven Design estratégico e Domain Driven Design tático, que são abordagens específicas para suportar todos esses conceitos.

Conclusão de a essência da Orientação a Objetos

Esse artigo é uma síntese da essência da orientação a objetos. Ele traz as bases conceituais associadas ao paradigma, mas não apenas as ideias de abstração, encapsulamento, etc. mas as consequências do seu uso. Por conta disso vemos, de modo geral, conceitos como Entropia de Software, Acoplamento e Coesão, e um pouquinho de código por que ninguém é de ferro.


Thiago Anselme
Thiago Anselme - Gerente de TI - Arquiteto de Soluções

Ele atua/atuou como Dev Full Stack C# .NET / Angular / Kubernetes e afins. Ele possui certificações Microsoft MCTS (6x), MCPD em Web, ITIL v3 e CKAD (Kubernetes) . Thiago é apaixonado por tecnologia, entusiasta de TI desde a infância bem como amante de aprendizado contínuo.

Deixe um comentário