Los 3 tipos de patrones de diseño que todos los desarrolladores deben conocer (con ejemplos de código de cada uno)

¿Qué es un patrón de diseño?

Los patrones de diseño son soluciones a nivel de diseño para problemas recurrentes con los que los ingenieros de software nos encontramos con frecuencia. No es un código, repito,CÓDIGO . Es como una descripción de cómo abordar estos problemas y diseñar una solución.

El uso de estos patrones se considera una buena práctica, ya que el diseño de la solución está bastante probado, lo que resulta en una mayor legibilidad del código final. Los patrones de diseño a menudo se crean y utilizan para lenguajes de programación orientada a objetos, como Java, en el que se escribirán la mayoría de los ejemplos de aquí en adelante.

Tipos de patrones de diseño

Hay alrededor de 26 patrones descubiertos actualmente (no creo que los vaya a hacer todos…).

Estos 26 se pueden clasificar en 3 tipos:

1. Creacional: estos patrones están diseñados para la instanciación de clases. Pueden ser patrones de creación de clases o patrones de creación de objetos.

2. Estructural: estos patrones están diseñados con respecto a la estructura y composición de una clase. El objetivo principal de la mayoría de estos patrones es aumentar la funcionalidad de la (s) clase (s) involucradas, sin cambiar gran parte de su composición.

3. Comportamiento: estos patrones se diseñan en función de cómo una clase se comunica con otras.

En esta publicación, repasaremos un patrón de diseño básico para cada tipo clasificado.

Tipo 1: Creacional - El patrón de diseño Singleton

El patrón de diseño Singleton es un patrón de creación, cuyo objetivo es crear solo una instancia de una clase y proporcionar solo un punto de acceso global a ese objeto. Un ejemplo comúnmente utilizado de una clase de este tipo en Java es Calendar, donde no puede crear una instancia de esa clase. También utiliza su propio getInstance()método para conseguir que se utilice el objeto.

Una clase que usa el patrón de diseño singleton incluirá,

  1. Una variable estática privada, que contiene la única instancia de la clase.
  2. Un constructor privado, por lo que no se puede crear una instancia en ningún otro lugar.
  3. Un método estático público, para devolver la única instancia de la clase.

Hay muchas implementaciones diferentes de diseño singleton. Hoy, revisaré las implementaciones de;

1. Instancia ansiosa

2. Instanciación perezosa

3. Creación de instancias segura para subprocesos

Currante

public class EagerSingleton { // create an instance of the class. private static EagerSingleton instance = new EagerSingleton(); // private constructor, so it cannot be instantiated outside this class. private EagerSingleton() { } // get the only instance of the object created. public static EagerSingleton getInstance() { return instance; } }

Este tipo de instanciación ocurre durante la carga de clases, ya que la instanciación de la instancia de variable ocurre fuera de cualquier método. Esto plantea un gran inconveniente si la aplicación cliente no está utilizando esta clase. El plan de contingencia, si no se utiliza esta clase, es la instanciación perezosa.

Días de descanso

No hay mucha diferencia con la implementación anterior. Las principales diferencias son que la variable estática se declara inicialmente nula y solo se instancia dentro del getInstance()método si, y solo si, la variable de instancia permanece nula en el momento de la verificación.

public class LazySingleton { // initialize the instance as null. private static LazySingleton instance = null; // private constructor, so it cannot be instantiated outside this class. private LazySingleton() { } // check if the instance is null, and if so, create the object. public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }

Esto soluciona un problema, pero aún existe otro. ¿Qué pasa si dos clientes diferentes acceden a la clase Singleton al mismo tiempo, hasta el milisegundo? Bueno, comprobarán si la instancia es nula al mismo tiempo, y la encontrarán verdadera, por lo que crearán dos instancias de la clase para cada solicitud de los dos clientes. Para solucionar este problema, se implementará la creación de instancias de Thread Safe.

(Hilo) La seguridad es clave

En Java, la palabra clave sincronizada se usa en métodos u objetos para implementar la seguridad de subprocesos, de modo que solo un subproceso accederá a un recurso en particular a la vez. La instanciación de la clase se coloca dentro de un bloque sincronizado para que solo un cliente pueda acceder al método en un momento dado.

public class ThreadSafeSingleton { // initialize the instance as null. private static ThreadSafeSingleton instance = null; // private constructor, so it cannot be instantiated outside this class. private ThreadSafeSingleton() { } // check if the instance is null, within a synchronized block. If so, create the object public static ThreadSafeSingleton getInstance() { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } return instance; } }

La sobrecarga del método sincronizado es alta y reduce el rendimiento de toda la operación.

Por ejemplo, si ya se ha creado una instancia de la variable de instancia, cada vez que un cliente accede al getInstance()método, el synchronizedmétodo se ejecuta y el rendimiento disminuye. Esto solo sucede para verificar si el instancevalor de las variables es nulo. Si encuentra que lo es, abandona el método.

Para reducir esta sobrecarga, se utiliza doble bloqueo. La verificación también se usa antes del synchronizedmétodo, y si el valor es nulo solo, se synchronizedejecuta el método.

// double locking is used to reduce the overhead of the synchronized method public static ThreadSafeSingleton getInstanceDoubleLocking() { if (instance == null) { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } } return instance; }

Ahora en la siguiente clasificación.

Tipo 2: Estructural: el patrón de diseño del decorador

Te voy a dar un pequeño escenario para darte un mejor contexto de por qué y dónde debes usar el Patrón Decorador.

Digamos que tienes una cafetería y, como cualquier novato, comienzas con solo dos tipos de café puro, el de la casa y el tostado oscuro. En su sistema de facturación, había una clase para las diferentes mezclas de café, que hereda la clase abstracta de bebidas. De hecho, la gente comienza a venir y tomar su maravilloso (¿aunque amargo?) Café. Luego están los novatos del café que, Dios no lo quiera, quieren azúcar o leche. ¡Qué farsa para el café! ??

Ahora necesita tener esos dos complementos también, tanto en el menú como, desafortunadamente, en el sistema de facturación. Originalmente, su persona de TI creará una subclase para ambos cafés, uno con azúcar y el otro con leche. Entonces, dado que los clientes siempre tienen la razón, uno dice estas temidas palabras:

"¿Puedo tomar un café con leche, con azúcar, por favor?"

???

Ahí va su sistema de facturación riéndose en su cara nuevamente. Bueno, volvamos a la mesa de dibujo….

The IT person then adds milk coffee with sugar as another subclass to each parent coffee class. The rest of the month is smooth sailing, people lining up to have your coffee, you actually making money. ??

But wait, there’s more!

The world is against you once again. A competitor opens up across the street, with not just 4 types of coffee, but more than 10 add-ons as well! ?

You buy all those and more, to sell better coffee yourself, and just then remember that you forgot to update that dratted billing system. You quite possibly cannot make the infinite number of subclasses for any and all combinations of all the add-ons, with the new coffee blends too. Not to mention, the size of the final system.??

Time to actually invest in a proper billing system. You find new IT personnel, who actually knows what they are doing and they say;

“Why, this will be so much easier and smaller if it used the decorator pattern.”

What on earth is that?

The decorator design pattern falls into the structural category, that deals with the actual structure of a class, whether is by inheritance, composition or both. The goal of this design is to modify an objects’ functionality at runtime. This is one of the many other design patterns that utilize abstract classes and interfaces with composition to get its desired result.

Let’s give Math a chance (shudder?) to bring this all into perspective;

Take 4 coffee blends and 10 add-ons. If we stuck to the generation of subclasses for each different combination of all the add-ons for one type of coffee. That’s;

(10–1)² = 9² = 81 subclasses

We subtract 1 from the 10, as you cannot combine one add-on with another of the same type, sugar with sugar sounds stupid. And that’s for just one coffee blend. Multiply that 81 by 4 and you get a whopping 324 different subclasses! Talk about all that coding…

But with the decorator pattern will require only 16 classes in this scenario. Wanna bet?

If we map out our scenario according to the class diagram above, we get 4 classes for the 4 coffee blends, 10 for each add-on and 1 for the abstract component and 1 more for the abstract decorator. See! 16! Now hand over that $100.?? (jk, but it will not be refused if given… just saying)

As you can see from above, just as the concrete coffee blends are subclasses of the beverage abstract class, the AddOn abstract class also inherits its methods from it. The add-ons, that are its subclasses, in turn inherit any new methods to add functionality to the base object when needed.

Let’s get to coding, to see this pattern in use.

First to make the Abstract beverage class, that all the different coffee blends will inherit from:

public abstract class Beverage { private String description; public Beverage(String description) { super(); this.description = description; } public String getDescription() { return description; } public abstract double cost(); }

Then to add both the concrete coffee blend classes.

public class HouseBlend extends Beverage { public HouseBlend() { super(“House blend”); } @Override public double cost() { return 250; } } public class DarkRoast extends Beverage { public DarkRoast() { super(“Dark roast”); } @Override public double cost() { return 300; } }

The AddOn abstract class also inherits from the Beverage abstract class (more on this below).

public abstract class AddOn extends Beverage { protected Beverage beverage; public AddOn(String description, Beverage bev) { super(description); this.beverage = bev; } public abstract String getDescription(); }

And now the concrete implementations of this abstract class:

public class Sugar extends AddOn { public Sugar(Beverage bev) { super(“Sugar”, bev); } @Override public String getDescription() { return beverage.getDescription() + “ with Mocha”; } @Override public double cost() { return beverage.cost() + 50; } } public class Milk extends AddOn { public Milk(Beverage bev) { super(“Milk”, bev); } @Override public String getDescription() { return beverage.getDescription() + “ with Milk”; } @Override public double cost() { return beverage.cost() + 100; } }

As you can see above, we can pass any subclass of Beverage to any subclass of AddOn, and get the added cost as well as the updated description. And, since the AddOn class is essentially of type Beverage, we can pass an AddOn into another AddOn. This way, we can add any number of add-ons to a specific coffee blend.

Now to write some code to test this out.

public class CoffeeShop { public static void main(String[] args) { HouseBlend houseblend = new HouseBlend(); System.out.println(houseblend.getDescription() + “: “ + houseblend.cost()); Milk milkAddOn = new Milk(houseblend); System.out.println(milkAddOn.getDescription() + “: “ + milkAddOn.cost()); Sugar sugarAddOn = new Sugar(milkAddOn); System.out.println(sugarAddOn.getDescription() + “: “ + sugarAddOn.cost()); } }

The final result is:

It works! We were able to add more than one add-on to a coffee blend and successfully update its final cost and description, without the need to make infinite subclasses for each add-on combination for all coffee blends.

Finally, to the last category.

Type 3: Behavioral - The Command Design Pattern

A behavioral design pattern focuses on how classes and objects communicate with each other. The main focus of the command pattern is to inculcate a higher degree of loose coupling between involved parties (read: classes).

Uhhhh… What’s that?

Coupling is the way that two (or more) classes that interact with each other, well, interact. The ideal scenario when these classes interact is that they do not depend heavily on each other. That’s loose coupling. So, a better definition for loose coupling would be, classes that are interconnected, making the least use of each other.

The need for this pattern arose when requests needed to be sent without consciously knowing what you are asking for or who the receiver is.

In this pattern, the invoking class is decoupled from the class that actually performs an action. The invoker class only has the callable method execute, which runs the necessary command, when the client requests it.

Let’s take a basic real-world example, ordering a meal at a fancy restaurant. As the flow goes, you give your order (command) to the waiter (invoker), who then hands it over to the chef(receiver), so you can get food. Might sound simple… but a bit meh to code.

The idea is pretty simple, but the coding goes around the nose.

The flow of operation on the technical side is, you make a concrete command, which implements the Command interface, asking the receiver to complete an action, and send the command to the invoker. The invoker is the person that knows when to give this command. The chef is the only one who knows what to do when given the specific command/order. So, when the execute method of the invoker is run, it, in turn, causes the command objects’ execute method to run on the receiver, thus completing necessary actions.

What we need to implement is;

  1. An interface Command
  2. A class Order that implements Command interface
  3. A class Waiter (invoker)
  4. A class Chef (receiver)

So, the coding goes like this:

Chef, the receiver

public class Chef { public void cookPasta() { System.out.println(“Chef is cooking Chicken Alfredo…”); } public void bakeCake() { System.out.println(“Chef is baking Chocolate Fudge Cake…”); } }

Command, the interface

public interface Command { public abstract void execute(); }

Order, the concrete command

public class Order implements Command { private Chef chef; private String food; public Order(Chef chef, String food) { this.chef = chef; this.food = food; } @Override public void execute() { if (this.food.equals(“Pasta”)) { this.chef.cookPasta(); } else { this.chef.bakeCake(); } } }

Waiter, the invoker

public class Waiter { private Order order; public Waiter(Order ord) { this.order = ord; } public void execute() { this.order.execute(); } }

You, the client

public class Client { public static void main(String[] args) { Chef chef = new Chef(); Order order = new Order(chef, “Pasta”); Waiter waiter = new Waiter(order); waiter.execute(); order = new Order(chef, “Cake”); waiter = new Waiter(order); waiter.execute(); } }

As you can see above, the Client makes an Order and sets the Receiver as the Chef. The Order is sent to the Waiter, who will know when to execute the Order (i.e. when to give the chef the order to cook). When the invoker is executed, the Orders’ execute method is run on the receiver (i.e. the chef is given the command to either cook pasta ? or bake cake?).

Quick recap

In this post we went through:

  1. What a design pattern really is,
  2. The different types of design patterns and why they are different
  3. One basic or common design pattern for each type

I hope this was helpful.  

Find the code repo for the post, here.