Orientação a objetos em C#: entenda de forma simples e prática
Se você está começando na carreira como desenvolvedor C#, dominar orientação a objetos é um passo fundamental.
Esse paradigma de programação não é apenas uma forma de codificar— é uma maneira de pensar o código de forma mais organizada, reutilizável e escalável.
Neste artigo, você vai entender como aplicar, na prática, os quatro pilares da orientação a objetos em C# com exemplos diretos e comentados.
O que é orientação a objetos e por que ela é fundamental em C#
De maneira simples, a orientação a objetos (OO) organiza o software em objetos — pequenas unidades que representam entidades do mundo real ou conceitos abstratos.
Cada objeto combina atributos (dados) e métodos (comportamentos), tornando o código mais próximo da lógica humana e mais fácil de manter.
Em C#, a OO é parte da base da linguagem.
Ao dominar esses conceitos, você ganha clareza, modularidade e capacidade de expansão do código — habilidades fundamentais para o mercado de trabalho.
Classes e objetos: a base da orientação a objetos em C#
Toda aplicação orientada a objetos começa com classes e objetos.
Uma classe define o modelo — a estrutura e o comportamento — enquanto o objeto é a instância real que atua no código.
Veja este exemplo com a classe Pessoa:
// Instancia um novo objeto da classe Pessoa
Pessoa pessoa1 = new Pessoa();
// Define o valor da propriedade Nome
pessoa1.Nome = "Carlos";
// Define o valor da propriedade Idade
pessoa1.Idade = 29;
// Chama o método Apresentar() que exibe a mensagem no console
pessoa1.Apresentar();
// Classe que representa uma pessoa com propriedades básicas
public class Pessoa
{
// Propriedade para armazenar o nome
public string Nome { get; set; }
// Propriedade para armazenar a idade
public int Idade { get; set; }
// Método para apresentar uma pessoa
public void Apresentar()
{
Console.WriteLine($"Olá, meu nome é {Nome} e tenho {Idade} anos.");
}
}
No código acima, cada objeto criado a partir da classe Pessoa pode ter valores diferentes. Seria possível criar várias instâncias chamadas de pessoa2, pessoa3, etc, utilizando o mesmo modelo — ou seja, a classe Pessoa. É isso que torna o código flexível e dinâmico.
Abstração: focando no essencial
A abstração permite esconder detalhes complexos e trabalhar apenas com o que é realmente importante.
Em C#, isso é feito com classes abstratas e interfaces — estruturas que definem o que deve ser feito, sem especificar como.
// Cria um retângulo e calcula sua área
Retangulo retangulo = new Retangulo();
retangulo.Largura = 5.0;
retangulo.Altura = 3.0;
// Chama o método CalcularArea() e exibe o resultado
double area = retangulo.CalcularArea();
Console.WriteLine($"A área do retângulo é: {area}");
// Polimorfismo: podemos usar a classe base como tipo
Forma forma = new Retangulo { Largura = 4.0, Altura = 6.0 };
Console.WriteLine($"Área usando polimorfismo: {forma.CalcularArea()}");
public abstract class Forma
{
// Método abstrato que obriga as classes derivadas a implementarem
// o cálculo da área de acordo com suas características específicas
public abstract double CalcularArea();
}
// Classe Retangulo herda de Forma (indicado pelo símbolo :)
// Deve obrigatoriamente implementar todos os métodos abstratos da classe pai
public class Retangulo : Forma
{
// Propriedade que armazena a largura do retângulo
public double Largura { get; set; }
// Propriedade que armazena a altura do retângulo
public double Altura { get; set; }
// Implementação do método abstrato
// Override indica que estamos sobrescrevendo o método da classe base
public override double CalcularArea() => Largura * Altura;
}
No código acima, a classe Forma representa o conceito geral, enquanto Retangulo implementa os detalhes. Por se tratar de um conceito (abstração), a classe Forma não pode ser instanciada diretamente. Essa separação traz organização e clareza, principalmente em projetos grandes.
Encapsulamento: protegendo seus dados
O encapsulamento garante que os dados de um objeto sejam acessados e modificados de forma correta e segura.
Isso reduz erros e melhora a segurança do código.
Observe o seguinte código:
// Cria uma nova conta bancária
ContaBancaria conta = new ContaBancaria();
// Exibe o saldo inicial (será 0.0 por padrão)
Console.WriteLine($"Saldo inicial: R$ {conta.Saldo}");
// Realiza um depósito válido
conta.Depositar(100.50);
Console.WriteLine($"Saldo após depósito de R$ 100,50: R$ {conta.Saldo}");
// Tenta depositar um valor inválido
conta.Depositar(-50);
Console.WriteLine($"Saldo após tentar depositar R$ -50: R$ {conta.Saldo}");
// Realiza outro depósito válido
conta.Depositar(250.75);
Console.WriteLine($"Saldo final: R$ {conta.Saldo}");
// Classe que representa uma conta bancária básica
public class ContaBancaria
{
// Campo privado que armazena o saldo real da conta
// A palavra private garante que só pode ser acessado dentro da classe
private double saldo;
// Propriedade pública que controla o acesso ao saldo
public double Saldo
{
// Qualquer código pode ler o saldo
get { return saldo; }
// Set privado: somente os métodos da classe podem modificar o saldo diretamente
private set { saldo = value; }
}
// Método público para depositar dinheiro na conta
public void Depositar(double valor)
{
// Valida se o valor é positivo antes de adicionar ao saldo
if (valor > 0)
{
// Usa a propriedade Saldo para adicionar o valor
// Internamente, acessa o setter privado que modifica o campo saldo
Saldo += valor;
}
}
}
Aqui, saldo não pode ser alterado diretamente de fora da classe. Para que isso seja possível, é necessário utilizar um getter — que retorna o valor de forma indireta quando Saldo é invocado num objeto.
Além disso, não é possível atribuir qualquer valor sem antes passar pela validação do setter — o que previne que valores errados sejam atribuídos nos campos privados do objeto. Resumindo, somente os métodos da própria classe podem modificar seu valor, garantindo controle e consistência.
Herança: reaproveitando e estendendo o código
A herança permite que uma classe herde atributos e métodos de outra, evitando duplicação e facilitando a manutenção.
Em C#, usamos : para indicar herança.
Veja um exemplo:
// Cria um objeto Carro usando inicializador de objeto { }
// Esta sintaxe permite definir as propriedades durante a criação
Carro meuCarro = new Carro
{
Modelo = "Sedan", // Propriedade herdada de Veiculo
NumeroPortas = 4 // Propriedade específica de Carro
};
// Chama o método Ligar() que foi herdado da classe Veiculo
// Mesmo não estando definido na classe Carro, está disponível pela herança
meuCarro.Ligar();
// Exibe as informações do carro
Console.WriteLine($"Modelo: {meuCarro.Modelo}");
Console.WriteLine($"Número de portas: {meuCarro.NumeroPortas}");
// Demonstração adicional: criando um veículo genérico
Veiculo veiculo = new Veiculo { Modelo = "Genérico" };
veiculo.Ligar();
// veiculo.NumeroPortas causaria erro - propriedade não existe em Veiculo
// Classe base que representa um veículo genérico
public class Veiculo
{
// Propriedade que armazena o modelo do veículo
public string Modelo { get; set; }
// Método que simula a ação de ligar o veículo
public void Ligar() => Console.WriteLine("Veículo ligado!");
}
// Classe Carro herda todas as propriedades e métodos públicos da classe Veiculo
public class Carro : Veiculo
{
// Propriedade específica da classe Carro
// Não existe na classe base Veiculo
public int NumeroPortas { get; set; }
}
No exemplo acima, Veiculo é utilizada como base pela classe que a herda — nesse caso Carro. Isso implica dizer que o que já foi implementado na superclasse (Veiculo) pode ser aproveitado diretamente.
Em outras palavras, com a herança, você pode criar uma hierarquia de classes bem estruturada — reutilizando código e facilitando a evolução do sistema.
Polimorfismo: comportamento dinâmico e flexível
O polimorfismo permite tratar objetos diferentes de forma uniforme, mantendo a flexibilidade do código.
Em C#, isso é feito com override ou sobrecarga de métodos.
// Demonstração de polimorfismo:
// Declaramos a variável como tipo Animal (classe base),
// mas instanciamos um objeto Cachorro (classe derivada)
Animal meuCachorro = new Cachorro();
// Ao chamar EmitirSom(), é usado o método da classe REAL do objeto (Cachorro)
// e não da classe declarada (Animal)
meuCachorro.EmitirSom(); // Saída: Au Au!
// Comparação: criando um animal genérico
Animal animal = new Animal();
animal.EmitirSom(); // Saída: Som genérico
// Array de animais com polimorfismo
Animal[] animais = new Animal[]
{
new Animal(), // Som genérico
new Cachorro(), // Au Au!
new Cachorro() // Au Au!
};
Console.WriteLine("\nDemonstrando polimorfismo com múltiplos animais:");
foreach (Animal a in animais)
{
a.EmitirSom();
}
// Classe base que representa um animal genérico
public class Animal
{
// Método virtual que pode ser sobrescrito pelas classes derivadas
// A palavra-chave 'virtual' permite que classes filhas tenham sua própria implementação
public virtual void EmitirSom() => Console.WriteLine("Som genérico");
}
// Classe Cachorro herda de Animal
public class Cachorro : Animal
{
// Sobrescreve o método EmitirSom() da classe base
// 'override' indica a substituição da implementação do método virtual da classe pai
public override void EmitirSom() => Console.WriteLine("Au Au!");
}
No código acima, o método virtual EmitirSom() da classe Animal pode ser sobrescrito (ou alterado) para que sirva os propósitos da classe Cachorro, que, por sua vez, utiliza a palavra-chave override para fazer a mudança necessária.
Dito de outro modo, o polimorfismo torna o código adaptável — o que garante fazer pequenos ajustes quando necessário, mas sem alterar toda a estrutura de uma classe.
Exemplo prático: mini sistema de funcionários
Veja como os quatro pilares da orientação a objetos em C# trabalham juntos neste exemplo:
// POLIMORFISMO: Mesma variável tipo base, objetos diferentes
Funcionario f1 = new Gerente { Nome = "Alice", Bonus = 1000 };
Funcionario f2 = new Vendedor { Nome = "Marcos", Comissao = 500 };
// POLIMORFISMO: Mesmo método, comportamentos diferentes
f1.ExibirInfo(); // Chama a versão de Gerente
f2.ExibirInfo(); // Chama a versão de Vendedor
Console.WriteLine($"\nTotal da folha: R${(f1.CalcularSalario() + f2.CalcularSalario()):F2}");
public abstract class Funcionario
{
// ENCAPSULAMENTO: Campo privado com propriedade pública
private string _nome;
public string Nome
{
get => _nome;
set
{
if (!string.IsNullOrWhiteSpace(value))
_nome = value;
}
}
// ABSTRAÇÃO: Método abstrato - cada tipo implementa sua lógica
public abstract decimal CalcularSalario();
// Método para exibir informações (pode ser sobrescrito)
public virtual void ExibirInfo()
{
Console.WriteLine($"{Nome} recebe R${CalcularSalario():F2}");
}
}
// HERANÇA: Gerente herda de Funcionario
public class Gerente : Funcionario
{
// ENCAPSULAMENTO: Controla acesso ao bônus
private decimal _bonus;
public decimal Bonus
{
get => _bonus;
set => _bonus = value >= 0 ? value : 0;
}
// POLIMORFISMO: Implementação específica de CalcularSalario()
public override decimal CalcularSalario() => 5000 + Bonus;
// POLIMORFISMO: Sobrescreve comportamento do método base
public override void ExibirInfo()
{
Console.WriteLine($"{Nome} (Gerente) recebe R${CalcularSalario():F2} " +
$"(Salário: R${CalcularSalario()-Bonus:F2} + Bônus: R${Bonus})");
}
}
// HERANÇA: Vendedor herda de Funcionario
public class Vendedor : Funcionario
{
// ENCAPSULAMENTO: Controla o acesso à comissão
private decimal _comissao;
public decimal Comissao
{
get => _comissao;
set => _comissao = value >= 0 ? value : 0;
}
// POLIMORFISMO: Implementação específica de CalcularSalario
public override decimal CalcularSalario() => 3000M + Comissao;
// POLIMORFISMO: Sobrescreve comportamento do método base
public override void ExibirInfo()
{
Console.WriteLine($"{Nome} (Vendedor) recebe R${CalcularSalario():F2} " +
$"(Salário: R${CalcularSalario()-Comissao:F2} + Comissão: R${Comissao})");
}
}
O mini sistema acima demonstra os conceitos da orientação a objetos de forma integrada — exatamente como acontece em aplicações reais. Observe como os quatro conceitos são orquestrados para que o código seja reutilizado e protegido ao mesmo tempo.
Dicas práticas para dominar orientação a objetos em C#
- Comece pequeno: pratique com classes simples e depois evolua para sistemas completos.
- Use propriedades em vez de campos públicos: isso mantém o controle sobre os dados.
- Abstraia comportamentos comuns: use interfaces para definir contratos claros.
- Analise projetos reais: explore repositórios no GitHub com código C# orientado a objetos.
- Refatore com frequência: OO é sobre evolução constante do design.
Conclusão: transforme a orientação a objetos em seu diferencial
Dominar os pilares da orientação a objetos em C# é mais do que entender teoria — é dar o próximo passo na sua jornada como desenvolvedor.
Quando você aplica abstração, encapsulamento, herança e polimorfismo, o código deixa de ser apenas funcional e passa a refletir a sua forma de pensar: organizada, clara e profissional.
Cada linha escrita é uma chance de evoluir. Cada erro, uma lição. Cada refatoração, um avanço.
💡 Não espere o momento perfeito — comece agora.
Pegue um projeto, aplique um conceito e veja como o seu código (e você) podem se transformar a cada dia.
GitHub: https://github.com/CarlosDi96/artigos-exemplos-de-codigo/tree/main/POOBasico