Patrones de diseño en Java parte 1, Patrones de diseño creacionales(Factory method, Singleton y Builder pattern)


Los patrones de diseño fueron desarrollados como soluciones comunes a problemas comunes que enfrentan todos los desarrolladores. Existe una clasificación en los patrones de diseño basados en su propósito, estos pueden ser uno de los siguientes:

  • Creacionales
  • Estructurales
  • Comportamiento

En este post nos enfocaremos en los patrones creacionales así como su simple implementación en Java.

Factory method

Factory method es un patrón de diseño que centraliza la creación de objetos que siguen una estructura jerárquica y está compuesto por los siguientes elementos:

  • Una clase constructora de objetos: En el ejemplo del post será SpritesFactory
  • El producto a construir: En el ejemplo serán los objetos Java construidos por la aplicación

Este ejemplo construirá objetos bajo la siguiente estructura jerárquica :

factory

Definiendo el modelo de la aplicación

/**
 *
 */
package com.raidentrance.model.stripes;

import java.awt.Graphics;

/**
 * @author raidentrance
 *
 */
public <strong>abstract</strong> class Sprite {

	protected int x;
	protected int y;
	protected int width;
	protected int heigth;

	public abstract void draw(Graphics g);

	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getY() {
		return y;
	}

	public void setY(int y) {
		this.y = y;
	}

	public int getWidth() {
		return width;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public int getHeigth() {
		return heigth;
	}

	public void setHeigth(int heigth) {
		this.heigth = heigth;
	}

}

La clase Sprites es una clase abstracta en la cuál se definen métodos genéricos para todos los Sprites getX(), setX(int x), getY(), setY(int y), getWidth(), setWidth(int width), getHeigth() y  setHeigth() así como un método abstracto que será diferente para cáda una de las implementaciones draw(Graphics g).

/**
 *
 */
package com.raidentrance.model.stripes;

import java.awt.Graphics;

/**
 * @author raidentrance
 *
 */
public class Hero extends Sprite {

	@Override
	public void draw(Graphics g) {
		System.out.println("Drawing a hero");
	}
}
/**
 *
 */
package com.raidentrance.model.stripes;

import java.awt.Graphics;

/**
 * @author raidentrance
 *
 */
public class BadBoy extends Sprite {

	@Override
	public void draw(Graphics g) {
		System.out.println("Drawing a bad boy");
	}
}

Hero.java y BadBoy.java son dos subclases de Sprite.java y se utilizará el patrón de diseño Factory method para crear sus instancias.

Creando la clase constructora de objetos

El siguiente paso es crear la clase responsable de seguir el patrón de diseño Factory method la cual será la responsable de crear las instancias en la aplicación:

/**
 *
 */
package com.raidentrance.factory;

import com.google.common.base.Preconditions;
import com.raidentrance.model.stripes.BadBoy;
import com.raidentrance.model.stripes.Hero;
import com.raidentrance.model.stripes.Sprite;

/**
 * @author raidentrance
 *
 */
public class SpritesFactory {
	public static enum SpriteType {
		HERO, BAD_BOY
	}

	public static Sprite createStripe(SpriteType type) {
		Preconditions.checkNotNull(type);
		switch (type) {
		case HERO:
			return new Hero();
		case BAD_BOY:
			return new BadBoy();
		}
		throw new IllegalArgumentException();
	}
}

Hay dos puntos importantes a analizar en la clase SpritesFactory, el primero es que contiene una enumeración definiendo los posibles tipos de objeto a construit y el segundo es el método  createStripes(StripeType type) el cuál recibe como parámetro un SpriteType y dependiendo de este devuelve su instancia correspondiente.

Probando todo junto

El último paso es probar el código y ver cómo se insanciarán los objetos.

/**
 *
 */
package com.raidentrance.factory;

import com.raidentrance.model.stripes.Sprite;

/**
 * @author raidentrance
 *
 */
public class Sample {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Sprite sprite = SpritesFactory.createStripe(SpritesFactory.SpriteType.HERO);
		sprite.draw(null);
		Sprite sprite2 = SpritesFactory.createStripe(SpritesFactory.SpriteType.BAD_BOY);
		sprite2.draw(null);
	}
}

Salida

Drawing a hero
Drawing a bad boy

Singleton pattern

El patrón de diseño singleton es utilizado para definir una clase que tiene una sola instancia y provee un solo punto de acceso a esta. En este ejemplo se creará una clase Singleton para definir las configuraciones de una aplicación.

Definiendo clases para manejo de errores

/**
 *
 */
package com.raidentrance.commons;

/**
 * @author raidentrance
 *
 */
public enum ErrorResult {
	EMPTRY_RESULT("%s cannot be null"),
	INVALID_ARGUMENT("%s is not a valid type");
	private String message;

	private ErrorResult(String message) {
		this.message = message;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

}
/**
 *
 */
package com.raidentrance.model;

/**
 * @author raidentrance
 *
 */
public class BussinessException extends Exception {

	private static final long serialVersionUID = 1788240335231977221L;

	public BussinessException(String msg) {
		super(msg);
	}

}

Definiendo clase Singleton

/**
 *
 */
package com.raidentrance.singleton;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import com.google.common.base.Preconditions;
import com.raidentrance.commons.ErrorResult;
import com.raidentrance.model.BussinessException;

/**
 * @author raidentrance
 *
 */
public class Settings {
	private String host;
	private int port;
	private String context;
	private String username;
	private String password;

	private static Settings instance = new Settings();

	private Settings() {
	}

	public static Settings getInstance() {
		return instance;
	}

	public String getHost() {
		return host;
	}

	public void setHost(String host) {
		this.host = host;
	}

	public int getPort() {
		return port;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public String getContext() {
		return context;
	}

	public void setContext(String context) {
		this.context = context;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getUrl() {
		Preconditions.checkNotNull(host,
				new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "host")));
		return host + ":" + port + context;
	}

	public String getCredentials() {
		return "Basic ".concat(Base64.getEncoder()
				.encodeToString(username.concat(":").concat(password).getBytes(StandardCharsets.UTF_8)));
	}
}

Puntos importantes a analizar:

  • Constructor privado: Es necesario definir un constructor privado ya que de este modo limitamos a solo crear objetos de esta clase dentro de ella misma.
  • Instancia private y static : Es necesario que la instancia que se utilizará sea privada y static ya que de este modo forzamos a solo tener una instancia y a que solo se pueda acceder a esta dentro de ella misma.
  • Método getInstance() : Único punto de acceso a la instancia.

Probando todo junto

/**
 *
 */
package com.raidentrance.singleton;

/**
 * @author raidentrance
 *
 */
public class Sample {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Settings settings = Settings.getInstance();
		System.out.println("Setting default settings.");
		settings.setHost("http://localhost");
		settings.setPort(8080);
		settings.setContext("/rest");
		settings.setUsername("user");
		settings.setPassword("SecretPassword");

		System.out.println(Settings.getInstance().getUrl());
		System.out.println(Settings.getInstance().getCredentials());

		System.out.println("Updating settings.");
		settings = Settings.getInstance();
		settings.setHost("http://192.168.5.1");
		settings.setUsername("prodUser");
		settings.setPassword("UltraSecretPassword");

		System.out.println(Settings.getInstance().getUrl());
		System.out.println(Settings.getInstance().getCredentials());
	}
}

Salida

Setting default settings.
http://localhost:8080/rest
Basic dXNlcjpTZWNyZXRQYXNzd29yZA==
Updating settings.
http://72.51.22.13:8080/rest
Basic cHJvZFVzZXI6VWx0cmFTZWNyZXRQYXNzd29yZA==

Como se puede observar, no importa cuantas veces se mande llamar el método getInstance(), este siempre devolverá el mismo objeto.

Builder Pattern

El patrón de diseño builder es utilizado para construir objetos complejos utilizando un enfoque paso a paso.

Definiendo el modelo de la aplicación

/**
 *
 */
package com.raidentrance.model;

import java.util.Date;

/**
 * @author raidentrance
 *
 */
public class Person {
	private String name;
	private Date birthDate;
	private int age;

	public Person() {
	}

	public Person(String name, Date birthDate, int age) {
		super();
		this.name = name;
		this.birthDate = birthDate;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Date getBirthDate() {
		return birthDate;
	}

	public void setBirthDate(Date birthDate) {
		this.birthDate = birthDate;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	@Override
	public String toString() {
		return "Person [name=" + name + ", birthDate=" + birthDate + ", age=" + age + "]";
	}
}
/**
 *
 */
package com.raidentrance.model;

/**
 * @author raidentrance
 *
 */
public class Book {
	private String isbn;
	private String title;
	private String author;
	private double price;

	public Book() {
	}

	public Book(String isbn, String title, String author, double price) {
		super();
		this.isbn = isbn;
		this.title = title;
		this.author = author;
		this.price = price;
	}

	public String getIsbn() {
		return isbn;
	}

	public void setIsbn(String isbn) {
		this.isbn = isbn;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getAuthor() {
		return author;
	}

	public void setAuthor(String author) {
		this.author = author;
	}

	public double getPrice() {
		return price;
	}

	public void setPrice(double price) {
		this.price = price;
	}

	@Override
	public String toString() {
		return "Book [isbn=" + isbn + ", title=" + title + ", author=" + author + ", price=" + price + "]";
	}

}

Helper para cálculo de edad

La siguiente clase se utilizará para realizar el calculo de la edad de una persona con base en su fecha de nacimiento.

/**
 *
 */
package com.raidentrance.commons;

import java.util.Calendar;
import java.util.Date;

/**
 * @author raidentrance
 *
 */
public class TimeHelper {
	public static int getAge(Date birthDate) {
		Calendar dob = Calendar.getInstance();
		dob.setTime(birthDate);
		Calendar today = Calendar.getInstance();
		int age = today.get(Calendar.YEAR) - dob.get(Calendar.YEAR);
		if (today.get(Calendar.MONTH) < dob.get(Calendar.MONTH)) {
			age--;
		} else if (today.get(Calendar.MONTH) == dob.get(Calendar.MONTH)
				&& today.get(Calendar.DAY_OF_MONTH) < dob.get(Calendar.DAY_OF_MONTH)) {
			age--;
		}
		return age;
	}
}

Creando un abstract builder

Un abstract buider permitirá seguir la misma estructura en todos los builders que se desee desarrollar, en este ejemplo se crearán dos builders, uno para la creación de personas y el otro para la creación de libros y ambos deben heredar de AbstractBuilder.

/**
 *
 */
package com.raidentrance.builder.commons;

/**
 * @author raidentrance
 *
 */
public abstract class AbstractBuilder<T> {
	protected T instance;

	public AbstractBuilder(T instance) {
		this.instance = instance;
	}

	public T build() {
		inject();
		validate();
		return instance;
	}

	public abstract void validate();

	public abstract void inject();
}

Puntos importantes en la clase AbstractBuilder :

  • Cuenta con una instancia genérica la cuál contendrá el objeto que se desea construir
  • Recibe como parámetro dicha instancia
  • Cuenta con 3 métodos importantes
    • inject(): Será utilizado para generar valores en base a los atributos que se tiene
    • validate():Valida que se cumplan con los requisitos para decir que el objeto está bien construido.

Creando builders

PersonBuilder.java

/**
 *
 */
package com.raidentrance.builder;

import java.util.Date;

import com.google.common.base.Preconditions;
import com.raidentrance.builder.commons.AbstractBuilder;
import com.raidentrance.commons.ErrorResult;
import com.raidentrance.commons.TimeHelper;
import com.raidentrance.model.BussinessException;
import com.raidentrance.model.Person;

/**
 * @author raidentrance
 *
 */
public class PersonBuilder extends AbstractBuilder<Person> {

	public PersonBuilder() {
		super(new Person());
	}

	public PersonBuilder setName(String name) {
		instance.setName(name);
		return this;
	}

	public PersonBuilder setBirthDate(Date birthDate) {
		instance.setBirthDate(birthDate);
		return this;
	}

	@Override
	public void validate() {
		Preconditions.checkNotNull(instance.getName(),
				new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "Name")));
		Preconditions.checkNotNull(instance.getBirthDate(),
				new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "Birth date")));
		Preconditions.checkNotNull(instance.getAge(),
				new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "Age")));
	}

	@Override
	public void inject() {
		Preconditions.checkNotNull(instance.getBirthDate(),
				new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "Birth date")));
		instance.setAge(TimeHelper.getAge(instance.getBirthDate()));
	}
}

Puntos importantes en la clase PersonBuilder :

  • El constructor de PersonBuilder envía como parámetro al constructor de AbstractBuilder una instancia nueva de Person
  • Cada método set devuelve una instancia de tipo PersonBuilder
  • El método inject() es utilizado para generar el valor de la edad con base en la fecha de nacimiento
  • El método validate() Valida que los datos no sean nulos
  • El método build() definido en AbstractBuilder devuelve una instancia completa y correcta.

BookBuilder.java

package com.raidentrance.builder;

import com.google.common.base.Preconditions;
import com.raidentrance.builder.commons.AbstractBuilder;
import com.raidentrance.commons.ErrorResult;
import com.raidentrance.model.Book;
import com.raidentrance.model.BussinessException;

/**
 * @author raidentrance
 *
 */
public class BookBuilder extends AbstractBuilder<Book> {

	public BookBuilder() {
		super(new Book());
	}

	public BookBuilder setIsbn(String isbn) {
		instance.setIsbn(isbn);
		return this;
	}

	public BookBuilder setTitle(String title) {
		instance.setTitle(title);
		return this;
	}

	public BookBuilder setAuthor(String author) {
		instance.setAuthor(author);
		return this;
	}

	public BookBuilder setPrice(double price) {
		instance.setPrice(price);
		return this;
	}

	@Override
	public void validate() {
		Preconditions.checkNotNull(instance.getIsbn(),
				new BussinessException(String.format(ErrorResult.EMPTRY_RESULT.getMessage(), "Isbn")));
	}

	@Override
	public void inject() {

	}
}

Puedes encontrar el código completo en el siguiente link: https://github.com/raidentrance/design-patterns

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contacto:raidentrance@gmail.com

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s