Skip to content
IRC-CodingIRC-Coding
Software DesignPrincipios de DesignModularidadClean CodeSoftware ArchitectureMejores PrácticasAlgoritmosFundamentos

Fundamentos del Software Design

Introducción al Software Design: principios, patterns y mejores prácticas para software mantenible y escalable.

S

schutzgeist

10 min read
Fundamentos del Software Design

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:

  1. Diseño de Arquitectura: estructura general de toda la aplicación
  2. Diseño Detallado: implementación concreta de componentes
  3. 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:

Arquitecturas de software duraderas

Arquitecturas de software duraderas

Clean Code

Clean Code

Design Patterns

Design Patterns

Volver al blog
Share:

Entradas relacionadas