- Los principios SOLID son un conjunto de principios de diseño de software que ayudan a los desarrolladores a crear sistemas más mantenibles, flexibles y escalables.
-
Cada clase debe tener una única responsabilidad o motivo de cambio. En otras palabras, una clase debe centrarse en una única tarea o función.
-
Definición: Cada clase debe tener una única responsabilidad, es decir, un solo motivo para cambiar.
-
Ejemplo: Imagina que tienes una clase que maneja la lógica de un libro y también la lógica para imprimir el libro.
public class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
// Método para imprimir los detalles del libro (mala práctica)
public void printBook() {
System.out.println("Title: " + title + ", Author: " + author);
}
}- Separar la responsabilidad de impresión en una clase diferente.
public class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
}
public class BookPrinter {
public void printBook(Book book) {
System.out.println("Title: " + book.getTitle() + ", Author: " + book.getAuthor());
}
}-
El Principio de Abierto/Cerrado establece que las clases deben estar abiertas para su extensión, pero cerradas para su modificación. Esto significa que debemos poder agregar nuevas funcionalidades extendiendo el código existente sin tener que modificar el código original.
-
Ejemplo: Cálculo de Precios con Descuento
-
Implementación Inicial (Incorrecta): Tenemos una clase PriceCalculator que calcula el precio de un producto. Al agregar un descuento, modificamos directamente la clase existente, lo que viola el principio OCP.
public class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public double getPrice() {
return price;
}
}
public class PriceCalculator {
public double calculatePrice(Product product, boolean applyDiscount) {
double price = product.getPrice();
if (applyDiscount) {
price = price * 0.9; // 10% de descuento
}
return price;
}
}En este diseño, cada vez que queramos agregar un nuevo tipo de descuento, tendríamos que modificar PriceCalculator.
- Implementación Mejorada (Correcta): Creamos una interfaz
PricedItemy extendemos las clases según sea necesario, sin modificar PriceCalculator.
public interface PricedItem {
double getPrice();
}
public class RegularProduct implements PricedItem {
private String name;
private double price;
public RegularProduct(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public double getPrice() {
return price;
}
}
public class DiscountedProduct implements PricedItem {
private PricedItem product;
private double discountPercentage;
public DiscountedProduct(PricedItem product, double discountPercentage) {
this.product = product;
this.discountPercentage = discountPercentage;
}
@Override
public double getPrice() {
return product.getPrice() * (1 - discountPercentage / 100);
}
}
public class PriceCalculator {
public double calculatePrice(PricedItem pricedItem) {
return pricedItem.getPrice();
}
}- Uso en la Aplicación
public class Main {
public static void main(String[] args) {
PricedItem regularProduct = new RegularProduct("Laptop", 1000.0);
PricedItem discountedProduct = new DiscountedProduct(regularProduct, 10); // 10% de descuento
PriceCalculator calculator = new PriceCalculator();
System.out.println("Precio regular: " + calculator.calculatePrice(regularProduct));
System.out.println("Precio con descuento: " + calculator.calculatePrice(discountedProduct));
}
}- El Principio de Sustitución de Liskov establece que las subclases deben ser sustituibles por sus clases base sin alterar el comportamiento del programa.
- Ejemplo: Vehículos y Capacidad de Vuelo
Imaginemos que tenemos una clase Vehicle una subclase Car y Plane. Si un Plane no puede sustituir a Vehicle sin problemas, estamos violando el LSP.
public class Vehicle {
public void startEngine() {
System.out.println("Engine started");
}
public void fly() {
// Los vehículos en general no vuelan
throw new UnsupportedOperationException("This vehicle can't fly");
}
}
public class Car extends Vehicle {
// No necesita cambiar nada
}
public class Plane extends Vehicle {
@Override
public void fly() {
System.out.println("Plane is flying");
}
}Al intentar sustituir Vehicle por Plane en cualquier contexto que use Vehicle, encontraremos problemas si el método fly es llamado por un Car.
Debemos replantear el diseño para que Vehicle no tenga un método fly que no todos sus hijos puedan implementar correctamente. En su lugar, podemos usar interfaces para manejar la capacidad de vuelo.
public abstract class Vehicle {
public void startEngine() {
System.out.println("Engine started");
}
}
public interface Flyable {
void fly();
}
public class Car extends Vehicle {
// No necesita implementar fly
}
public class Plane extends Vehicle implements Flyable {
@Override
public void fly() {
System.out.println("Plane is flying");
}
}public class Main {
public static void main(String[] args) {
Vehicle car = new Car();
Vehicle plane = new Plane();
car.startEngine();
plane.startEngine();
// Si el vehículo es volador, volará
if (plane instanceof Flyable) {
((Flyable) plane).fly();
}
}
}-
El Principio de Segregación de Interfaces establece que los clientes no deben estar forzados a depender de interfaces que no utilizan. Esto significa que es mejor tener varias interfaces específicas que una sola interfaz general y grande.
-
Ejemplo: Dispositivos Multifuncionales
Imaginemos que tenemos una interfaz MultiFunctionDevice que incluye métodos para imprimir, escanear y enviar fax. No todos los dispositivos implementarán todas estas funciones, lo que puede llevar a métodos no implementados en algunas clases.
public interface MultiFunctionDevice {
void print();
void scan();
void fax();
}
public class Printer implements MultiFunctionDevice {
@Override
public void print() {
System.out.println("Printing...");
}
@Override
public void scan() {
throw new UnsupportedOperationException("Scan not supported");
}
@Override
public void fax() {
throw new UnsupportedOperationException("Fax not supported");
}
}
public class Scanner implements MultiFunctionDevice {
@Override
public void print() {
throw new UnsupportedOperationException("Print not supported");
}
@Override
public void scan() {
System.out.println("Scanning...");
}
@Override
public void fax() {
throw new UnsupportedOperationException("Fax not supported");
}
}Dividimos la interfaz grande en varias interfaces más pequeñas y específicas.
public interface Printer {
void print();
}
public interface Scanner {
void scan();
}
public interface Fax {
void fax();
}
public class SimplePrinter implements Printer {
@Override
public void print() {
System.out.println("Printing...");
}
}
public class SimpleScanner implements Scanner {
@Override
public void scan() {
System.out.println("Scanning...");
}
}
public class MultiFunctionPrinter implements Printer, Scanner, Fax {
@Override
public void print() {
System.out.println("Printing...");
}
@Override
public void scan() {
System.out.println("Scanning...");
}
@Override
public void fax() {
System.out.println("Faxing...");
}
}public class Main {
public static void main(String[] args) {
Printer printer = new SimplePrinter();
printer.print();
Scanner scanner = new SimpleScanner();
scanner.scan();
MultiFunctionPrinter mfp = new MultiFunctionPrinter();
mfp.print();
mfp.scan();
mfp.fax();
}
}-
El Principio de Inversión de Dependencias establece que los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones. Además, las abstracciones no deben depender de los detalles, los detalles deben depender de las abstracciones.
-
Ejemplo: Servicio de Notificaciones
Tenemos una clase NotificationService que depende directamente de una implementación concreta EmailService.
public class EmailService {
public void sendEmail(String message) {
System.out.println("Sending email: " + message);
}
}
public class NotificationService {
private EmailService emailService;
public NotificationService() {
this.emailService = new EmailService();
}
public void notify(String message) {
emailService.sendEmail(message);
}
}En este diseño, NotificationService depende directamente de EmailService, lo que hace que sea difícil cambiar la implementación del servicio de notificaciones (por ejemplo, para usar SMS en lugar de email).
Introducimos una abstracción MessageService que NotificationService utilizará. Luego, implementamos EmailService como una de las posibles implementaciones de MessageService.
public interface MessageService {
void sendMessage(String message);
}
public class EmailService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Sending email: " + message);
}
}
public class SmsService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
public class NotificationService {
private MessageService messageService;
// Inyección de dependencia a través del constructor
public NotificationService(MessageService messageService) {
this.messageService = messageService;
}
public void notify(String message) {
messageService.sendMessage(message);
}
}public class Order {
private List<String> items;
private double totalAmount;
public Order(List<String> items) {
this.items = items;
this.totalAmount = calculateTotal();
}
private double calculateTotal() {
// Lógica para calcular el total de la orden
return 100.0; // simplificado
}
public void printOrder() {
// Lógica para imprimir la orden
}
public void saveToDatabase() {
// Lógica para guardar la orden en la base de datos
}
}Refactoriza la clase Order para que cumpla con el Principio de Responsabilidad Única. Debes separar las responsabilidades de cálculo, impresión y almacenamiento en clases diferentes.
Pistas:
- La clase Order solo debe gestionar los datos de la orden.
- Crea una nueva clase para manejar la impresión de la orden.
- Crea una nueva clase para manejar el almacenamiento de la orden en la base de datos.
Refactoriza el código para que cumpla con el Principio de Abierto/Cerrado. Debes permitir la extensión de nuevos métodos de pago sin modificar la clase PaymentProcessor.
public class PaymentProcessor {
public void processPayment(String paymentType) {
if (paymentType.equals("credit")) {
// Lógica para procesar el pago con tarjeta de crédito
} else if (paymentType.equals("paypal")) {
// Lógica para procesar el pago con PayPal
}
}
}Pistas
- Crea una interfaz PaymentMethod con un método
processPayment. - Implementa esta interfaz en clases concretas para cada tipo de pago (por ejemplo,
CreditCardPaymentyPayPalPayment). - Modifica PaymentProcessor para que use la interfaz
PaymentMethod.
Refactoriza el código para que cumpla con el Principio de Segregación de Interfaces. Crea interfaces más específicas para evitar que las clases implementen métodos que no necesitan.
public interface Worker {
void work();
void eat();
}
public class HumanWorker implements Worker {
@Override
public void work() {
// Lógica para trabajar
}
@Override
public void eat() {
// Lógica para comer
}
}
public class RobotWorker implements Worker {
@Override
public void work() {
// Lógica para trabajar
}
@Override
public void eat() {
throw new UnsupportedOperationException("Robots don't eat");
}
}Pistas
- Crea una interfaz Workable para la funcionalidad de trabajo.
- Crea una interfaz Eatable para la funcionalidad de comer.
- Implementa estas interfaces en las clases correspondientes.
Refactoriza el código para que cumpla con el Principio de Inversión de Dependencias. Introduce una abstracción para la funcionalidad de almacenamiento de datos.
public class Database {
public void save(String data) {
// Lógica para guardar datos en la base de datos
}
}
public class DataService {
private Database database = new Database();
public void saveData(String data) {
database.save(data);
}
}Pistas
- Crea una interfaz
DataStoragecon un método save. - Implementa
DataStorageen la claseDatabase. - Modifica
DataServicepara que dependa deDataStorageen lugar deDatabase.
Refactoriza la clase Employee para que cumpla con el Principio de Responsabilidad Única. Debes separar las responsabilidades de cálculo, generación de reportes y almacenamiento en clases diferentes.
public class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public void calculatePay() {
// Lógica para calcular el salario
}
public void generateReport() {
// Lógica para generar un reporte del empleado
}
public void saveToDatabase() {
// Lógica para guardar el empleado en la base de datos
}
}Pistas
- La clase
Employeesolo debe manejar los datos del empleado. - Crea una nueva clase
PayrollServicepara manejar el cálculo del salario. - Crea una nueva clase
EmployeeReportpara manejar la generación del reporte. - Crea una nueva clase
EmployeeRepositorypara manejar el almacenamiento en la base de datos.
Refactoriza el código para que cumpla con el Principio de Abierto/Cerrado. Debes permitir la extensión de nuevos cálculos de impuestos sin modificar la clase TaxCalculator.
public class TaxCalculator {
public double calculateTax(String country) {
if (country.equals("USA")) {
// Lógica para calcular el impuesto en USA
return 0.1;
} else if (country.equals("UK")) {
// Lógica para calcular el impuesto en UK
return 0.2;
}
return 0;
}
}Pistas
- Crea una interfaz
TaxStrategycon un métodocalculateTax. - Implementa esta interfaz en clases concretas para cada país (por ejemplo, USATaxStrategy y UKTaxStrategy).
- Modifica
TaxCalculatorpara que use la interfazTaxStrategy.