Fundamentos del Diseño de Software 2024
Si te preguntas qué es realmente el diseño de software y por qué es tan importante, estás en el lugar correcto. Como desarrollador de aplicaciones, quiero acercarte los conceptos fundamentales.
¿Qué es el diseño de software?
El diseño de software es el proceso mediante el cual planificas cómo debe estructurarse tu aplicación. No se trata solo de qué debe hacer el software, sino principalmente de cómo debe hacerlo. Un buen diseño es la clave para crear software eficiente, mantenible y escalable.
Los tres niveles del diseño de software:
- Diseño de Arquitectura: estructura general de toda la aplicación
- Diseño Detallado: implementación concreta de componentes
- Diseño de Interface: definición de APIs e interfaces de usuario
La importancia de un buen diseño
Un buen diseño es crucial para la calidad de tu software. Garantiza que tu aplicación no solo funcione ahora, sino que permanezca extensible y mantenible en el futuro.
Ventajas de un buen diseño:
- Mantenibilidad: más fácil de entender y modificar
- Extensibilidad: nuevas funcionalidades se añaden sin complicaciones
- Testabilidad: los componentes pueden probarse de forma aislada
- Reutilización: los módulos pueden usarse en otros proyectos
- Rendimiento: uso eficiente de los recursos
Principios SOLID
Los principios SOLID son cinco directrices fundamentales para el desarrollo de software orientado a objetos:
1. Single Responsibility Principle (SRP)
Una clase debe tener una única responsabilidad.
// ❌ Mal ejemplo: la clase tiene múltiples responsabilidades
public class UserService {
public void saveUser(User user) { /* guarda el usuario */ }
public void sendEmail(User user) { /* envía correo */ }
public void generateReport(User user) { /* genera reporte */ }
}
// ✅ Buen ejemplo: cada clase tiene una responsabilidad
public class UserService {
public void saveUser(User user) { /* guarda el usuario */ }
}
public class EmailService {
public void sendEmail(User user) { /* envía correo */ }
}
public class ReportService {
public void generateReport(User user) { /* genera reporte */ }
}
2. Open/Closed Principle (OCP)
Los componentes de software deben estar abiertos a extensiones, pero cerrados para modificaciones.
// ❌ Mal ejemplo: requiere cambios de código para cada nuevo tipo de descuento
public class DiscountCalculator {
public double calculateDiscount(String type, double amount) {
if (type.equals("STUDENT")) return amount * 0.1;
if (type.equals("SENIOR")) return amount * 0.2;
// nuevo tipo requiere modificar el código
return 0;
}
}
// ✅ Buen ejemplo: abierto a extensiones
public interface Discount {
double calculate(double amount);
}
public class StudentDiscount implements Discount {
public double calculate(double amount) { return amount * 0.1; }
}
public class SeniorDiscount implements Discount {
public double calculate(double amount) { return amount * 0.2; }
}
3. Liskov Substitution Principle (LSP)
Las subclases deben ser intercambiables por sus clases base.
// ❌ Mal ejemplo: viola LSP
public class Rectangle {
protected int width, height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // viola el comportamiento de Rectangle
}
}
// ✅ Buen ejemplo: respeta LSP
public abstract class Shape {
public abstract int getArea();
}
public class Rectangle extends Shape {
private int width, height;
public int getArea() { return width * height; }
}
public class Square extends Shape {
private int side;
public int getArea() { return side * side; }
}
4. Interface Segregation Principle (ISP)
Interfaces pequeñas y específicas son mejores que interfaces grandes y genéricas.
// ❌ Mal ejemplo: interface demasiado grande
public interface Worker {
void work();
void eat();
void sleep();
}
public class Robot implements Worker {
public void work() { /* ... */ }
public void eat() { /* el robot no come */ }
public void sleep() { /* el robot no duerme */ }
}
// ✅ Buen ejemplo: interfaces especializadas
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public class Robot implements Workable {
public void work() { /* ... */ }
}
5. Dependency Inversion Principle (DIP)
Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.
// ❌ Mal ejemplo: dependencia directa
public class LightSwitch {
private LightBulb bulb;
public LightSwitch() {
this.bulb = new LightBulb(); // dependencia directa
}
public void flip() {
bulb.turnOn();
}
}
// ✅ Buen ejemplo: dependencia de interface
public interface Switchable {
void turnOn();
void turnOff();
}
public class LightBulb implements Switchable {
public void turnOn() { /* ... */ }
public void turnOff() { /* ... */ }
}
public class LightSwitch {
private Switchable device;
public LightSwitch(Switchable device) {
this.device = device; // dependencia de abstracción
}
public void flip() {
device.turnOn();
}
}
Otros principios importantes del diseño
KISS (Keep It Simple, Stupid)
Mantén tu código simple y comprensible. Las soluciones simples suelen ser las mejores.
DRY (Don’t Repeat Yourself)
Evita duplicar código. No te repitas en tu código.
YAGNI (You Ain’t Gonna Need It)
No desarrolles para escenarios hipotéticos o futuros. Enfócate en lo que se necesita ahora.
Separation of Concerns
Separa diferentes aspectos de tu aplicación en módulos distintos.
Patrones de Arquitectura
Layered Architecture
La arquitectura en capas organiza el código en niveles lógicos:
- Presentation Layer: interfaz de usuario
- Business Layer: lógica de negocio
- Data Access Layer: acceso a datos
- Database Layer: base de datos
MVC (Model-View-Controller)
Separa datos, presentación y control:
- Model: datos y lógica de negocio
- View: interfaz de usuario
- Controller: mediador entre Model y View
Microservices Architecture
Divide la aplicación en servicios pequeños e independientes:
- Cada servicio tiene su propia base de datos
- Comunicación a través de APIs
- Despliegue independiente
Event-Driven Architecture
Los sistemas se comunican mediante eventos:
- Bajo acoplamiento entre componentes
- Comunicación asincrónica
- Buena escalabilidad
Patrones de Diseño (GoF Patterns)
Creational Patterns
Factory Method
Crea objetos sin necesidad de conocer su clase exacta.
public interface Vehicle {
void drive();
}
public class Car implements Vehicle {
public void drive() { System.out.println("Car drives"); }
}
public class Motorcycle implements Vehicle {
public void drive() { System.out.println("Motorcycle drives"); }
}
public abstract class VehicleFactory {
public abstract Vehicle createVehicle();
}
public class CarFactory extends VehicleFactory {
public Vehicle createVehicle() { return new Car(); }
}
Singleton
Garantiza que una clase se instancia una sola vez.
public class DatabaseConnection {
private static DatabaseConnection instance;
private DatabaseConnection() { /* constructor privado */ }
public static DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
}
Patrones Estructurales
Adapter
Permite que interfaces incompatibles trabajen juntas.
public interface MediaPlayer {
void play(String audioType, String fileName);
}
public interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
public class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer();
}
}
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
}
}
}
Patrones de Comportamiento
Observer
Permite notificar a múltiples objetos cuando el estado cambia.
import java.util.ArrayList;
import java.util.List;
public interface Observer {
void update(String message);
}
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
public void registerObserver(Observer observer) {
observers.add(observer);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(news);
}
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
}
Strategy
Define una familia de algoritmos y los hace intercambiables.
public interface PaymentStrategy {
void pay(int amount);
}
public class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card");
}
}
public class PayPalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal");
}
}
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
Técnicas de Refactorización
Extract Method
Extrae código en una método separado para mejorar la legibilidad.
// Antes
public void processOrder(Order order) {
// Validación
if (order == null) throw new IllegalArgumentException();
if (order.getItems().isEmpty()) throw new IllegalArgumentException();
// Cálculo
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
// Almacenamiento
order.setTotal(total);
orderRepository.save(order);
}
// Después
public void processOrder(Order order) {
validateOrder(order);
double total = calculateTotal(order);
saveOrder(order, total);
}
private void validateOrder(Order order) {
if (order == null) throw new IllegalArgumentException();
if (order.getItems().isEmpty()) throw new IllegalArgumentException();
}
private double calculateTotal(Order order) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
return total;
}
private void saveOrder(Order order, double total) {
order.setTotal(total);
orderRepository.save(order);
}
Replace Conditional with Polymorphism
Reemplaza condicionales con polimorfismo.
// Antes
public class Bird {
public void fly(String type) {
if (type.equals("Eagle")) {
System.out.println("Eagle flies high");
} else if (type.equals("Penguin")) {
System.out.println("Penguin cannot fly");
}
}
}
// Después
public abstract class Bird {
public abstract void fly();
}
public class Eagle extends Bird {
public void fly() {
System.out.println("Eagle flies high");
}
}
public class Penguin extends Bird {
public void fly() {
System.out.println("Penguin cannot fly");
}
}
Métricas de Calidad de Código
Cyclomatic Complexity
Mide la complejidad del código contando el número de rutas de decisión.
Maintainability Index
Evalúa qué tan fácil es mantener el código a lo largo del tiempo.
Test Coverage
Porcentaje del código que está cubierto por pruebas.
Importancia de la Documentación
Una buena documentación es crucial para la mantenibilidad:
Tipos de Documentación
- API Documentation: Descripción de interfaces y métodos
- Architecture Documentation: Visión general de la arquitectura del sistema
- Code Comments: Explicaciones de partes complejas del código
- User Documentation: Guías de uso para los usuarios finales
Mejores Prácticas en Documentación
- Documenta el “por qué”, no solo el “qué”
- Mantén la documentación actualizada
- Usa lenguaje claro y comprensible
- Utiliza diagramas para visualizar conceptos
Ejemplo Práctico: Sistema de E-Commerce
Aquí hay un ejemplo práctico que aplica muchos de los principios que hemos cubierto:
// Interfaces para acoplamiento bajo
public interface OrderService {
Order createOrder(List<Item> items);
void processPayment(Order order, PaymentStrategy strategy);
}
public interface InventoryService {
boolean checkAvailability(Item item);
void reserveItem(Item item);
}
// Implementación con principios SOLID
@Service
public class OrderServiceImpl implements OrderService {
private final InventoryService inventoryService;
private final NotificationService notificationService;
public OrderServiceImpl(InventoryService inventoryService,
NotificationService notificationService) {
this.inventoryService = inventoryService;
this.notificationService = notificationService;
}
@Override
public Order createOrder(List<Item> items) {
validateItems(items);
reserveItems(items);
Order order = new Order(items);
order.calculateTotal();
return order;
}
@Override
public void processPayment(Order order, PaymentStrategy strategy) {
strategy.pay(order.getTotal());
notificationService.sendOrderConfirmation(order);
}
private void validateItems(List<Item> items) {
for (Item item : items) {
if (!inventoryService.checkAvailability(item)) {
throw new ItemNotAvailableException(item);
}
}
}
private void reserveItems(List<Item> items) {
for (Item item : items) {
inventoryService.reserveItem(item);
}
}
}
Preguntas y Respuestas Relevantes para Examen
1. ¿Qué son los principios SOLID y por qué son importantes?
Respuesta: Los principios SOLID son cinco pautas de diseño: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. Son importantes porque conducen a código más mantenible, extensible y testeable. Ayudan a evitar deuda técnica y mejoran la calidad del código.
2. Explica el Principio de Responsabilidad Única con un ejemplo.
Respuesta: El SRP establece que una clase debe tener una única razón para cambiar. Ejemplo: una clase UserService debe ocuparse solo de la gestión de usuarios, no del envío de correos electrónicos ni de la generación de reportes. Esto mejora la mantenibilidad porque los cambios en una responsabilidad no afectan la otra.
3. ¿Cuál es la diferencia entre cohesión y acoplamiento?
Respuesta: Cohesión describe qué tan fuertemente relacionados están los elementos dentro de un módulo (alta cohesión es deseable). Acoplamiento describe qué tan dependientes son los módulos entre sí (bajo acoplamiento es deseable). El objetivo es lograr alta cohesión con bajo acoplamiento para mejorar la mantenibilidad.
4. ¿Cuándo se utiliza el patrón Singleton?
Respuesta: Singleton se usa cuando necesitas exactamente una instancia de una clase, como en gestores de conexiones a base de datos, servicios de logging o gestores de configuración. Garantiza que solo existe una instancia globalmente y proporciona un punto de acceso centralizado.
5. Explica el Principio Open/Closed.
Respuesta: El OCP establece que los componentes de software deben estar abiertos para extensión pero cerrados para modificación. Significa que debes poder añadir nueva funcionalidad a través de la extensión, como herencia o composición, sin cambiar el código existente.
Recomendaciones de libros sobre diseño de software
Estos son enlaces de afiliados externos. Al comprar a través de nuestros enlaces obtenemos comisiones:



