Skip to content
Menu
Laboratorios TERA Byte
  • Home
  • Quienes Somos
  • Política de privacidad
  • Contáctame
Laboratorios TERA Byte

Del caos al control: Principio Open/Closed con el Patrón Specification en Java

Posted on 8 August, 20258 August, 2025

🧠 El problema de fondo

Github Repository

Cuando trabajas con filtros en Java, es común empezar de forma inocente. Tienes una clase de productos, y necesitas filtrar por color, por tamaño, y tal vez por ambos. Así que creas métodos como estos:

Java
class ProductFilter {

  public Stream<Product> filterByColor(List<Product> products, Color color) {
    return products.stream().filter(p -> p.color == color);
  }

  public Stream<Product> filterBySize(List<Product> products, Size size) {
    return products.stream().filter(p -> p.size == size);
  }

  public Stream<Product> filterBySizeAndColor(List<Product> products, Size size, Color color){
    return products.stream().filter(p -> p.size == size && p.color == color);
  }
}

Pero el problema viene después. ¿Qué pasa si mañana te piden filtrar por peso? ¿Y pasado mañana por categoría, stock o precio?

Cada nuevo criterio implica modificar la clase ProductFilter. Y si ya tienes combinaciones como “color y tamaño”, ahora te piden “color, tamaño y peso”, y después “color o tamaño y categoría, pero no en oferta”…

Tu clase de filtros se vuelve una bomba de tiempo. Y no solo por la cantidad de código duplicado, sino porque violas uno de los principios fundamentales de diseño: Open/Closed Principle.

¿Qué es el Open/Closed Principle?

El Principio , parte de los principios SOLID, dice lo siguiente:

“Una entidad de software debe estar abierta para extensión, pero cerrada para modificación.”

Esto significa que deberías poder agregar nueva funcionalidad sin tener que modificar el código existente. En otras palabras, tu clase debería poder crecer sin ser editada directamente. ¿Por qué? Porque cada vez que la modificas, puedes romper lo que ya funcionaba.


¿Cómo evitamos eso? Usando el Patrón Specification

El patrón Specification nos permite encapsular condiciones en objetos. En vez de tener lógica metida en ifs o métodos con combinaciones, defines clases que expresan claramente una condición, como por ejemplo “el producto es verde” o “el producto es grande”.

Y lo mejor es que estas condiciones se pueden componer, es decir, combinar varias para formar reglas complejas sin necesidad de escribir nuevos métodos.


Veamos un ejemplo práctico

Supón que tienes estas definiciones básicas:

Java
enum Color {
  RED, GREEN, BLUE
}

enum Size {
  SMALL, MEDIUM, LARGE
}

class Product {
  public String name;
  public Color color;
  public Size size;

  public Product(String name, Color color, Size size) {
    this.name = name;
    this.color = color;
    this.size = size;
  }
}

Ahora, el filtro clásico sería algo como esto:

Java
class ProductFilter {
  public Stream<Product> filterByColor(List<Product> products, Color color) {
    return products.stream().filter(p -> p.color == color);
  }

  public Stream<Product> filterBySize(List<Product> products, Size size) {
    return products.stream().filter(p -> p.size == size);
  }

  public Stream<Product> filterBySizeAndColor(List<Product> products, Size size, Color color) {
    return products.stream().filter(p -> p.size == size && p.color == color);
  }
}

El problema es evidente: si quieres combinar más criterios, tienes que modificar esta clase. No está cerrada para modificación.


Refactor con Specification Pattern

Creamos una interfaz genérica para representar condiciones:

Java
@FunctionalInterface
interface Specification<T> {
  boolean isSatisfied(T item);
}

Y un filtro que use esa interfaz:

Java
interface Filter<T> {
  Stream<T> filter(List<T> items, Specification<T> spec);
}

Luego, creamos especificaciones concretas:

Java
class ColorSpecification implements Specification<Product> {
  private final Color color;

  public ColorSpecification(Color color) {
    this.color = color;
  }

  public boolean isSatisfied(Product p) {
    return p.color == color;
  }
}

class SizeSpecification implements Specification<Product> {
  private final Size size;

  public SizeSpecification(Size size) {
    this.size = size;
  }

  public boolean isSatisfied(Product p) {
    return p.size == size;
  }
}

Y también podemos combinarlas para crear especificaciones AND, OR, NOT:

Java
class AndSpecification<T> implements Specification<T> {
  private final Specification<T> first, second;

  public AndSpecification(Specification<T> first, Specification<T> second) {
    this.first = first;
    this.second = second;
  }

  public boolean isSatisfied(T item) {
    return first.isSatisfied(item) && second.isSatisfied(item);
  }
}

Finalmente, el filtro genérico:

Java
class BetterFilter implements Filter<Product> {
  public Stream<Product> filter(List<Product> items, Specification<Product> spec) {
    return items.stream().filter(p -> spec.isSatisfied(p));
  }
}

¿Y cómo se usa todo esto?

Supongamos que tienes tus productos:

Java
Product apple = new Product("Apple", Color.GREEN, Size.SMALL);
Product tree = new Product("Tree", Color.GREEN, Size.LARGE);
Product house = new Product("House", Color.BLUE, Size.LARGE);

List<Product> products = List.of(apple, tree, house);

Ahora puedes filtrar así:

Java
BetterFilter bf = new BetterFilter();

bf.filter(products, new ColorSpecification(Color.GREEN))
  .forEach(p -> System.out.println(p.name + " is green"));

bf.filter(products, new AndSpecification<>(
    new ColorSpecification(Color.BLUE),
    new SizeSpecification(Size.LARGE)
)).forEach(p -> System.out.println(p.name + " is large and blue"));

Y si mañana necesitas filtrar productos que no sean rojos, puedes escribir una nueva especificación NotSpecification.


¿Y si tengo muchas condiciones?

Puedes crear una clase de utilidad para componer varias:

Java
class Specs {
  public static <T> Specification<T> and(Specification<T>... specs) {
    return item -> Arrays.stream(specs).allMatch(s -> s.isSatisfied(item));
  }

  public static <T> Specification<T> not(Specification<T> spec) {
    return item -> !spec.isSatisfied(item);
  }
}

Lo podemos usar asi:

Java
Specification<Product> greenAndLargeButNotRed = Specs.and(
  new ColorSpecification(Color.GREEN),
  new SizeSpecification(Size.LARGE),
  Specs.not(new ColorSpecification(Color.RED))
);

bf.filter(products, greenAndLargeButNotRed)
  .forEach(p -> System.out.println("MATCH: " + p.name));

Conclusión

Este enfoque te da una arquitectura sólida que escala sin explotar. Puedes agregar nuevas reglas de negocio sin tocar las clases ya existentes. Cumples el Principio de Abierto/Cerrado, tu sistema se vuelve más fácil de mantener, y tu código es más expresivo.

Y eso, en un sistema real con múltiples condiciones y reglas cambiantes, marca la diferencia entre sobrevivir y tener que reescribir todo cada trimestre.

Te Dejamos el link con el repositorio: Github Repository

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Comentarios recientes

No comments to show.

Archivos

  • August 2025
  • June 2025

Categorías

  • Blog
©2025 Laboratorios TERA Byte | Powered by SuperbThemes!