Enums ou várias classes herdadas
Estou lendo sobre OOD e me deparei com um problema de design de estacionamento. O estacionamento possui pisos com vagas de estacionamento. A classe de vagas é a seguinte:
public enum ParkingSpotType {
HANDICAPPED, COMPACT, LARGE, MOTORBIKE, ELECTRIC
}
public abstract class ParkingSpot {
private String number;
private boolean free;
private Vehicle vehicle;
private final ParkingSpotType type;
public boolean IsFree();
public ParkingSpot(ParkingSpotType type) {
this.type = type;
}
public boolean assignVehicle(Vehicle vehicle) {
this.vehicle = vehicle;
free = false;
}
public boolean removeVehicle() {
this.vehicle = null;
free = true;
}
}
public class HandicappedSpot extends ParkingSpot {
public HandicappedSpot() {
super(ParkingSpotType.HANDICAPPED);
}
}
public class CompactSpot extends ParkingSpot {
public CompactSpot() {
super(ParkingSpotType.COMPACT);
}
}
public class LargeSpot extends ParkingSpot {
public LargeSpot() {
super(ParkingSpotType.LARGE);
}
}
public class MotorbikeSpot extends ParkingSpot {
public MotorbikeSpot() {
super(ParkingSpotType.MOTORBIKE);
}
}
public class ElectricSpot extends ParkingSpot {
public ElectricSpot() {
super(ParkingSpotType.ELECTRIC);
}
}
Os parâmetros são autoexplicativos. Então, por que precisamos de classes separadas para cada valor ENUM? Qual é a vantagem disso? Pensei em ter apenas um tipo de classe ParkingSpot com um campo representando o tipo de vaga de estacionamento. Mas por que precisamos tem classes separadas para cada valor ENUM junto com o armazenamento de Enum como um campo também?
Nota: Para design completo, consulte https://www.educative.io/courses/grokking-the-object-oriented-design-interview/gxM3gRxmr8Z
Respostas
Você está correto ao dizer que agora as classes derivadas são supérfluas, porque sua única diferença é um valor enum específico, que é rotulado exatamente da mesma maneira. Você poderia omitir o enum ou as classes derivadas e nada mudaria.
No entanto, essa conclusão pode mudar quando houver mais diferenças do que apenas o valor enum.
Por exemplo, se o preço da vaga for calculado de forma diferente com base no tipo de vaga. Por ter as classes derivadas, você pode manter a lógica de cálculo de cada tipo de vaga de estacionamento separadamente e fazer com que substituam o mesmo método de base.
Comparativamente, se você tivesse apenas a classe base para trabalhar, ela teria que conter toda a lógica de cálculo.
No entanto, o exemplo atual é um pouco leve no contexto e lista de recursos futuros para decidir se a herança é garantida aqui ou não. Não devemos apenas avaliar o estado atual do código (no qual sua observação está correta), mas também levar em conta quaisquer mudanças que são planejadas / esperadas em um futuro próximo.
Dito isso, suspeito que geralmente você acabará tendo que remover as classes derivadas ou o enum. Não vejo a vantagem de implementar as mesmas informações de duas maneiras diferentes.
A diferença se torna aparente se considerarmos
CompactSpot.Assign(Car.SUV)
isso deve obviamente produzir um erro de algum tipo e temos duas opções sobre como implementá-lo.
ou:
ParkingSpot.Assign(car)
{
if(car is SUV && this.type = COMPACT)
{
throw CarTooLarge()
}
if(car != HANDICAPPED && this.type == HANDICAPPED)
....
}
CompactSpot : ParkingSpot
Assign(car)
{
if(car is SUV)
{
throw CarTooLarge()
}
base.assign(car);
}
}
A abordagem de herança nos permite dividir a lógica condicional entre várias classes, em vez de ter um grande bloco condicional e lógica na classe base.
Se eu consumir sua classe ParkingSpot como uma DLL binária, posso herdar um novo tipo de ParkingSpot e adicionar minha própria lógica. O que não posso fazer se você tiver enum
A pergunta responde a si mesma se pensarmos em comportamento em vez de estado, e devemos. A herança é motivada por "fazer a mesma coisa, mas de maneira diferente" ou ter funcionalidade adicional para vários subtipos. Talvez os designers têm uma boa noção de que ParkingSpote Vehiclecomportamento vai expandir e evoluir. Talvez por uma questão de aprendizado, eles simplesmente incluíram subtipulação.
Embora um ENUM diferencie veículos / parkingSpots do mesmo tipo, os requisitos de funcionalidade devem ser uma consideração prioritária. Mesmo assim, um enum ainda pode ser apropriado.
Há uma necessidade de combinar veículos e vagas de estacionamento - o fato de ser um ENUM é apenas um detalhe de implementação. Portanto, uma VehicleTypepropriedade púbica parece caber. Essa propriedade está muito mais no espírito de OO do que, digamos, o código do cliente vasculhando o Reflection / metadados. O código do cliente não deve calcular o estado de outro objeto.
Mas deve haver algum serviço, digamos ParkingAllocator que primeiro procurará uma vaga livre relevante para um determinado veículo e somente se uma vaga livre de um determinado veículo estiver disponível, estacionaremos um veículo lá. Portanto, se estivermos tentando atribuir um veículo para um ponto, temos absoluta certeza de que o ponto é grande o suficiente para um veículo. Isso faz sentido?
O diagrama de caso de uso mostra o atendente do estacionamento e os atores do cliente. Eles não estão no diagrama de classes, mas fazem parte do espaço do problema.
Acho que existem requisitos ausentes ou com defeito.
Um veículo menor é permitido em um slot maior? As motocicletas cabem em qualquer lugar.
Qualquer objeto de veículo pode ser elétrico ou ter permissões para deficientes físicos ou ambos - na vida real, pelo menos - então eu vejo
ElectricSloteHandicapSlotcomo atributos de uma vaga de estacionamento. Conforme mostrado, o diagrama de classes aqui é inconsistente com o conceito de veículos de diferentes tamanhos cabendo em slots de tamanhos diferentes.Não há correspondência aparente entre os tipos de vagas de estacionamento e os tipos de veículos. Quais veículos exigem "grande", por exemplo? De imediato, um Dicionário pode corresponder explicitamente os locais aos veículos e / ou vice-versa. Valores inteiros de enum subjacentes também podem representar o tamanho relativo da vaga de estacionamento. E como eu disse acima, "elétrico" não é um tamanho.
Estruturas de dados!
essas coisas ...
if(car is SUV)
{
throw CarTooLarge()
}
base.assign(car);
}
desaparece se essa correspondência de tipo de veículo / tipo local for uma estrutura de dados. A Dictionaryé uma estrutura de dados. De imediato, vejo dois dicionários - veículo-> local e local-> veículo - como o núcleo de uma classe com métodos apropriados.