Entendendo a Programação Orientada a Objetos em C#
Herança e interfaces são partes integrantes da programação orientada a objetos (POO). Tim Corey, em seu vídeo " Herança vs. Interfaces em C#: Programação Orientada a Objetos ", oferece uma explicação detalhada sobre quando usar herança e quando optar por interfaces.
Este artigo serve como um guia completo para o vídeo de Tim Corey. Este guia detalha os principais conceitos, exemplos e explicações de código apresentados no vídeo, destacando as diferenças entre herança e interfaces e quando usar cada uma.
Introdução
Tim, em (0:00), começa destacando a importância de distinguir entre herança e interfaces. Ele enfatiza a necessidade de compreender quando usar cada conceito para alcançar os melhores resultados. Seu objetivo é demonstrar isso por meio de exemplos, começando com o uso incorreto da herança simples e, em seguida, corrigindo-o.
Criando o Projeto
Em (1:08), Tim cria um aplicativo de console simples usando .NET 5. Ele nomeia o projeto de "OODemoApp" e explica que o objetivo principal é demonstrar os conceitos em vez de criar código pronto para produção.
Entendendo a Herança
Tim em (1:55) aprofunda-se nos princípios básicos da herança. Ele define herança como um mecanismo no qual as propriedades e os métodos de uma classe base são herdados por uma classe derivada. Ele enfatiza que a herança não deve ser usada meramente para reutilização e compartilhamento de código, mas para estabelecer uma relação lógica de "é um/uma".
Pontos principais:
- Relação "É um" : Garante que a classe derivada seja um tipo da classe base.
- Lógica comum : Classes que herdam de outras classes devem compartilhar uma lógica comum, e não apenas propriedades ou assinaturas de métodos.
Classe de exemplo: Carro de aluguel
Tim em (7:52) cria uma classe RentalCar para ilustrar o conceito fundamental de herança. Esta categoria representa um carro alugado em uma locadora de veículos em Miami, Flórida.
public class RentalCar
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public int NumberOfPassengers { get; set; }
public void StartEngine()
{
Console.WriteLine("Turn key to ignition setting");
Console.WriteLine("Turn key to on");
}
public void StopEngine()
{
Console.WriteLine("Turn key to off");
}
}
public class RentalCar
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public int NumberOfPassengers { get; set; }
public void StartEngine()
{
Console.WriteLine("Turn key to ignition setting");
Console.WriteLine("Turn key to on");
}
public void StopEngine()
{
Console.WriteLine("Turn key to off");
}
}
Demonstrando as Armadilhas da Herança Incorreta
Tim, às 10h15, explica como o uso inadequado da herança pode levar a problemas. Ele destaca que, se a herança for mal utilizada, pode levar a um código difícil de gerenciar e estender. Ele desaconselha o uso de herança apenas para compartilhar código.
Adicionando Enums para Tipos de Carros e Caminhões
Tim, às 10h45, adiciona uma enumeração para tipos de carros. Ele cria um novo arquivo de classe chamado Enums.cs para manter todos os enums em um só lugar. Esta enumeração ajudará a diferenciar entre os diferentes estilos de carros.
// Enums.cs
public enum CarType
{
Hatchback,
Sedan,
Compact
}
// Enums.cs
public enum CarType
{
Hatchback,
Sedan,
Compact
}
Em seguida, ele adiciona uma propriedade à classe RentalCar para especificar o tipo de carro.
public class RentalCar : RentalVehicle
{
public CarType Style { get; set; }
// Other properties and methods
}
public class RentalCar : RentalVehicle
{
public CarType Style { get; set; }
// Other properties and methods
}
Expandindo para incluir caminhões
Como Tim explica em (12:27), a agência de aluguel decide adicionar caminhões à sua frota, o que introduz novos requisitos. Ele cria uma classe RentalTruck que herda da classe pai RentalVehicle.
public class RentalTruck : RentalVehicle
{
public TruckType Style { get; set; }
// Other properties and methods
}
public class RentalTruck : RentalVehicle
{
public TruckType Style { get; set; }
// Other properties and methods
}
Em seguida, ele define um novo enum para tipos de caminhão.
// Enums.cs
public enum TruckType
{
ShortBed,
LongBed
}
// Enums.cs
public enum TruckType
{
ShortBed,
LongBed
}
Lidando com diferentes tipos de imóveis
Tim em (15:28) enfatiza que só porque duas propriedades compartilham o mesmo nome não significa que elas sejam iguais. Ele ilustra isso com a propriedade Style, que poderia significar diferentes enums (CarType para carros e TruckType para caminhões).
Apresentando barcos à frota
A agência de aluguel amplia sua frota para incluir barcos. Tim demonstra como lidar com isso criando uma classe RentalBoat. Inicialmente, parece administrável, já que os barcos podem compartilhar algumas propriedades com carros e caminhões.
public class RentalBoat : RentalVehicle
{
// Properties and methods specific to boats
}
public class RentalBoat : RentalVehicle
{
// Properties and methods specific to boats
}
Lidando com veleiros
A introdução de barcos à vela representa um desafio, uma vez que estes não possuem motores. Tim em (19:57) ilustra as limitações da herança neste cenário.
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}
Resolvendo o problema com interfaces
Tim sugere tornar os métodos StartEngine e StopEngine virtuais na classe base para permitir a sobrescrita em classes derivadas que não utilizam esses métodos.
public abstract class RentalVehicle
{
// Common properties
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}
public abstract class RentalVehicle
{
// Common properties
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}
Métodos de tratamento que não devem ser chamados
Tim em (21:56) explica as armadilhas de ter métodos em classes herdadas que não deveriam ser chamados. Por exemplo, a classe RentalSailboat, que não possui motor, herda os métodos StartEngine e StopEngine da classe RentalVehicle. Essa situação pode causar problemas se esses métodos forem chamados involuntariamente, pois eles precisam lançar exceções para indicar que não são aplicáveis.
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}
Reconhecendo as limitações da herança
Tim em (24:06) enfatiza como a herança pode levar a uma base de código complexa e confusa quando deixa de fazer sentido lógico. Por exemplo, um veleiro não deve ser tratado como um veículo de aluguel com motor. Isso demonstra as limitações da herança e a necessidade de uma abordagem de projeto melhor.
Em direção a um design melhor com interfaces
Para solucionar esses problemas, Tim sugere um design melhor utilizando interfaces. Ele começa criando um novo projeto de aplicativo de console chamado "BetterOODemo" para demonstrar a abordagem aprimorada.
Definindo a Interface
Tim apresenta a interface IRental para englobar propriedades comuns a todos os aluguéis.
public interface IRental
{
int RentalId { get; set; }
string CurrentRenter { get; set; }
decimal PricePerDay { get; set; }
}
public interface IRental
{
int RentalId { get; set; }
string CurrentRenter { get; set; }
decimal PricePerDay { get; set; }
}
Criando a classe base para veículos terrestres
Tim então cria uma classe base para veículos terrestres, separando o conceito de aluguel de veículos do próprio veículo.
public abstract class LandVehicle
{
public int NumberOfPassengers { get; set; }
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}
public abstract class LandVehicle
{
public int NumberOfPassengers { get; set; }
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}
Ao renomear a classe base do veículo para LandVehicle, Tim garante que apenas os veículos apropriados herdem os métodos relacionados ao motor.
Implementando classes para carros e caminhões
Tim cria classes Car e Truck que herdam de LandVehicle e implementam a interface IRental.
public class Car : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public CarType Style { get; set; }
}
public class Truck : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public TruckType Style { get; set; }
}
public class Car : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public CarType Style { get; set; }
}
public class Truck : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public TruckType Style { get; set; }
}
Este design mantém uma clara separação de responsabilidades, garantindo que cada classe possua apenas propriedades e métodos relevantes ao seu tipo.
Evitando a duplicação de código
Tim em (31:41) discute a importância de evitar duplicação desnecessária de código. Ele explica que, embora a interface IRental exija as mesmas propriedades em várias classes, isso não é considerado uma violação do princípio DRY (Don't Repeat Yourself - Não se Repita), pois nenhuma lógica é duplicada — apenas declarações de propriedade.
Implementando a Classe de Aluguel de Veleiros
Tim em (35:09) explica como lidar com a classe RentalSailboat separadamente implementando a interface IRental, em vez de herdar de LandVehicle. Essa abordagem ajuda a evitar as armadilhas associadas à herança inadequada.
public class Sailboat : IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
// Additional properties and methods specific to sailboats
}
public class Sailboat : IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
// Additional properties and methods specific to sailboats
}
Gerenciando diferentes aluguéis
Tim cria uma lista para gerenciar diferentes tipos de aluguéis, utilizando a interface IRental para armazenar vários tipos de itens, incluindo caminhões, veleiros e carros.
List<IRental> rentals = new List<IRental>
{
new Truck { CurrentRenter = "Truck Renter" },
new Sailboat { CurrentRenter = "Sailboat Renter" },
new Car { CurrentRenter = "Car Renter" }
};
List<IRental> rentals = new List<IRental>
{
new Truck { CurrentRenter = "Truck Renter" },
new Sailboat { CurrentRenter = "Sailboat Renter" },
new Car { CurrentRenter = "Car Renter" }
};
Este design permite iterar pelos aluguéis e acessar propriedades comuns como CurrentRenter, PricePerDay e RentalId.
foreach (var rental in rentals)
{
Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}
foreach (var rental in rentals)
{
Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}
Fundição para Tipos Específicos
Para acessar propriedades e métodos específicos de diferentes tipos de aluguel, Tim demonstra como usar a palavra-chave is para verificar e converter objetos para seus respectivos tipos.
foreach (var rental in rentals)
{
if (rental is Truck truck)
{
Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}");
}
else if (rental is Sailboat sailboat)
{
// Access sailboat-specific properties
}
else if (rental is Car car)
{
// Access car-specific properties
}
}
foreach (var rental in rentals)
{
if (rental is Truck truck)
{
Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}");
}
else if (rental is Sailboat sailboat)
{
// Access sailboat-specific properties
}
else if (rental is Car car)
{
// Access car-specific properties
}
}
Flexibilidade com interfaces
Tim enfatiza que o uso de interfaces proporciona flexibilidade para mudanças futuras. Por exemplo, adicionar novos tipos de itens para alugar, como tanques ou televisores, não afetaria a estrutura existente.
Evitando o uso excessivo da herança
Tim desaconselha o uso excessivo de herança para compartilhamento de código, pois isso pode levar a uma base de código complexa e inflexível. Em vez disso, ele recomenda aproveitar as interfaces e a composição para alcançar os resultados desejados sem estender a relação "é um/uma" além de seus limites lógicos.
Conclusão
A explicação de Tim Corey sobre herança e interfaces em Programação Orientada a Objetos oferece um caminho claro para a criação de código flexível e de fácil manutenção. Ao destacar as armadilhas comuns e fornecer um design refinado com interfaces, ele garante que os desenvolvedores possam tomar decisões informadas sobre a estruturação eficaz de seus aplicativos.
Para uma análise mais aprofundada desses conceitos e para vê-los em ação, assista ao vídeo completo de Tim. O canal dele é uma mina de ouro de tutoriais de programação. Não perca esta oportunidade!
