Orientação a objetos: Introdução e guia de estudos

Orientação a objetos é um paradigma de programação que organiza o software em unidades chamadas objetos, que reúnem dados e comportamentos e interagem entre si. Neste artigo, você vai entender o conceito, conhecer seus quatro pilares e ver como cada um deles aparece em código C# real.

O que é orientação a objetos

De forma direta, a orientação a objetos propõe que um programa seja modelado como um conjunto de objetos que colaboram. Cada objeto guarda o próprio estado e expõe as operações que fazem sentido sobre esse estado.

Por isso, em vez de pensar em uma sequência de funções que manipulam variáveis soltas, você passa a pensar em entidades do domínio. Um aluno, um pagamento ou um pedido deixam de ser apenas dados e passam a ter regras próprias.

Essa definição costuma parecer abstrata demais na primeira leitura. No entanto, ela fica clara quando comparamos um código sem organização com outro que aplica os conceitos do paradigma.

Além disso, vale destacar que a orientação a objetos não pertence a uma linguagem específica. C#, Java, TypeScript, Dart, Python e Kotlin aplicam os mesmos fundamentos, ainda que com sintaxes diferentes.

Na prática, isso significa que o tempo investido nesse estudo não se perde com a próxima moda do mercado. Os conceitos que você aprende hoje continuam válidos por muitos anos.

Por que a orientação a objetos ainda importa

O paradigma surgiu nos anos 1960 com a linguagem Simula e ganhou força com o Smalltalk na década seguinte. Desde então, ele se tornou a base das principais linguagens e frameworks usados nas empresas.

Mesmo assim, é comum ouvir que a orientação a objetos está ultrapassada. Na verdade, o que mudou foi a forma de aplicá-la, porque hoje combinamos objetos com recursos funcionais, como expressões lambda e tipos imutáveis.

O C#, por exemplo, é uma linguagem orientada a objetos desde a primeira versão. Ao mesmo tempo, ela incorporou records, pattern matching e LINQ, que convivem muito bem com classes e interfaces.

Além disso, boa parte dos padrões de projeto, dos princípios SOLID e das abordagens de arquitetura parte desse paradigma. Portanto, dominar a orientação a objetos é o pré-requisito para entender praticamente todo o resto.

Em outras palavras, ela funciona como a base do programador. Quem domina esses fundamentos lê código alheio com mais facilidade, conversa melhor com o time e toma decisões de design mais seguras.

Orientação a objetos e outros paradigmas

Um paradigma é um modelo que orienta a forma de estruturar um programa. Além do orientado a objetos, os mais conhecidos são o procedural e o funcional.

No paradigma procedural, o programa é uma sequência de instruções e funções que manipulam dados compartilhados. Essa abordagem funciona bem em scripts curtos, mas tende a se espalhar quando o sistema cresce.

Já no paradigma funcional, o foco está em funções puras e dados imutáveis. Assim, o código evita efeitos colaterais e fica mais previsível em cenários de concorrência.

A orientação a objetos, por sua vez, coloca dados e regras no mesmo lugar. Com isso, cada parte do sistema passa a ter um dono claro e responsabilidades bem definidas.

Na prática, as linguagens modernas misturam essas abordagens sem conflito. Em um mesmo projeto C#, por exemplo, você modela o domínio com classes e consulta coleções com LINQ, que tem forte inspiração funcional.

Classes e objetos na prática

Antes de chegar aos pilares, precisamos separar dois termos que aparecem o tempo todo. A classe é o molde, enquanto o objeto é a instância criada a partir desse molde.

Uma analogia comum é a planta de uma casa. A planta descreve cômodos e medidas, mas você só mora na casa construída a partir dela, e várias casas podem sair da mesma planta.

public class Student
{
    public string Name { get; }
    public int Age { get; }

    public Student(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

// Dois objetos diferentes criados a partir da mesma classe
var maria = new Student("Maria", 34);
var john = new Student("John", 17);

Nesse exemplo, Student é a classe, e maria e john são dois objetos distintos. Cada um carrega os próprios valores, embora ambos sigam a mesma estrutura.

Em C#, os dados de uma classe normalmente ficam em propriedades, e os comportamentos ficam em métodos. Além disso, o construtor garante que todo objeto nasça em um estado válido.

Os quatro pilares da orientação a objetos

A literatura costuma resumir a orientação a objetos em quatro pilares: encapsulamento, herança, abstração e polimorfismo. A própria documentação oficial do C# na Microsoft Learn apresenta o paradigma a partir desses mesmos quatro conceitos.

Esses pilares não funcionam de forma isolada. Pelo contrário, eles se complementam, e um bom design usa todos ao mesmo tempo, cada um resolvendo uma parte do problema.

Para tornar os exemplos concretos, vamos usar dois cenários. Primeiro, um cadastro de alunos; em seguida, um sistema de pagamentos com boleto e cartão de crédito.

Encapsulamento

O encapsulamento consiste em agrupar dados e comportamentos relacionados dentro de uma mesma unidade. Além disso, ele controla quem pode ler ou alterar esse estado.

Para entender o problema que ele resolve, observe o código abaixo, escrito sem nenhuma organização.

// Estado solto, sem dono definido
int age = 34;

bool IsAdult() => age >= 18;

void ChangeAge(int newAge)
{
    age = newAge;
}

Nesse cenário, a variável age fica solta e qualquer trecho do programa pode alterá-la. À medida que o sistema cresce, fica difícil saber quem modifica esse valor e a que entidade ele pertence.

Aplicando o encapsulamento, reunimos tudo o que faz sentido estar junto em uma classe.

public class Student
{
    public int Age { get; private set; }

    public Student(int age)
    {
        ChangeAge(age);
    }

    public bool IsAdult() => Age >= 18;

    public void ChangeAge(int newAge)
    {
        // Toda alteração passa pela validação
        if (newAge < 0)
            throw new ArgumentException("Age cannot be negative.");

        Age = newAge;
    }
}

Agora a idade pertence claramente ao aluno. Além disso, o private set impede alterações externas, então toda mudança passa pelo método ChangeAge, que valida o valor.

Com isso, o objeto protege a própria consistência. Nenhum código externo consegue colocar o aluno em um estado inválido, e as regras ficam concentradas em um único lugar.

Herança

A herança permite que uma classe reaproveite e estenda o comportamento de outra. A classe original recebe o nome de classe base, e a nova classe recebe o nome de classe derivada.

Assim como herdamos características dos nossos pais, uma classe derivada herda propriedades e métodos da sua classe base. Contudo, ela também pode acrescentar membros próprios.

Vamos partir de uma classe simples de pagamento, que guarda apenas a data de vencimento.

public class Payment
{
    public DateTime DueDate { get; init; }
}

Um pagamento, porém, pode acontecer por boleto, cartão de crédito, Pix ou outros meios que ainda vão surgir. Tratar todas essas variações dentro da mesma classe deixaria o código grande e difícil de manter.

Por isso, criamos classes especializadas que herdam de Payment.

// Boleto bancário
public class BoletoPayment : Payment
{
    public string BarCode { get; init; } = string.Empty;
}

// Cartão de crédito
public class CreditCardPayment : Payment
{
    public string TransactionNumber { get; init; } = string.Empty;
}

Dessa forma, BoletoPayment possui tanto o vencimento quanto o código de barras. Ou seja, a classe reaproveita o que já existe e acrescenta apenas o que é específico do boleto.

Vale lembrar que, no C#, uma classe herda diretamente de apenas uma classe base. Entretanto, ela pode implementar várias interfaces, como veremos mais adiante.

Abstração

A abstração significa expor apenas o essencial e esconder os detalhes de implementação. Em outras palavras, quanto menos o mundo externo souber sobre o funcionamento interno de uma classe, mais fácil será consumi-la e alterá-la.

Observe uma primeira versão do boleto, que expõe tudo o que possui.

public class BoletoPayment : Payment
{
    public int CheckDigit;
    public string BarCode { get; init; } = string.Empty;

    public int CalculateCheckDigit()
    {
        // regra de cálculo do dígito verificador
        return 0;
    }
}

Nesse caso, o campo CheckDigit e o método CalculateCheckDigit ficam visíveis para qualquer código. A pergunta que precisamos fazer é simples: alguém fora do boleto realmente precisa deles?

Na maioria dos sistemas, a resposta é não. Além disso, se outras partes do código passarem a depender desses membros, qualquer mudança no cálculo vai provocar uma refatoração em cascata.

Por isso, escondemos o que é detalhe interno.

public class BoletoPayment : Payment
{
    private int _checkDigit;
    public string BarCode { get; init; } = string.Empty;

    private int CalculateCheckDigit()
    {
        // regra de cálculo do dígito verificador
        return 0;
    }
}

Com isso, a classe oferece uma superfície pública menor e mais estável. Assim, mudanças no cálculo do dígito afetam apenas o próprio boleto.

Repare que abstração e encapsulamento caminham juntos. Enquanto o encapsulamento agrupa e protege, a abstração decide o que merece ficar visível, tema que aprofundamos no artigo sobre abstração com interfaces e modificadores de acesso.

Polimorfismo

O termo vem do grego, em que "poli" significa muitos e "morfo" significa formas. Na orientação a objetos, ele permite que objetos de tipos diferentes respondam à mesma chamada, cada um à sua maneira.

Voltando aos pagamentos, todos compartilham uma pergunta: este pagamento ainda pode ser pago? A regra padrão diz que, passado o vencimento, o pagamento não pode mais ser quitado.

No entanto, o boleto tem uma exceção conhecida. Quando o vencimento cai em um fim de semana ou feriado, ele pode ser pago no próximo dia útil.

Não queremos duplicar a regra padrão em todas as classes. Ao mesmo tempo, precisamos sobrescrevê-la no caso do boleto, e o C# resolve isso com virtual e override.

public class Payment
{
    public DateTime DueDate { get; init; }

    // Regra padrão, que as classes derivadas podem sobrescrever
    public virtual bool CanBePaid(DateTime today)
        => today.Date <= DueDate.Date;
}

public class BoletoPayment : Payment
{
    public override bool CanBePaid(DateTime today)
        => today.Date <= NextBusinessDay(DueDate).Date;

    private static DateTime NextBusinessDay(DateTime date)
    {
        // Simplificação: considera apenas fins de semana
        while (date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
            date = date.AddDays(1);

        return date;
    }
}

Agora, o código que consome os pagamentos não precisa saber qual tipo está tratando.

List<Payment> payments =
[
    new CreditCardPayment { DueDate = new DateTime(2026, 10, 3) },
    new BoletoPayment { DueDate = new DateTime(2026, 10, 3) }
];

foreach (var payment in payments)
    Console.WriteLine(payment.CanBePaid(new DateTime(2026, 10, 5)));

Nesse exemplo, o vencimento cai em um sábado e o pagamento acontece na segunda. Portanto, o cartão retorna false, enquanto o boleto retorna true, porque o runtime executa a versão correta de cada objeto.

Esse é o ponto central do polimorfismo. Você escreve o código uma vez contra o tipo base, e cada classe derivada contribui com o próprio comportamento, assunto detalhado no artigo sobre sobrescrita e sobrecarga.

Herança ou composição na orientação a objetos

A herança é poderosa, mas ela também cria um acoplamento forte entre classe base e classe derivada. Por isso, uma recomendação clássica do livro Design Patterns é preferir composição em vez de herança.

Na composição, um objeto recebe outros objetos que executam parte do trabalho. Assim, você troca comportamentos sem precisar criar uma nova hierarquia de classes.

public interface IDueDateRule
{
    bool CanBePaid(DateTime dueDate, DateTime today);
}

// A regra de vencimento chega pelo construtor primário
public class Payment(IDueDateRule rule)
{
    public DateTime DueDate { get; init; }

    public bool CanBePaid(DateTime today)
        => rule.CanBePaid(DueDate, today);
}

Nessa versão, a regra de vencimento vira uma dependência. Dessa forma, você pode criar regras diferentes, testá-las isoladamente e combiná-las com qualquer tipo de pagamento.

Além disso, as interfaces também oferecem polimorfismo. Qualquer classe que implemente IDueDateRule pode substituir outra, sem que o Payment precise mudar.

Esse desenho conversa diretamente com a injeção de dependência. Afinal, quem monta o objeto decide qual regra entregar, e o pagamento apenas confia no contrato.

Na prática, use herança quando existe uma relação clara do tipo "é um", como um boleto que é um pagamento. Por outro lado, prefira composição quando a relação for "usa um" ou quando o comportamento precisar variar com frequência.

Erros comuns ao aplicar orientação a objetos

Conhecer os pilares não garante um bom design. Por isso, vale observar os erros que aparecem com mais frequência em projetos reais.

O primeiro é o chamado modelo anêmico. Nele, as classes têm apenas propriedades públicas com get e set, enquanto toda a regra de negócio fica espalhada em serviços externos.

Nesse caso, a orientação a objetos existe só na aparência. Os objetos viram meros carregadores de dados, e o encapsulamento desaparece.

O segundo erro é criar hierarquias de herança muito profundas. Quando uma classe depende de quatro ou cinco níveis acima dela, qualquer mudança na base se torna arriscada.

Em alguns casos, a melhor saída é impedir a herança de propósito. O C# oferece o modificador sealed para isso, e o artigo sobre classes seladas mostra quando ele faz sentido.

O terceiro erro é expor membros demais. Campos públicos, métodos auxiliares visíveis e coleções que podem ser alteradas por fora quebram a abstração e aumentam o acoplamento.

Por fim, existe o excesso oposto, que é criar classes e interfaces para tudo. Afinal, o objetivo do paradigma é simplificar o entendimento do domínio, e não adicionar camadas sem propósito.

Uma boa forma de evitar esses problemas é estudar os princípios SOLID logo depois dos pilares. Eles funcionam como um guia prático para aplicar a orientação a objetos com equilíbrio.

Como continuar estudando

Como o paradigma é atemporal, muitos dos melhores livros sobre o assunto são antigos. Mesmo assim, eles continuam extremamente atuais.

O primeiro é Design Patterns: Elements of Reusable Object-Oriented Software, de Gamma, Helm, Johnson e Vlissides. Ele cataloga soluções recorrentes de design e mostra como os pilares se combinam em problemas reais.

O segundo é Clean Code, de Robert C. Martin. Embora seja mais amplo, ele traz capítulos valiosos sobre classes, coesão e responsabilidades, e o resumo do Clean Code aqui no blog é um bom ponto de partida.

Além da leitura, a prática faz toda a diferença. No balta.io, o curso Modelando Domínios Ricos resolve um problema completo usando apenas orientação a objetos e SOLID, do começo ao fim.

Antes dele, vale garantir uma base sólida na linguagem com o curso de Fundamentos do C#. Em seguida, você pode avançar para testes de unidade e, depois, para a carreira de backend com .NET, todos disponíveis no catálogo de cursos do balta.io.

Também existe uma versão em vídeo deste conteúdo no YouTube, com exemplos em C#, JavaScript, TypeScript e Dart. Assim, você compara como cada linguagem expressa os mesmos conceitos.

Conclusão

A orientação a objetos organiza o software em torno de objetos que guardam estado e comportamento. Por isso, ela aproxima o código da linguagem do negócio e facilita a manutenção ao longo do tempo.

Os quatro pilares resumem essa ideia. O encapsulamento protege o estado, a herança reaproveita comportamento, a abstração esconde detalhes e o polimorfismo permite tratar tipos diferentes de forma uniforme.

Contudo, aplicar a orientação a objetos bem exige equilíbrio. Prefira composição quando a herança gerar acoplamento, evite modelos anêmicos e exponha apenas o necessário.

Por fim, lembre que esse conhecimento não depende de linguagem nem de framework. Uma vez dominado, ele acompanha você em qualquer stack e em qualquer fase da carreira.

FAQ

Qual a diferença entre classe e objeto?

A classe é o molde que define propriedades e métodos. O objeto é uma instância concreta criada a partir desse molde, com valores próprios.

Quais são os quatro pilares da orientação a objetos?

São encapsulamento, herança, abstração e polimorfismo. Juntos, eles organizam o código, reduzem o acoplamento e facilitam a evolução do sistema.

O C# é uma linguagem orientada a objetos?

Sim. O C# nasceu orientado a objetos e, com o tempo, incorporou recursos funcionais como LINQ, records e pattern matching.

Encapsulamento e abstração são a mesma coisa?

Não. O encapsulamento agrupa e protege dados e comportamentos, enquanto a abstração decide quais detalhes ficam visíveis para quem consome a classe.

Devo sempre usar herança?

Não. Use herança quando existir uma relação clara do tipo "é um" e prefira composição quando o comportamento precisar variar ou ser testado isoladamente.

Preciso aprender orientação a objetos antes de SOLID?

Sim. Os princípios SOLID partem dos pilares do paradigma, então entender classes, herança e polimorfismo torna o estudo muito mais simples.

Glossário

Termo Significado
Paradigma Modelo ou estilo que orienta a forma de estruturar um programa
Classe Molde que define as propriedades e os métodos de um tipo
Objeto Instância concreta de uma classe, com estado próprio
Propriedade Membro de uma classe que expõe ou armazena um dado
Método Função declarada dentro de uma classe que representa um comportamento
Construtor Método especial executado na criação do objeto
Encapsulamento Agrupamento de dados e comportamentos com controle de acesso
Herança Mecanismo pelo qual uma classe reaproveita e estende outra
Abstração Exposição apenas do essencial, escondendo detalhes de implementação
Polimorfismo Capacidade de tipos diferentes responderem à mesma chamada de formas distintas
Composição Construção de um objeto a partir de outros objetos que executam parte do trabalho
Interface Contrato que define membros que uma classe deve implementar
virtual / override Palavras-chave do C# que permitem sobrescrever um método em uma classe derivada
Modelo anêmico Classes com dados e sem regras de negócio, que ficam em serviços externos

Fontes