oop - solid - ¿Alguien puede proporcionar un ejemplo del principio de sustitución de Liskov(LSP) utilizando vehículos?
solid liskov substitution principle example (5)
El principio de sustitución de Liskov establece que un subtipo debe ser sustituible por ese tipo (sin alterar la corrección del programa).
- ¿Puede alguien proporcionar un ejemplo de este principio en el dominio de vehículos (automotores)?
- ¿Puede alguien proporcionar un ejemplo de una violación de este principio en el dominio de los vehículos?
He leído sobre el ejemplo cuadrado / rectángulo, pero creo que un ejemplo con vehículos me dará una mejor comprensión del concepto.
El principio de sustitución de Liskov establece que un objeto con una interfaz determinada puede ser reemplazado por un objeto diferente que implementa esa misma interfaz al tiempo que conserva toda la corrección del programa original. Eso significa que no solo la interfaz debe tener exactamente los mismos tipos, sino que el comportamiento también debe ser correcto.
En un vehículo, debería poder reemplazar una pieza por otra diferente, y el automóvil seguiría funcionando. Digamos que su radio anterior no tiene un sintonizador digital, pero quiere escuchar la radio HD para comprar una radio nueva que tenga un receptor HD. Debería poder sacar la radio vieja y conectar la radio nueva, siempre que tenga la misma interfaz. En la superficie, eso significa que el enchufe eléctrico que conecta la radio al automóvil debe tener la misma forma en la nueva radio que en la radio antigua. Si el enchufe del auto es rectangular y tiene 15 pines, entonces el nuevo conector de la radio debe ser rectangular y también tiene 15 pines.
Pero hay otras consideraciones además del ajuste mecánico: el comportamiento eléctrico en el enchufe también debe ser el mismo. Si el pin 1 en el conector para la radio antigua tiene + 12V, entonces el pin 1 en el conector para la nueva radio también tiene que ser + 12V. Si el pin 1 en la nueva radio era el pin de "salida del altavoz izquierdo", la radio podría cortarse o quemar un fusible. Eso sería una clara violación de la LSP.
También podría considerar una situación de baja calificación: digamos que su radio costosa muere y que solo puede pagar una radio AM. No tiene salida estéreo, pero tiene el mismo conector que su radio actual. Digamos que la especificación tiene el pin 3 siendo el altavoz izquierdo, y el pin 4 el altavoz derecho. Si su radio AM reproduce la señal monofónica a través de los pines 3 y 4, podría decir que su comportamiento es consistente y que sería una sustitución aceptable. Pero si su nueva radio AM reproduce audio solo en el pin 3, y nada en el pin 4, el sonido sería desequilibrado, y eso probablemente no sería una sustitución aceptable. Esa situación también violaría el LSP, porque si bien puede escuchar sonidos y no se queman los fusibles, la radio no cumple con todas las especificaciones de la interfaz.
En mi opinión, para archivar el LSP, los subtipos nunca pueden agregar nuevos métodos públicos. Sólo métodos y campos privados. Y, por supuesto, los subtipos pueden anular los métodos de la clase básica. Si un subtipo tiene un único método público que el basetype no tiene, simplemente no puede sustituir el subtipo con el basetype. Si pasa una instancia al método de un cliente mediante el cual recibe una instancia de subtipo pero el tipo de parámetro es el tipo de base o si tiene una colección de tipo de tipo baset donde también forman parte los subtipos, entonces ¿cómo puede llamar al método de la clase de subtipo? sin preguntar por su tipo usando una sentencia if y si el tipo coincide, haga una conversión a ese subtipo para llamar al método.
Imagen Quiero alquilar un coche cuando me esté mudando de casa. Llamo a la empresa de alquiler y les pregunto qué modelos tienen. Sin embargo, me dicen que me darán el próximo auto que esté disponible:
public class CarHireService {
public Car hireCar() {
return availableCarPool.getNextCar();
}
}
Pero me han dado un folleto que me dice que todos sus modelos vienen con estas características:
public interface Car {
public void drive();
public void playRadio();
public void addLuggage();
}
Eso suena justo lo que estoy buscando, así que reservo un auto y me voy feliz. El día de la mudanza, aparece un automóvil de Fórmula Uno fuera de mi casa:
public class FormulaOneCar implements Car {
public void drive() {
//Code to make it go super fast
}
public void addLuggage() {
throw new NotSupportedException("No room to carry luggage, sorry.");
}
public void playRadio() {
throw new NotSupportedException("Too heavy, none included.");
}
}
No estoy contento, porque esencialmente su folleto me mintió: no importa si el auto de Fórmula Uno tiene una bota falsa que parece que puede contener equipaje pero no se abre, ¡eso no sirve para mudarse de casa!
Si me dicen que "estas son las cosas que hacen todos nuestros autos", entonces cualquier auto que me den debe comportarse de esta manera. Si no puedo confiar en los detalles en su folleto, es inútil. Esa es la esencia del principio de sustitución de Liskov .
Para mí, esta Cita de 1996 del tío Bob ( Robert C Martin ) resume el mejor LSP:
Las funciones que usan punteros o referencias a clases básicas deben poder usar objetos de clases derivadas sin saberlo.
En tiempos recientes, como alternativa a las abstracciones de herencia basadas en la subclasificación de una base / superclase (generalmente abstracta), a menudo también usamos interfaces para la abstracción polimórfica. El LSP tiene implicaciones tanto para el consumidor como para la implementación de la abstracción:
- Cualquier código que consuma una clase o una abstracción de interfaz no debe asumir nada más acerca de la clase más allá de la abstracción definida;
- Cualquier subclase de una superclase o implementación de una abstracción debe cumplir con los requisitos y convenciones de la interfaz con la abstracción.
Cumplimiento LSP
Este es un ejemplo que utiliza un IVehicle
interfaz que puede tener múltiples implementaciones (alternativamente, puede sustituir la interfaz por una clase base abstracta con varias subclases - mismo efecto).
interface IVehicle
{
void Drive(int miles);
void FillUpWithFuel();
int FuelRemaining {get; } // C# syntax for a readable property
}
Esta implementación de un consumidor de IVehicle
mantiene dentro de los límites de LSP:
void MethodWhichUsesIVehicle(IVehicle aVehicle)
{
...
// Knows only about the interface. Any IVehicle is supported
aVehicle.Drive(50);
}
Violación deslumbrante - cambio de tipo de tiempo de ejecución
Aquí hay un ejemplo de una violación de LSP, usando RTTI y luego hacia abajo: el tío Bob llama a esto una "violación flagrante":
void MethodWhichViolatesLSP(IVehicle aVehicle)
{
if (aVehicle is Car)
{
var car = aVehicle as Car;
// Do something special for car - this method is not on the IVehicle interface
car.ChangeGear();
}
// etc.
}
El método de violación va más allá de la interfaz de IVehicle
contratada y corta una ruta específica para una implementación conocida de la interfaz (o una subclase, si se usa la herencia en lugar de las interfaces). El tío Bob también explica que las violaciones de LSP que utilizan el comportamiento de cambio de tipo también suelen violar el principio de apertura y cierre , ya que se requerirá una modificación continua de la función para acomodar las nuevas subclases.
Violación: la condición previa se refuerza con un subtipo
Otro ejemplo de violación sería cuando una "condición previa se refuerza con un subtipo" :
public class Vehicle
{
public virtual void Drive(int miles)
{
Assert(miles > 0 && miles < 300); // Consumers see this as the contract
}
}
public class Scooter : Vehicle
{
public override void Drive(int miles)
{
Assert(miles > 0 && miles < 50); // ** Violation
base.Drive(miles);
}
}
Aquí, la subclase Scooter intenta violar el LSP al tratar de fortalecer (restringir aún más) la condición previa en el método de Drive
clase base que miles < 300
, hasta ahora un máximo de menos de 50 millas. Esto no es válido, ya que según la definición del contrato de IVehicle
permite 300 millas.
De manera similar, las condiciones de publicación no se pueden debilitar (es decir, relajar) por un subtipo.
(Los usuarios de los contratos de código en C # notarán que las condiciones previas y las condiciones posteriores DEBEN colocarse en la interfaz a través de una clase ContractClassFor
, y no se pueden colocar dentro de las clases de implementación, evitando así la violación)
Violación sutil: abuso de una implementación de interfaz por una subclase
Una violación more subtle
(también la terminología del tío Bob) se puede mostrar con una clase derivada dudosa que implementa la interfaz:
class ToyCar : IVehicle
{
public void Drive(int miles) { /* Show flashy lights, make random sounds */ }
public void FillUpWithFuel() {/* Again, more silly lights and noises*/}
public int FuelRemaining {get {return 0;}}
}
Aquí, independientemente de a qué distancia se ToyCar
el ToyCar
, el combustible restante siempre será cero, lo que sorprenderá a los usuarios de la interfaz IVehicle
(es decir, ¿consumo infinito de MPG - ¿movimiento perpetuo?). En este caso, el problema es que, a pesar de que ToyCar
ha implementado todos los requisitos de la interfaz, ToyCar
simplemente no es un IVehicle
real y simplemente " IVehicle
" la interfaz.
Una forma de evitar que sus interfaces o clases base abstractas sean abusadas de esta manera es asegurarse de que haya un buen conjunto de Pruebas unitarias disponibles en la clase base interfaz / abstracta para probar que todas las implementaciones cumplan con las expectativas (y cualquier suposición). Las pruebas unitarias también son excelentes para documentar el uso típico. por ejemplo, esta NUnit Theory
rechazará que ToyCar
a su base de código de producción:
[Theory]
void EnsureThatIVehicleConsumesFuelWhenDriven(IVehicle vehicle)
{
vehicle.FillUpWithFuel();
Assert.IsTrue(vehicle.FuelRemaining > 0);
int fuelBeforeDrive = vehicle.FuelRemaining;
vehicle.Drive(20); // Fuel consumption is expected.
Assert.IsTrue(vehicle.FuelRemaining < fuelBeforeDrive);
}
Editar, Re: OpenDoor
Abrir puertas parece ser una preocupación completamente diferente, por lo que debe separarse en consecuencia (es decir, la "S" y la "I" en SOLID), por ejemplo
- en una nueva interfaz
IVehicleWithDoors
, que podría heredarIVehicle
- O IMO mejor aún, en una interfaz
IDoor
separada, y luego los vehículos comoCar
andTruck
implementarán las interfacesIVehicle
eIDoor
, peroScooter
yMotorcycle
no. o incluso 3 interfaces,IVehicle
(Drive()
),IDoor
(Open()
) yIVehicleWithDoors
que hereda ambas.
En todos los casos, para evitar violar el LSP, el código que requiere los objetos de estas interfaces no debe abatir la interfaz para acceder a la funcionalidad adicional. El código debe seleccionar la interfaz / clase (super) mínima adecuada que necesita, y atenerse únicamente a la funcionalidad contratada en esa interfaz.
Primero, necesitas definir qué son un vehículo y un automóvil. Según Google (definiciones no muy completas):
Vehículo:
Una cosa usada para transportar personas o bienes, esp. en tierra, como un automóvil, camión o carro.
Automóvil:
Un vehículo de carretera, típicamente con cuatro ruedas, accionado por un motor de combustión interna o eléctrico.
Motor y capaz de transportar un pequeño número de personas.
Entonces, un automóvil es un vehículo, pero un vehículo no es un automóvil.