JavaRush /Blog Java /Random-ES /Patrones de diseño en Java
Viacheslav
Nivel 3

Patrones de diseño en Java

Publicado en el grupo Random-ES
Los patrones o patrones de diseño son una parte del trabajo de un desarrollador que a menudo se pasa por alto, lo que dificulta el mantenimiento del código y su adaptación a nuevos requisitos. Te sugiero que mires qué es y cómo se usa en el JDK. Naturalmente, todos los patrones básicos, de una forma u otra, nos rodean desde hace mucho tiempo. Veámoslos en esta reseña.
Patrones de diseño en Java - 1
Contenido:

Plantillas

Uno de los requisitos más comunes en las vacantes es el “Conocimiento de patrones”. En primer lugar, vale la pena responder una pregunta sencilla: "¿Qué es un patrón de diseño?" El patrón se traduce del inglés como "plantilla". Es decir, este es un patrón determinado según el cual hacemos algo. Lo mismo ocurre en la programación. Existen algunas mejores prácticas y enfoques establecidos para resolver problemas comunes. Todo programador es un arquitecto. Incluso cuando crea solo unas pocas clases o incluso una, depende de usted cuánto tiempo puede sobrevivir el código bajo requisitos cambiantes y qué tan conveniente es para que otros lo utilicen. Y aquí es donde el conocimiento de las plantillas ayudará, porque... Esto le permitirá comprender rápidamente cuál es la mejor manera de escribir código sin reescribirlo. Como sabes, los programadores son personas perezosas y es más fácil escribir algo bien de inmediato que rehacerlo varias veces). Los patrones también pueden parecer similares a los algoritmos. Pero tienen una diferencia. El algoritmo consta de pasos específicos que describen las acciones necesarias. Los patrones solo describen el enfoque, pero no describen los pasos de implementación. Los patrones son diferentes, porque... resolver diferentes problemas. Normalmente se distinguen las siguientes categorías:
  • Generativo

    Estos patrones resuelven el problema de flexibilizar la creación de objetos.

  • Estructural

    Estos patrones resuelven el problema de construir conexiones efectivas entre objetos.

  • conductual

    Estos patrones resuelven el problema de la interacción efectiva entre objetos.

Para ver ejemplos, sugiero utilizar el compilador de código en línea repl.it.
Patrones de diseño en Java - 2

Patrones creacionales

Empecemos desde el principio del ciclo de vida de los objetos: con la creación de objetos. Las plantillas generativas ayudan a crear objetos de manera más conveniente y brindan flexibilidad en este proceso. Uno de los más famosos es " Constructor ". Este patrón le permite crear objetos complejos paso a paso. En Java, el ejemplo más famoso es StringBuilder:
class Main {
  public static void main(String[] args) {
    StringBuilder builder = new StringBuilder();
    builder.append("Hello");
    builder.append(',');
    builder.append("World!");
    System.out.println(builder.toString());
  }
}
Otro enfoque bien conocido para crear un objeto es mover la creación a un método separado. Este método se convierte, por así decirlo, en una fábrica de objetos. Por eso el patrón se llama " Método de fábrica ". En Java, por ejemplo, su efecto se puede ver en la clase java.util.Calendar. La clase en sí Calendares abstracta y para crearla se utiliza el método getInstance:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getTime());
    System.out.println(calendar.getClass().getCanonicalName());
  }
}
A menudo, esto se debe a que la lógica detrás de la creación de objetos puede ser compleja. Por ejemplo, en el caso anterior, accedemos a la clase base Calendary se crea una clase GregorianCalendar. Si miramos el constructor, podemos ver que se crean diferentes implementaciones dependiendo de las condiciones Calendar. Pero a veces un método de fábrica no es suficiente. A veces es necesario crear diferentes objetos para que encajen entre sí. Otra plantilla nos ayudará con esto: " Fábrica abstracta ". Y luego necesitamos crear diferentes fábricas en un solo lugar. Al mismo tiempo, la ventaja es que los detalles de implementación no son importantes para nosotros, es decir, No importa qué fábrica específica obtengamos. Lo principal es que crea las implementaciones correctas. Súper ejemplo:
Patrones de diseño en Java - 3
Es decir, dependiendo del entorno (sistema operativo), recibiremos una determinada fábrica que creará elementos compatibles. Como alternativa al enfoque de crear a través de otra persona, podemos utilizar el patrón " Prototipo ". Su esencia es simple: se crean nuevos objetos a imagen y semejanza de objetos ya existentes, es decir. según su prototipo. En Java, todo el mundo se ha encontrado con este patrón: este es el uso de una interfaz java.lang.Cloneable:
class Main {
  public static void main(String[] args) {
    class CloneObject implements Cloneable {
      @Override
      protected Object clone() throws CloneNotSupportedException {
        return new CloneObject();
      }
    }
    CloneObject obj = new CloneObject();
    try {
      CloneObject pattern = (CloneObject) obj.clone();
    } catch (CloneNotSupportedException e) {
      //Do something
    }
  }
}
Como puede ver, la persona que llama no sabe cómo funciona el archivo clone. Es decir, crear un objeto a partir de un prototipo es responsabilidad del propio objeto. Esto es útil porque no vincula al usuario a la implementación del objeto de plantilla. Bueno, el último de esta lista es el patrón "Singleton". Su propósito es simple: proporcionar una única instancia del objeto para toda la aplicación. Este patrón es interesante porque a menudo muestra problemas de subprocesos múltiples. Para una mirada más profunda, consulte estos artículos:
Patrones de diseño en Java - 4

Patrones estructurales

Con la creación de objetos quedó más claro. Ahora es el momento de observar los patrones estructurales. Su objetivo es construir jerarquías de clases y sus relaciones que sean fáciles de mantener. Uno de los primeros y más conocidos patrones es “ Diputado ” (Proxy). El proxy tiene la misma interfaz que el objeto real, por lo que no hay diferencia para el cliente si trabaja a través del proxy o directamente. El ejemplo más simple es java.lang.reflect.Proxy :
import java.util.*;
import java.lang.reflect.*;
class Main {
  public static void main(String[] arguments) {
    final Map<String, String> original = new HashMap<>();
    InvocationHandler proxy = (obj, method, args) -> {
      System.out.println("Invoked: " + method.getName());
      return method.invoke(original, args);
    };
    Map<String, String> proxyInstance = (Map) Proxy.newProxyInstance(
        original.getClass().getClassLoader(),
        original.getClass().getInterfaces(),
        proxy);
    proxyInstance.put("key", "value");
    System.out.println(proxyInstance.get("key"));
  }
}
Como puede ver, en el ejemplo que tenemos tenemos el original: este es el HashMapque implementa la interfaz Map. A continuación creamos un proxy que reemplaza al original HashMappara la parte del cliente, que llama a los métodos puty get, agregando nuestra propia lógica durante la llamada. Como podemos ver, la interacción en el patrón se produce a través de interfaces. Pero a veces un sustituto no es suficiente. Y luego se puede utilizar el patrón " Decorador ". A un decorador también se le llama envoltorio o envoltorio. El proxy y el decorador son muy similares, pero si miras el ejemplo, verás la diferencia:
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    List<String> list = new ArrayList<>();
    List<String> decorated = Collections.checkedList(list, String.class);
    decorated.add("2");
    list.add("3");
    System.out.println(decorated);
  }
}
A diferencia de un proxy, un decorador envuelve algo que se pasa como entrada. Un proxy puede aceptar lo que necesita ser proxy y también administrar la vida del objeto proxy (por ejemplo, crear un objeto proxy). Hay otro patrón interesante: " Adaptador ". Es similar a un decorador: el decorador toma un objeto como entrada y devuelve un contenedor sobre este objeto. La diferencia es que el objetivo no es cambiar la funcionalidad, sino adaptar una interfaz a otra. Java tiene un ejemplo muy claro de esto:
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    String[] array = {"One", "Two", "Three"};
    List<String> strings = Arrays.asList(array);
    strings.set(0, "1");
    System.out.println(Arrays.toString(array));
  }
}
En la entrada tenemos una matriz. A continuación, creamos un adaptador que lleva el array a la interfaz List. Cuando trabajamos con él, en realidad estamos trabajando con una matriz. Por lo tanto, agregar elementos no funcionará, porque... La matriz original no se puede cambiar. Y en este caso obtendremos UnsupportedOperationException. El siguiente enfoque interesante para desarrollar la estructura de clases es el patrón compuesto . Lo interesante es que un determinado conjunto de elementos que utilizan una interfaz están organizados en una determinada jerarquía en forma de árbol. Al llamar a un método en un elemento principal, recibimos una llamada a este método en todos los elementos secundarios necesarios. Un excelente ejemplo de este patrón es la interfaz de usuario (ya sea java.awt o JSF):
import java.awt.*;
class Main {
  public static void main(String[] arguments) {
    Container container = new Container();
    Component component = new java.awt.Component(){};
    System.out.println(component.getComponentOrientation().isLeftToRight());
    container.add(component);
    container.applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
    System.out.println(component.getComponentOrientation().isLeftToRight());
  }
}
Como podemos ver, hemos agregado un componente al contenedor. Y luego le pedimos al contenedor que aplicara la nueva orientación de los componentes. Y el contenedor, sabiendo en qué componentes se compone, delegó la ejecución de este comando a todos los componentes secundarios. Otro patrón interesante es el patrón " Puente ". Se llama así porque describe una conexión o puente entre dos jerarquías de clases diferentes. Una de estas jerarquías se considera una abstracción y la otra una implementación. Esto se destaca porque la abstracción en sí no realiza acciones, sino que delega esta ejecución a la implementación. Este patrón se utiliza a menudo cuando hay clases de "control" y varios tipos de clases de "plataforma" (por ejemplo, Windows, Linux, etc.). Con este enfoque, una de estas jerarquías (abstracción) recibirá una referencia a objetos de otra jerarquía (implementación) y les delegará el trabajo principal. Debido a que todas las implementaciones seguirán una interfaz común, se pueden intercambiar dentro de la abstracción. En Java, un claro ejemplo de esto es java.awt:
Patrones de diseño en Java - 5
Para obtener más información, consulte el artículo " Patrones en Java AWT ". Entre los patrones estructurales, también me gustaría destacar el patrón " Fachada ". Su esencia es ocultar la complejidad del uso de las bibliotecas/marcos detrás de esta API detrás de una interfaz conveniente y concisa. Por ejemplo, puede utilizar JSF o EntityManager de JPA como ejemplo. También existe otro patrón llamado " Flyweight ". Su esencia es que si diferentes objetos tienen el mismo estado, entonces se puede generalizar y almacenar no en cada objeto, sino en un solo lugar. Y luego cada objeto podrá hacer referencia a una parte común, lo que reducirá los costos de memoria para el almacenamiento. Este patrón a menudo implica el almacenamiento previo en caché o el mantenimiento de un grupo de objetos. Curiosamente, también conocemos este patrón desde el principio:
Patrones de diseño en Java - 6
Por la misma analogía, aquí se puede incluir un conjunto de cadenas. Puedes leer el artículo sobre este tema: " Patrón de diseño Flyweight ".
Patrones de diseño en Java - 7

Patrones de comportamiento

Entonces, descubrimos cómo se pueden crear objetos y cómo se pueden organizar las conexiones entre clases. Lo más interesante que queda es brindar flexibilidad para cambiar el comportamiento de los objetos. Y los patrones de comportamiento nos ayudarán con esto. Uno de los patrones mencionados con más frecuencia es el patrón " Estrategia ". Aquí es donde comienza el estudio de los patrones en el libro " La cabeza primero. Patrones de diseño ". Usando el patrón “Estrategia”, podemos almacenar dentro de un objeto cómo realizaremos la acción, es decir el objeto interno almacena una estrategia que se puede cambiar durante la ejecución del código. Este es un patrón que usamos a menudo cuando usamos un comparador:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Comparator<String> comparator = Comparator.comparingInt(String::length);
    Set dataSet = new TreeSet(comparator);
    dataSet.addAll(data);
    System.out.println("Dataset : " + dataSet);
  }
}
Antes que nosotros - TreeSet. Tiene el comportamiento de TreeSetmantener el orden de los elementos, es decir los ordena (ya que es un SortedSet). Este comportamiento tiene una estrategia predeterminada, que vemos en JavaDoc: ordenar en "orden natural" (para cadenas, este es el orden lexicográfico). Esto sucede si usa un constructor sin parámetros. Pero si queremos cambiar la estrategia, podemos pasar Comparator. En este ejemplo, podemos crear nuestro conjunto como new TreeSet(comparator)y luego el orden de almacenamiento de elementos (estrategia de almacenamiento) cambiará al especificado en el comparador. Curiosamente, existe casi el mismo patrón llamado " Estado ". El patrón "Estado" dice que si tenemos algún comportamiento en el objeto principal que depende del estado de este objeto, entonces podemos describir el estado mismo como un objeto y cambiar el objeto de estado. Y delegar llamadas del objeto principal al estado. Otro patrón que conocemos gracias al estudio de los conceptos básicos del lenguaje Java es el patrón " Comando ". Este patrón de diseño sugiere que diferentes comandos se pueden representar como clases diferentes. Este patrón es muy similar al patrón Estrategia. Pero en el patrón Estrategia, estábamos redefiniendo cómo se realizaría una acción específica (por ejemplo, ordenar TreeSet). En el patrón “Comando”, redefinimos qué acción se realizará. El comando de patrón está con nosotros todos los días cuando usamos hilos:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Runnable command = () -> {
      System.out.println("Command action");
    };
    Thread th = new Thread(command);
    th.start();
  }
}
Como puede ver, comando define una acción o comando que se ejecutará en un nuevo hilo. También vale la pena considerar el patrón de “ Cadena de responsabilidad ”. Este patrón también es muy sencillo. Este patrón dice que si es necesario procesar algo, entonces puede recopilar controladores en una cadena. Por ejemplo, este patrón se utiliza a menudo en servidores web. En la entrada, el servidor tiene alguna solicitud del usuario. Esta solicitud luego pasa por la cadena de procesamiento. Esta cadena de controladores incluye filtros (por ejemplo, no aceptar solicitudes de una lista negra de direcciones IP), controladores de autenticación (permitir solo usuarios autorizados), un controlador de encabezado de solicitud, un controlador de almacenamiento en caché, etc. Pero hay un ejemplo más simple y comprensible en Java java.util.logging:
import java.util.logging.*;
class Main {
  public static void main(String[] args) {
    Logger logger = Logger.getLogger(Main.class.getName());
    ConsoleHandler consoleHandler = new ConsoleHandler(){
		@Override
            public void publish(LogRecord record) {
                System.out.println("LogRecord обработан");
            }
        };
    logger.addHandler(consoleHandler);
    logger.info("test");
  }
}
Como puede ver, los controladores se agregan a la lista de controladores de registradores. Cuando un registrador recibe un mensaje para procesar, cada mensaje pasa a través de una cadena de controladores (de logger.getHandlers) para ese registrador. Otro patrón que vemos todos los días es el " Iterador ". Su esencia es separar una colección de objetos (es decir, una clase que representa una estructura de datos, por ejemplo List) y recorrer esta colección.
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Iterator<String> iterator = data.iterator();
    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }
  }
}
Como puede ver, el iterador no forma parte de la colección, sino que está representado por una clase separada que atraviesa la colección. Es posible que el usuario del iterador ni siquiera sepa sobre qué colección está iterando, es decir, ¿Qué colección está visitando? También vale la pena considerar el patrón Visitante . El patrón de visitante es muy similar al patrón de iterador. Este patrón le ayuda a evitar la estructura de los objetos y realizar acciones sobre estos objetos. Se diferencian más bien en el concepto. El iterador atraviesa la colección para que al cliente que lo utiliza no le importe qué contiene la colección, solo los elementos de la secuencia son importantes. El visitante significa que existe una determinada jerarquía o estructura de los objetos que visitamos. Por ejemplo, podemos utilizar el procesamiento de directorios por separado y el procesamiento de archivos por separado. Java tiene una implementación lista para usar de este patrón en la forma java.nio.file.FileVisitor:
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
class Main {
  public static void main(String[] args) {
    SimpleFileVisitor visitor = new SimpleFileVisitor() {
      @Override
      public FileVisitResult visitFile(Object file, BasicFileAttributes attrs) throws IOException {
        System.out.println("File:" + file.toString());
        return FileVisitResult.CONTINUE;
      }
    };
    Path pathSource = Paths.get(System.getProperty("java.io.tmpdir"));
    try {
      Files.walkFileTree(pathSource, visitor);
    } catch (AccessDeniedException e) {
      // skip
    } catch (IOException e) {
      // Do something
    }
  }
}
A veces es necesario que algunos objetos reaccionen a los cambios en otros objetos, y entonces el patrón "Observador" nos ayudará . La forma más conveniente es proporcionar un mecanismo de suscripción que permita a algunos objetos monitorear y responder a eventos que ocurren en otros objetos. Este patrón se utiliza a menudo en varios oyentes y observadores que reaccionan ante diferentes eventos. Como ejemplo sencillo, podemos recordar la implementación de este patrón de la primera versión de JDK:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Observer observer = (obj, arg) -> {
      System.out.println("Arg: " + arg);
    };
    Observable target = new Observable(){
      @Override
      public void notifyObservers(Object arg) {
        setChanged();
        super.notifyObservers(arg);
      }
    };
    target.addObserver(observer);
    target.notifyObservers("Hello, World!");
  }
}
Hay otro patrón de comportamiento útil: " Mediador ". Es útil porque en sistemas complejos ayuda a eliminar la conexión entre diferentes objetos y delegar todas las interacciones entre objetos a algún objeto, que es un intermediario. Una de las aplicaciones más llamativas de este patrón es Spring MVC, que utiliza este patrón. Puede leer más sobre esto aquí: " Spring: Mediator Pattern ". A menudo puedes ver lo mismo en los ejemplos java.util.Timer:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Timer mediator = new Timer("Mediator");
    TimerTask command = new TimerTask() {
      @Override
      public void run() {
        System.out.println("Command pattern");
        mediator.cancel();
      }
    };
    mediator.schedule(command, 1000);
  }
}
El ejemplo se parece más a un patrón de comando. Y la esencia del patrón "Mediador" está oculta en la implementación de Timer'a. Dentro del temporizador hay una cola de tareas TaskQueuey un hilo TimerThread. Nosotros, como clientes de esta clase, no interactuamos con ellos, sino que interactuamos con Timerel objeto que, en respuesta a nuestra llamada a sus métodos, accede a los métodos de otros objetos de los que es intermediario. Externamente puede parecer muy similar a "Fachada". Pero la diferencia es que cuando se utiliza una fachada, los componentes no saben que la fachada existe y se comunican entre sí. Y cuando se utiliza "Mediador", los componentes conocen y utilizan al intermediario, pero no se contactan directamente entre sí. Vale la pena considerar el patrón " Método de plantilla ", que se desprende claramente de su nombre. La conclusión es que el código está escrito de tal manera que a los usuarios del código (desarrolladores) se les proporciona alguna plantilla de algoritmo, cuyos pasos pueden redefinirse. Esto permite a los usuarios del código no escribir el algoritmo completo, sino pensar solo en cómo realizar correctamente uno u otro paso de este algoritmo. Por ejemplo, Java tiene una clase abstracta AbstractListque define el comportamiento de un iterador mediante List. Sin embargo, el propio iterador utiliza métodos de hoja como: get, set, remove. El comportamiento de estos métodos lo determina el desarrollador de los descendientes AbstractList. Por lo tanto, el iterador en AbstractList- es una plantilla para el algoritmo para iterar sobre una hoja. Y los desarrolladores de implementaciones específicas AbstractListcambian el comportamiento de esta iteración definiendo el comportamiento de pasos específicos. El último de los patrones que analizamos es el patrón “ Snapshot ” (Momento). Su esencia es preservar un determinado estado de un objeto con la capacidad de restaurar este estado. El ejemplo más reconocible del JDK es la serialización de objetos, es decir. java.io.Serializable. Veamos un ejemplo:
import java.io.*;
import java.util.*;
class Main {
  public static void main(String[] args) throws IOException {
    ArrayList<String> list = new ArrayList<>();
    list.add("test");
    // Save State
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    try (ObjectOutputStream out = new ObjectOutputStream(stream)) {
      out.writeObject(list);
    }
    // Load state
    byte[] bytes = stream.toByteArray();
    InputStream inputStream = new ByteArrayInputStream(bytes);
    try (ObjectInputStream in = new ObjectInputStream(inputStream)) {
      List<String> listNew = (List<String>) in.readObject();
      System.out.println(listNew.get(0));
    } catch (ClassNotFoundException e) {
      // Do something. Can't find class fpr saved state
    }
  }
}
Patrones de diseño en Java - 8

Conclusión

Como vimos en la revisión, existe una gran variedad de patrones. Cada uno de ellos resuelve su propio problema. Y conocer estos patrones puede ayudarle a comprender a tiempo cómo escribir su sistema para que sea flexible, mantenible y resistente al cambio. Y finalmente, algunos enlaces para profundizar más: #viacheslav
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION