Implementa búsquedas sobre Grafos utilizando DFS (Depth First Search) con Java


DFS (Depth First Search) es un algoritmo recursivo para la búsqueda sobre grafos, en este post se explicará como implementarlo con Java, se tomará como base el post Implementa un grafo de ciudades en Java.

Creando clases base para la construcción del grafo

El grafo que se creará contendrá información de ciudades y la distancia entre ellas, para hacerlo necesitaremos 3 clases Node, Edge y Graph. A continuación se mostrará cada una de ellas con su explicación:

Node.java


import java.util.ArrayList;
import java.util.List;

/**
 * @author raidentrance
 *
 */
public class Node {
	private String city;
	private List adjacents = new ArrayList();

	public Node(String city) {
		this.city = city;
	}

	public void addEdge(Edge edge) {
		adjacents.add(edge);
	}

	public List getAdjacents() {
		return adjacents;
	}

	public String getCity() {
		return city;
	}

	@Override
	public String toString() {
		return "Node [city=" + city + ", adjacents=" + adjacents + "]";
	}
}

La clase Node representará una ciudad en nuestro grafo, como se puede ver la ciudad contiene una lista de Edges los cuáles representarán las uniones entre los nodos, más adelante se darán más detalles sobre la clase Edge.
Edge.java


/**
 * @author raidentrance
 *
 */
public class Edge {
	private Node origin;
	private Node destination;
	private double distance;

	public Edge(Node origin, Node destination, double distance) {
		this.origin = origin;
		this.destination = destination;
		this.distance = distance;
	}

	public Node getOrigin() {
		return origin;
	}

	public void setOrigin(Node origin) {
		this.origin = origin;
	}

	public Node getDestination() {
		return destination;
	}

	public void setDestination(Node destination) {
		this.destination = destination;
	}

	public double getDistance() {
		return distance;
	}

	public void setDistance(double distance) {
		this.distance = distance;
	}

	@Override
	public String toString() {
		return "Edge [origin=" + origin.getCity() + ", destination=" + destination.getCity() + ", distance=" + distance
				+ "]";
	}
}

La clase Edge representa la unión entre dos nodos, como se puede ver contiene un nodo origen, uno destino y la distancia entre ambos.
Graph.java


import java.util.ArrayList;
import java.util.List;

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

	private List nodes = new ArrayList();

	public void addNode(Node node) {
		nodes.add(node);
	}

	public List getNodes() {
		return nodes;
	}

	@Override
	public String toString() {
		return "Graph [nodes=" + nodes + "]";
	}
}

La clase Graph representará el conjunto de las ciudades junto con sus uniones y direcciones.

Creando el grafo

Una vez que contamos con las clases necesarias para crear nuestro grafo el siguiente paso será llenarlo con la información de las ciudades que utilizaremos:

cities-graph

Analicemos el grafo:

  • Es posible ir del df a Toluca y a cuernavaca
  • Es posible ir de Toluca a Tlaxcala, Puebla, DF y Cuernavaca
  • Es posible ir de Cuernavaca al DF y a Puebla
  • Es posible ir de Puebla a Toluca y Tlaxcala
  • En este ejemplo solo podemos llegar a Tlaxcala pero no podemos ir a ningún otro lugar
  • En este ejemplo existe la ciudad de Cancún pero no hay una ruta hacia ella.

Veamos el código para construirlo:

MapBuilder.java


import com.raidentrance.graphs.Edge;
import com.raidentrance.graphs.Graph;
import com.raidentrance.graphs.Node;

/**
 * @author raidentrance
 *
 */
public class MapBuilder {
	private static final Graph instance = new Graph();

	private MapBuilder() {
	}

	public static Graph getGraph() {
		Node df = new Node("DF");
		Node toluca = new Node("Toluca");
		Node cuernavaca = new Node("Cuernavaca");
		Node puebla = new Node("Puebla");
		Node tlaxcala = new Node("Tlaxcala");
		Node cancun = new Node("Cancún");

		df.addEdge(new Edge(df, toluca, 100));
		df.addEdge(new Edge(df, cuernavaca, 90));

		toluca.addEdge(new Edge(toluca, puebla, 350));
		toluca.addEdge(new Edge(toluca, cuernavaca, 150));
		toluca.addEdge(new Edge(toluca, tlaxcala, 340));
		toluca.addEdge(new Edge(toluca, df, 100));

		cuernavaca.addEdge(new Edge(cuernavaca, df, 90));
		cuernavaca.addEdge(new Edge(cuernavaca, puebla, 100));

		puebla.addEdge(new Edge(puebla, tlaxcala, 20));
		puebla.addEdge(new Edge(puebla, toluca, 350));

		instance.addNode(df);
		instance.addNode(toluca);
		instance.addNode(cuernavaca);
		instance.addNode(puebla);
		instance.addNode(cancun);
		instance.addNode(tlaxcala);
		return instance;
	}
}

La clase MapBuilder tiene un método que devuelve un objeto de tipo Graph que representa el grafo definido en la imagen anterior.

Implementando DFS sobre el grafo

El siguiente paso será implementar el algoritmo DFS para búsqueda de ciudades en el grafo que definimos, para esto crearemos la clase GraphExampleApplication.


import java.util.HashSet;
import java.util.List;
import java.util.Optional;

import com.raidentrance.graphs.Edge;
import com.raidentrance.graphs.Graph;
import com.raidentrance.graphs.Node;
import com.raidentrance.map.MapBuilder;

/**
 * @author raidentrance
 *
 */
public class GraphExampleApplication {
	private Graph graph;

	public GraphExampleApplication() {
		graph = MapBuilder.getGraph();
	}

	private Optional getNode(String city) {
		List nodes = graph.getNodes();
		for (Node node : nodes) {
			if (node.getCity().equals(city)) {
				return Optional.of(node);
			}
		}
		return Optional.empty();
	}

	public boolean hasPathDfs(String source, String destination) {
		Optional start = getNode(source);
		Optional end = getNode(destination);
		if (start.isPresent() && end.isPresent()) {
			return hasPathDfs(start.get(), end.get(), new HashSet());
		} else {
			return false;
		}
	}

	private boolean hasPathDfs(Node source, Node destination, HashSet visited) {
		if (visited.contains(source.getCity())) {
			return false;
		}
		visited.add(source.getCity());
		if (source == destination) {
			return true;
		}
		for (Edge edge : source.getAdjacents()) {
			if (hasPathDfs(edge.getDestination(), destination, visited)) {
				return true;
			}
		}
		return false;
	}
}

Analicemos cada uno de los componentes:

  • El atributo graph contiene el grafo a utilizar.
  • El constructor inicializa la variable graph con el grafo definido en la imágen anterior.
  • El método OptionalgetNode(String city): Recibe como parámetro una ciudad y devuelve el Nodo en el grafo en caso de existir que contenga el nombre de la ciudad.
  • El método boolean hasPathDfs(String source, String destination): Recibe dos ciudades, el origen y el destino, con esto ejecuta el método de búsqueda DFS para determinar si existe la ruta en el grafo.
  • El método boolean hasPathDfs(Node source, Node destination, HashSet visited): Recibe un nodo origen, un nodo destino y un set con nodos visitados y sigue la siguiente lógica:
    • Si el nodo origen ya fue visitado devuelve false.
    • Agrega el nodo origen a los nodos visitados
    • Toma todos los nodos adyacentes del nodo origen y se ejecuta de forma recursiva.

Caso de ejemplo

Analicemos el método boolean hasPathDfs(Node source, Node destination, HashSet visited) en el caso yendo de Puebla a Cuernavaca.

Ejecución 1: Source =Puebla, Destination=Cuernavaca, Visited={}

  • No se ha visitado la ciudad origen, en este caso Puebla, así que continua.
  • Se agrega Puebla a la lista de ciudades visitadas
  • En este caso el origen y el destino son diferentes así que continua
  • Se iteran los nodos adyacentes para obtener el siguiente nodo origen, en este caso son (Tlaxcala y Toluca) y se ejecuta el método hasPathDfs de forma recursiva tomando el primer nodo como nuevo source veamos la siguiente ejecución.

cities-graph

Ejecución 2: Source =Tlaxcala, Destination=Cuernavaca, Visited={Puebla}

  • No se ha visitado la ciudad origen, en este caso Tlaxcala, así que continua.
  • Se agrega Tlaxcala a la lista de ciudades visitadas
  • En este caso el origen y el destino son diferentes así que continua
  • Se iteran los nodos adyacentes para obtener el siguiente nodo origen, en este caso Tlaxcala no tiene ninguno así que se devuelve false y se continua con la siguiente iteración.

cities-graph

Ejecución 3: Source =Toluca, Destination=Cuernavaca, Visited={Puebla,Tlaxcala}

  • No se ha visitado la ciudad origen, en este caso Toluca, así que continua.
  • Se agrega Toluca a la lista de ciudades visitadas
  • En este caso el origen y el destino son diferentes así que continua
  • Se iteran los nodos adyacentes para obtener el siguiente nodo origen, en este caso son (Puebla, Cuernavaca,Tlaxcala y DF) y se ejecuta el método hasPathDfs de forma recursiva tomando el primer nodo como nuevo source veamos la siguiente ejecución.

cities-graph

Ejecución 3: Source =Puebla, Destination=Cuernavaca, Visited={Puebla,Tlaxcala,Toluca}

  • La ciudad origen, en este caso Puebla, ya ha sido visitada, en este caso devuelve false y continua con la siguiente ciudad adyacente(Puebla, Cuernavaca,Tlaxcala y DF)

cities-graph

Ejecución 4: Source =Cuernavaca, Destination=Cuernavaca, Visited={Puebla,Tlaxcala,Toluca}

  • No se ha visitado la ciudad origen, en este caso Cuernavaca, así que continua.
  • Se agregaCuernavaca a la lista de ciudades visitadas
  • En este caso el origen y el destino son iguales así que devuelve verdadero, esto significa que si existe una ruta válida para ir desde Puebla hacia Toluca.

Probando los distintos escenarios

Para terminar agregaremos un main a nuestra aplicación, para probar todos los posibles escenarios.

	public static void main(String[] args) {

		System.out.println("\n\t Paths from DF \n");
		System.out.println(String.format("From DF to DF %s", new GraphExampleApplication().hasPathDfs("DF", "DF")));
		System.out.println(
				String.format("From DF to Toluca %s", new GraphExampleApplication().hasPathDfs("DF", "Toluca")));
		System.out.println(String.format("From DF to Cuernavaca %s",
				new GraphExampleApplication().hasPathDfs("DF", "Cuernavaca")));
		System.out.println(
				String.format("From DF to Puebla %s", new GraphExampleApplication().hasPathDfs("DF", "Puebla")));
		System.out.println(
				String.format("From DF to Tlaxcala %s", new GraphExampleApplication().hasPathDfs("DF", "Tlaxcala")));
		System.out.println(
				String.format("From DF to Cancún %s", new GraphExampleApplication().hasPathDfs("DF", "Cancún")));
		// Paths from Toluca

		System.out.println("\n\t Paths from Toluca \n");
		System.out.println(String.format("From Toluca to Toluca %s",
				new GraphExampleApplication().hasPathDfs("Toluca", "Toluca")));
		System.out.println(
				String.format("From Toluca to DF %s", new GraphExampleApplication().hasPathDfs("Toluca", "DF")));
		System.out.println(String.format("From Toluca to Cuernavaca %s",
				new GraphExampleApplication().hasPathDfs("Toluca", "Cuernavaca")));
		System.out.println(String.format("From Toluca to Puebla %s",
				new GraphExampleApplication().hasPathDfs("Toluca", "Puebla")));
		System.out.println(String.format("From Toluca to Tlaxcala %s",
				new GraphExampleApplication().hasPathDfs("Toluca", "Tlaxcala")));
		System.out.println(String.format("From Toluca to Cancún %s",
				new GraphExampleApplication().hasPathDfs("Toluca", "Cancún")));

		System.out.println("\n\t Paths from Cuernavaca \n");
		System.out.println(String.format("From Cuernavaca to Cuernavaca %s",
				new GraphExampleApplication().hasPathDfs("Cuernavaca", "Cuernavaca")));
		System.out.println(String.format("From Cuernavaca to DF %s",
				new GraphExampleApplication().hasPathDfs("Cuernavaca", "DF")));
		System.out.println(String.format("From Cuernavaca to Toluca %s",
				new GraphExampleApplication().hasPathDfs("Cuernavaca", "Toluca")));
		System.out.println(String.format("From Cuernavaca to Puebla %s",
				new GraphExampleApplication().hasPathDfs("Cuernavaca", "Puebla")));
		System.out.println(String.format("From Cuernavaca to Tlaxcala %s",
				new GraphExampleApplication().hasPathDfs("Cuernavaca", "Tlaxcala")));
		System.out.println(String.format("From Cuernavaca to Cancún %s",
				new GraphExampleApplication().hasPathDfs("Cuernavaca", "Cancún")));

		System.out.println("\n\t Paths from Puebla \n");
		System.out.println(String.format("From Puebla to Puebla %s",
				new GraphExampleApplication().hasPathDfs("Puebla", "Puebla")));
		System.out.println(String.format("From Puebla to Cuernavaca %s",
				new GraphExampleApplication().hasPathDfs("Puebla", "Cuernavaca")));
		System.out.println(
				String.format("From Puebla to DF %s", new GraphExampleApplication().hasPathDfs("Puebla", "DF")));
		System.out.println(String.format("From Puebla to Toluca %s",
				new GraphExampleApplication().hasPathDfs("Puebla", "Toluca")));
		System.out.println(String.format("From Puebla to Tlaxcala %s",
				new GraphExampleApplication().hasPathDfs("Puebla", "Tlaxcala")));
		System.out.println(String.format("From Puebla to Cancún %s",
				new GraphExampleApplication().hasPathDfs("Puebla", "Cancún")));

		System.out.println("\n\t Paths from Tlaxcala \n");
		System.out.println(String.format("From Tlaxcala to Tlaxcala %s",
				new GraphExampleApplication().hasPathDfs("Tlaxcala", "Tlaxcala")));
		System.out.println(String.format("From Tlaxcala to Puebla %s",
				new GraphExampleApplication().hasPathDfs("Tlaxcala", "Puebla")));
		System.out.println(String.format("From Tlaxcala to Cuernavaca %s",
				new GraphExampleApplication().hasPathDfs("Tlaxcala", "Cuernavaca")));
		System.out.println(
				String.format("From Tlaxcala to DF %s", new GraphExampleApplication().hasPathDfs("Tlaxcala", "DF")));
		System.out.println(String.format("From Tlaxcala to Toluca %s",
				new GraphExampleApplication().hasPathDfs("Tlaxcala", "Toluca")));
		System.out.println(String.format("From Tlaxcala to Cancún %s",
				new GraphExampleApplication().hasPathDfs("Tlaxcala", "Cancún")));

		System.out.println("\n\t Paths from Cancún \n");
		System.out.println(String.format("From Cancún to Cancún %s",
				new GraphExampleApplication().hasPathDfs("Cancún", "Cancún")));
		System.out.println(String.format("From Cancún to Tlaxcala %s",
				new GraphExampleApplication().hasPathDfs("Cancún", "Tlaxcala")));
		System.out.println(String.format("From Cancún to Puebla %s",
				new GraphExampleApplication().hasPathDfs("Cancún", "Puebla")));
		System.out.println(String.format("From Cancún to Cuernavaca %s",
				new GraphExampleApplication().hasPathDfs("Cancún", "Cuernavaca")));
		System.out.println(
				String.format("From Cancún to DF %s", new GraphExampleApplication().hasPathDfs("Cancún", "DF")));
		System.out.println(String.format("From Cancún to Toluca %s",
				new GraphExampleApplication().hasPathDfs("Cancún", "Toluca")));

	}

Salida:


	 Paths from DF 

From DF to DF true
From DF to Toluca true
From DF to Cuernavaca true
From DF to Puebla true
From DF to Tlaxcala true
From DF to Cancún false

	 Paths from Toluca 

From Toluca to Toluca true
From Toluca to DF true
From Toluca to Cuernavaca true
From Toluca to Puebla true
From Toluca to Tlaxcala true
From Toluca to Cancún false

	 Paths from Cuernavaca 

From Cuernavaca to Cuernavaca true
From Cuernavaca to DF true
From Cuernavaca to Toluca true
From Cuernavaca to Puebla true
From Cuernavaca to Tlaxcala true
From Cuernavaca to Cancún false

	 Paths from Puebla 

From Puebla to Puebla true
From Puebla to Cuernavaca true
From Puebla to DF true
From Puebla to Toluca true
From Puebla to Tlaxcala true
From Puebla to Cancún false

	 Paths from Tlaxcala 

From Tlaxcala to Tlaxcala true
From Tlaxcala to Puebla false
From Tlaxcala to Cuernavaca false
From Tlaxcala to DF false
From Tlaxcala to Toluca false
From Tlaxcala to Cancún false

	 Paths from Cancún 

From Cancún to Cancún true
From Cancún to Tlaxcala false
From Cancún to Puebla false
From Cancún to Cuernavaca false
From Cancún to DF false
From Cancún to Toluca false

De este modo se pueden realizar búsquedas sobre un grafo utilizando el algoritmo DFS.

Si te gusta el contenido y quieres enterarte cuando realicemos un post nuevo síguenos en nuestras redes sociales https://twitter.com/geeks_mx y https://www.facebook.com/geeksJavaMexico/.

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contacto:raidentrance@gmail.com

Java design patterns, first part. Creational design patterns (Factory method, Singleton and Builder pattern)


Design patterns were developed as solutions for common problems that every developer faces. Design patterns are classified based on their main purpose; a design pattern may be any of the following types:

  •  Creational patterns
  •  Structural patterns
  •  Behavioral patterns

In this post, we are primarily focused on creational patterns and their implementation in Java.

Factory method

Factory method is a design pattern that centralizes the creation of objects that follow a hierarchical structure; this pattern is composed by the following elements:

  •  A factory class: In this case, as an example, we create the class SpritesFactory.
  • The product to be built: For this example, the Java objects built by the application.

Our example will be build using following hierarchical structure:
factory

Modelling our application

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

import java.awt.Graphics;

/**
 * @author raidentrance
 *
 */
public abstract 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;
	}
}

The Sprites class is an abstract class where we define common methods for all of the Sprites, such as: getX(), setX(int x), getY(), setY(int y), getWidth(),setWidth(int width), getHeigth() and  setHeigth(); as well as an abstract method called draw(Graphics g) that is different for each one of the implementations.

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 and BadBoy.java are Sprite.java subclasses. We use the Factory method to instantiate these subclasses.

Creating the object factory class

The next step is to create the class responsible of following the Factory method design pattern. This class creates the instances within the application.

/**
*
*/
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();
	}
}

There are two key points to highlight in the SpriteFactory class. Firstly, it contains an enumeration that defines all of the possible types of objects than can be built. Secondly, the method createStripes(StripeType type) receives an SpriteType as argument and depending on this argument it returns the corresponding instance.

Putting all together

Finally, we create a class to gather the classes previously described and observe how the objects are instantiated.

/**
*
*/
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);
	}
}

Output

Drawing a hero
Drawing a bad boy
Singleton pattern

Singleton pattern is used to define a class that contains a single instance and provides a single point of access to it.

In the following example, we create a Singleton class to define the set up parameters of an application.

Defining a class for error handling

/**
*
*/
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);
	}
}

Defining the Singleton class

/**
*
*/

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)));
	}
}

Points to highlight:

  1. Private constructor: It is fundamental to create a private constructor, by this means, the Singleton class is the only one capable to instantiate itself.
  2. Private static instance: It is necessary to define the instance as private and static, in order to make this variable accessible only inside the Singleton class.
  3. getInstance() method: This is the only point of access to the instance.

Putting all together.

/**
 *
 */
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());
	}
}

Output

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

As shown in this example, it doesn’t really matter how many times you invoke the getInstance() method, this will always return the same instance.

Builder Pattern

This design pattern is used to build complex object step by step.

Modelling our application

/**
 *
 */
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 class to calculate the age

The following class calculates a person’s age based on her birthday.

/**
 *
 */
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;
    }
}

Creating an abstract builder

An abstract builder class allows you to follow the same structure in all of the builders that you might want to develop. In this example, we create two builders, the first one to create Person objects and the second one to create Book objects, both of them extend 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();
}

Points to highlight on the AbstractBuilder class:

  1. This class has a generic instance that will contain the object that we want to build.
  2. Its constructor receives this instance as argument.
  3. It contains three important methods:
    • inject(): This method is used to generate values based on the attributes that it has.
    • validate(): This method validates that all of the prerequisites to correctly build an object are accomplished.
    • build(): This method calls the inject() and validate() method, and returns the instance.

Creating 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()));
    }
}

Points to highlight on the PersonBuilder class:

  1. The PersonBuilder constructor sends a new Person instance as argument.
  2. Each setter method returns a PersonBuilder instance.
  3. The inject() method is used to generate the age based on a birthday.
  4. The validate() method validates that the variables are not null.
  5. The build() method is already defined on the AbstractBuilder class and it returns a complete and correctly formed instance.

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() {

    }
}

You can find the code previously explained in the following link:  https://github.com/raidentrance/design-patterns

Author: Alejandro Agapito Bautista

Twitter: @raidentrance

e-mail:raidentrance@gmail.com

Translated By: Oscar Camacho – melchior01

Contact: adamfirstangel@hotmail.com

Errores básicos que cometen los desarrolladores


En el post anterior se habló sobre buenas prácticas de desarrollo utilizando REST, para verlo hacer click aquí. En este post se explicará de forma general algunos errores comunes tanto para desarrollo en general como para desarrolladores en Java específicamente.

Malos comentarios en el código

Existen 2 tipos de desarrolladores que realizan mal este punto, los primeros son los que no documentan absolutamente nada y por esto no se sabe a ciencia cierta lo que hace su código hasta leerlo completo y los segundos son los que quieren documentar su código pero no saben como hacerlo y hacen lo peor que se puede hacer (Poner un comentario en cada línea de código) Veamos un ejemplo de este tipo de código:

// Método contains que recibe 2 parámetros un arreglo y un entero
public static boolean contains(int[] a, int b) {
	// Itera el arreglo a
	// Inicio del for
	for (int i : a) {
		// Valida si la variable i es igual a el parámetro b
		// Inicio del if
		if (i == b) {
			// Regresa verdadero
			return true;
		}
		// Fin del if
		// Fin del for
	}
	// Devuelve falso
	return false;
}

Los comentarios que describen línea a línea el código no son una buena documentación por lo siguiente:

  • Se debe recordar que las personas que leerán el código son desarrolladores y también saben que return false; Significa que el método devuelve el valor falso.
  • Al hacer este tipo de comentarios se debe leer el método completo para saber lo que hace y es lo mismo leer los comentarios que leer el código(Incluso pienso que sería más útil leer sólo el código).
  • El código se vuelve más difícil a la vista para leer.

Entonces, ¿Cómo debo comentar mi código? Veamos a continuación el mismo código pero comentado de la forma correcta:

/**
 * Busca en el arreglo especificado el valor recibido iterandolo.
 * @param a Arreglo en el cuál se realizará la búsqueda
 * @param b Valor que se busca en el arreglo
 * @return <code>true</code> Si el valor <code>b</code> existe en el arreglo <code>a</code>.
 * <code>false</code> Si el valor <code>b</code> no existe en el arreglo <code>a</code>
 */
public static boolean contains(int[] a, int b) {
	for (int i : a) {
		if (i == b) {
			return true;
		}
	}
	return false;
}

Con esto se tiene una descripción clara de lo que hace el método, lo que recibe como parámetro y lo que devuelve. A demás, si se utiliza un IDE de desarrollo se agregará esta documentación al momento de auto completar el código como se muestra en la siguiente imagen:
Captura de pantalla 2016-09-23 a las 9.15.27 a.m..pngOtras malas prácticas al momento de comentar código:

  • Dejar código comentado “por si se llega a utilizar en el futuro”: Existen controladores de versiones donde si es necesario ver una versión anterior se puede hacer, no es necesario mantener una pieza de código por la vida completa del proyecto.
  • Utilizar comentarios como un control de versiones

No utilizar verificadores de estilo de código

Existen varios tipos de indentaciones correctas y no hay problema con usar una u otra, lo que recomiendo en este punto es utilizar un plugin que verifique el estilo de código, ya que si un desarrollador acostumbra indentar su código de algún modo y otro desarrollador acostumbra hacerlo de otro, los dos pueden están en lo correcto pero lo ideal para mantener el orden en el proyecto es que el proyecto siga un solo estilo.

En Java existen plugins en Maven que ayudan a forzar a seguir un solo estilo de código en el proyecto, el más común es Apache Maven Checkstyle Plugin.

Comentar test unitarios para compilar el código

Crea test unitarios para los componentes desarrollados y NO los comentes, los test unitarios deben ejecutarse en cada compilación de forma exitosa, si comentas los tests unitarios para compilar de forma correcta significa que algo estas haciendo mal. Sigue los siguientes puntos para hacer test unitarios de forma correcta:

  • No utilices el constructor para inicializar la prueba, utiliza las anotaciones de JUnit para esto.
  • Nunca asumas el orden de ejecución de los tests unitarios, tu prueba debe ser independiente a las demás.
  • Crea tests unitarios repetibles sin intervención humana, si tienes que borrar un registro en una base de datos para ejecutar el caso de prueba, lo estas haciendo mal.
  • Nunca utilices codigo duro(hard code) en Maven existe una ruta llamada /src/test/resources la cual es el lugar ideal para colocar las configuraciones de tus pruebas.

Mal uso del controlador de versiones

Un desarrollador nuevo se nota en primera vista cuando vez su repositorio de código y ves alguno de los siguientes puntos:

  • Archivos compilados en el controlador de versiones: subir a git(O cualquier otro controlador) los archivos compilados de sus proyectos, el controlador de versiones es para el código, no para los archivos ya compilados, haz buen uso del archivo .gitignore.
  • Malos comentarios en commits : Colocar comentarios como “bug fixed” en commits que contienen cambios grandes, busca siempre dar una descripción clara sobre el cambio hecho en el repositorio.
  • Trabajar sobre master: Trabaja siempre sobre ramas y manten en master la versión estable del código.
  • Subir al repositorio trabajo incompleto: El repositorio debe contener piezas de código completas, si deseas subir poco a poco un requerimiento al repositorio, divídelo en módulos pequeños y sube el código al repositorio cada que un módulo esté completo.
  • El código siempre debe compilar: Muchas veces por error se sube código al repositorio que no compila y rompe los builds, lo cuál provoca que cuando otros desarrolladores descarguen el código no se pueda construir. Utiliza herramientas de Continuous integration para evitar este problema(Más adelante se hablará con más detalle al respecto en otros posts).

No utilizar patrones de diseño y estándares de desarrollo

Es muy común la idea de los desarrolladores “Lo importante es que funcione” y esta frase es una de las principales fuentes de bugs y proyectos  inmantenibles. El uso de patrones de diseño no solo facilita el desarrollo de las aplicaciones sino que facilita la integración de un miembro nuevo al equipo, la escalabilidad del proyecto y sobre todo la seguridad de que lo que estas desarrollando lo estas desarrollando de forma correcta.

A continuación se presenta una lista de patrones de diseño comunes disponibles en el desarrollo de aplicaciones:

  • Creational Design Patterns:
    • Factory Pattern
    • Abstract Factory Pattern
    • Singleton Pattern
    • Prototype Pattern
    • Builder Pattern.
    • etc
  • Structural Design Pattern:
    • Adapter Pattern Bridge
    • Pattern Composite
    • Pattern Decorator
    • Pattern Facade
    • Pattern Flyweight
    • Pattern Proxy Pattern
    • etc
  • Behavioral Design Pattern:
    • Chain Of Responsibility Pattern
    • Command Pattern
    • Interpreter Pattern
    • Iterator Pattern
    • Mediator Pattern
    • Memento Pattern Observer
    • Pattern State
    • Pattern Strategy
    • Pattern Template
    • Pattern Visitor Pattern
    • etc

Pobre manejo de errores

 Un error común y doloroso de muchos desarrolladores nuevos es que su manejo de excepciones es el siguiente:

catch( Exception e ) {}

Lo cual es una de las peores prácticas que puede cometer un desarrollador en cualquier lenguaje de programación ya que provoca problemas en la aplicación y dificulta la detección rápida de errores en la aplicación.

Hardcode

Una de las peores prácticas de programación que se pueden encontrar y la más inaceptable de todas, asignar valores a variables o configuraciones en lugar de tomarlos de una fuente externa para que el código funcione “mientras termino” y dejarlos hasta producción.

NOTA: Estas son solo algunas malas prácticas de desarrollo que debes evitar, sabemos que existen mil más, si tienes alguna que te gustaría compartir escríbela en la sección de comentarios.

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contacto:raidentrance@gmail.com