Patrón Intérprete

0
7282

Patrón Intérprete

Lo primero, os dejo el enlace a las: fuentes de este tutorial

Introducción

En un proyecto en el que estoy participando en este momento (aunque no estoy de técnico, sino de coacher de scrum) se planteó un problema técnico.
Se necesitaba crear una forma rápida de definir y ejecutar una especie de fórmula fácilmente modificable. Yo en seguida me acordé del patrón intérprete
y de la notación polaca (en lugar de (A + B) * C , pues A B + C *). Este simple cambio de notación en la expresión permite que sea mucho más sencilla la
programación de la ejecución de la operación. Si queréis saber más acerca de todo esto, os dejo un enlace (tirando de éste podéis conocer más acerca del patrón intérprete,
de la notación polaca, de cómo pasar de notación infija (A + B) a notación polaca (sufija o prefija).

Definiendo el lenguaje

Lo primero que haremos será definir el lenguaje de nuestro problema. El ejemplo típico y que mejor se entiende es en el ámbito de operaciones matemáticas:

Símbolo Operación Clase Java
+ Suma SumFunction
Resta SubtractFunction
* Multiplicación MultFunction
/ División DivFunction
Cualquier Representación Númérica Valor numérico NumberFunction
@ Logaritmo base e LogeFunction
Cualquier letra [A-Z] [a-z] Variable VariableFunction

Yo creo que con esto es suficiente.

Definiendo el intérprete

Necesitaremos también una clase que interprete la expresión y construya la secuencia de funciones según el lenguaje definido.
A esta clase la llamaremos (en un alarde de originalidad) InterpreterFunction.

Así mismo, para la correcta implementación del patrón, definiremos un interfaz que han de cumplir todas las clases y le denominaremos ExpressionFunction. Además los clientes
que utilicen nuestro sistema pueden necesitar pasar información de contexto para la ejecución de la función (por ejemplo las variables a sustituir).
Esta información estará dentro la clase ExpressionFunctionContext (por ahora la dejamos en blanco):

package com.autentia.tutoriales.interprete;

import java.math.BigDecimal;

public interface ExpressionFunction {

	public BigDecimal evaluate(ExpressionFunctionContext context);
	
	
}

Empezando con las funciones:

En el código están todas y con sus tests unitarios, os pongo algunas únicamente:

class NumberFunction implements ExpressionFunction {
	
	private BigDecimal number;
	
	
	public NumberFunction(BigDecimal number) {
		this.number = number;
	}
	
	
	@Override
	public BigDecimal evaluate(ExpressionFunctionContext context) {
		return this.number;
	}

}
class DivFunction implements ExpressionFunction {

	
	private ExpressionFunction dividendo;
	private ExpressionFunction divisor;

	public DivFunction(ExpressionFunction dividendo, ExpressionFunction divisor) {
		this.dividendo = dividendo;
		this.divisor = divisor;
	}


	@Override
	public BigDecimal evaluate(ExpressionFunctionContext context) {
		return dividendo.evaluate(context).divide(divisor.evaluate(context),RoundingMode.HALF_EVEN);
	}

}

class LogeFunction implements ExpressionFunction {

	private ExpressionFunction valor;
	
	public LogeFunction(ExpressionFunction valor) {
		this.valor = valor;
	}


	@Override
	public BigDecimal evaluate(ExpressionFunctionContext context) {
		return new BigDecimal(Math.log(valor.evaluate(context).floatValue()));
	}

}

public class VariableFunction implements ExpressionFunction {
	
	
	private String variableName;
	
	public VariableFunction(String variableName) {
		this.variableName = variableName;
	}
	
	
	@Override
	public BigDecimal evaluate(ExpressionFunctionContext context) {		    		
		return new NumberFunction(getValorFromContext(context)).evaluate(context);	    
	}


	private BigDecimal getValorFromContext(ExpressionFunctionContext context) {
		return context.getNumberFromContextByVariableName(this.variableName);
	}

}

Para implementar esta clase he necesitado completar la clase ExpressionFunctionContext:

public class ExpressionFunctionContext {

	private Map mapaVariablesNumeros = null;
	
	private ExpressionFunctionContext() {
		mapaVariablesNumeros = new HashMap(3);
	}
	
	public static ExpressionFunctionContext createExpressionContext(){
		return new ExpressionFunctionContext();
	}
	
	public ExpressionFunctionContext addVariable(String variableName, BigDecimal valor) {
		mapaVariablesNumeros.put(variableName, valor);
		return this;
	}
	
	
	BigDecimal getNumberFromContextByVariableName(String variableName) {
		if(mapaVariablesNumeros==null) {
			throw new ExpressionFunctionException("el contexto es nulo");
		}
		
		BigDecimal valor = mapaVariablesNumeros.get(variableName);
		
		if(valor==null){
			throw new ExpressionFunctionException("No se ha encontrado ninguna variable en el contexto con el nombre:"+variableName);
		}
		
		return valor;
	}

}

Para que entendáis cómo se usan, qué mejor que poner los tests de cada una de ellas:

public class NumberFunctionTest {

	@Test
	public void probando_number_function_test() {
		NumberFunction numberFunction = new NumberFunction(new BigDecimal(8.7));
		Assert.assertEquals(new BigDecimal(8.7), numberFunction.evaluate(null));
	}


}


public class DivFunctionTest {

	@Test
	public void probando_div_function_con_numeros_test() {
		
		DivFunction division = new DivFunction(new NumberFunction(new BigDecimal(6.4f)), new NumberFunction(new BigDecimal(3.0f)));
		
		BigDecimal expected = new BigDecimal(6.4f).divide(new BigDecimal(3.0f),RoundingMode.HALF_EVEN);
		Assert.assertEquals(expected, division.evaluate(null));
		
	}
	
	
	
	@Test
	public void probando_div_function_con_numeros_y_variables_test() {
		
		DivFunction division = new DivFunction(new NumberFunction(new BigDecimal(8.7f)), new VariableFunction("X"));
		
		BigDecimal expected = new BigDecimal(8.7f).divide(new BigDecimal(6.3f),RoundingMode.HALF_EVEN);
		Assert.assertEquals(expected, division.evaluate(ExpressionFunctionContext.createExpressionContext().addVariable("X", new BigDecimal(6.3f))));
		
	}
	


}

public class VariableFunctionTest {

	@Test
	public void probando_variable_function_con_variable_en_el_contexto_test() {
		
		ExpressionFunctionContext context = ExpressionFunctionContext.createExpressionContext().addVariable("A", new BigDecimal(7.90));
		
		VariableFunction function = new VariableFunction("A");
		
		Assert.assertEquals(new BigDecimal(7.90), function.evaluate(context));
		
	}

	@Test(expected=ExpressionFunctionException.class)
	public void probando_variable_function_sin_variable_en_el_contexto_test() {
		
		ExpressionFunctionContext context = ExpressionFunctionContext.createExpressionContext();
		
		VariableFunction function = new VariableFunction("A");
		
		Assert.assertEquals(new BigDecimal(7.90), function.evaluate(context));
		
	}
	
	

}

Ahora vamos a la clase que hace que esto se empiece a entender. Para poder hacer uso de la notación polaca usaremos una pila (Stack) parecido a como lo hacen las calculadoras HP (por lo menos en mis tiempos de universidad) .
Defino la clase como un servicio. De esta manera la pila de ejecuciones ya estará creada. Además, usaremos un fichero de propiedades para definir la expresión que ejecuta. Os la enseño:


@Service
public class InterpreterFunction implements ExpressionFunction {

	private ExpressionFunction expressionsTree;
	
	private Stack expressionsStack;
	
	private String actualExpression;
	
	public String getActualExpression() {
		return actualExpression;
	}

	@Autowired
	public InterpreterFunction(@Value("#{props['interpreter.expression']}")String expression) {
		this.actualExpression = expression;
		expressionsStack = new Stack();
		
		for (String token : expression.split(" ")) {
			ExpressionFunction subFunction  = createTheRightFunction(token);					
			expressionsStack.push(subFunction);
		}
		expressionsTree = expressionsStack.pop();
	}
	
	//TODO considerar extraer esto a una Factoría de familias de funciones. Para una sesión de refactorización
	private ExpressionFunction createTheRightFunction(String token) {	
		
		ExpressionFunction funcion = null;
		
		if (NumberUtils.isNumber(token)) {
			funcion = new NumberFunction(new BigDecimal(token));
		} else if(StringUtils.isAlpha(token)) {
			funcion = new VariableFunction(token);
		} else if ("+".equals(token)) {
			funcion = new SumFunction(expressionsStack.pop(),expressionsStack.pop());
		} else if ("-".equals(token)) {
			ExpressionFunction right = expressionsStack.pop();
			ExpressionFunction left = expressionsStack.pop();
			funcion = new SubtractFunction(left, right);
		} else if ("*".equals(token)) {
			funcion = new MultFunction(expressionsStack.pop(), expressionsStack.pop());
		} else if ("/".equals(token)) {
			ExpressionFunction right = expressionsStack.pop();
			ExpressionFunction left = expressionsStack.pop();
			funcion =  new DivFunction(left, right);
		} else if ("@".equals(token)) {
			funcion = new LogeFunction(expressionsStack.pop());
		} else {
			throw new ExpressionFunctionException("regla no reconocida: " + token);
		}
		
		
		return funcion;
	}

	public BigDecimal evaluate(ExpressionFunctionContext context) {
		return expressionsTree.evaluate(context);
	}

}

Y ahora una par de clases de tests.
Una para probar que funciona correctamente sin contar con Spring. Usamos un Runner de JUnit llamado Parameterized que permite pasar parámetros al test. Así conseguimos
lanzar 6 tests en uno con diferentes parámetros.

@RunWith(value = Parameterized.class)
public class InterpreterFunctionTest {
	
	
	private String expresion;
	private BigDecimal resultado;
	
	static ExpressionFunctionContext contexto = null;
	
	@Parameters
	public static Collection getTestParameters() {
		contexto = ExpressionFunctionContext.createExpressionContext().addVariable("X", new BigDecimal(8)).addVariable("Y", new BigDecimal(7));
		
		return Arrays.asList(new Object	[][] {

				{ "7 8 + 50 -",new BigDecimal("-35")},
				{ "0 0 +", new BigDecimal("0")},
				{ "6.54 99.76 + 45 50 - - X +",new BigDecimal("119.30")},
				{ "6.54 99.76 * 45 50 - - X +",new BigDecimal("665.43")},
				{ "6.54 99.76 * 45 50 / Y * - @",new BigDecimal("6.46")},
				{ "X Y * 7 +",new BigDecimal("63")}
				});
		
		
	}
	public InterpreterFunctionTest(String regla, BigDecimal resultado) {
		this.expresion = regla;
		this.resultado = resultado;
		
	}
	
	@Test
	public void testingSomeOperationsExpression() {	
		InterpreterFunction evaluator = new InterpreterFunction(expresion);
		assertEquals(resultado.doubleValue(), evaluator.evaluate(contexto).doubleValue(),0.1);
	}	  
	  
}

Ahora otro test para comprobar que Spring me lo crea correctamente y me inyecta la expresión:

@ContextConfiguration(locations={"classpath:applicationContext-test.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class InterpreterFunctionSpringTest {
	
	
	@Autowired
	InterpreterFunction interpreter;
	
	@Value("#{props['interpreter.expression']}")
	String expression1;
	
	@Value("#{props['interpreter.result']}")
	String result;
	
	
	@Test
	public void inyeccion_expression_correcta_Test() {
		Assert.assertEquals(expression1,interpreter.getActualExpression());
	}
	
	@Test
	public void evaluacion_correcta_suma_simple_Test() {
		BigDecimal expected = new BigDecimal(result);
		
		Assert.assertEquals(expected,interpreter.evaluate(null));
	}	
	
}

El fichero del contexto de Spring:



	
	
	
	

El fichero de propiedades es muy simple:

interpreter.expression = 6.54 99.76 + 45 50 - - 8 +
interpreter.result = 119.30

Bueno, pues nos hemos hecho un mini motor de reglas con 11 ó 12 clases (aunque en este caso son operaciones matemáticas), ahora sólo queda buscarle sentido en tu negocio.

Reconocimientos

Gracias a Iván Zaera que hace ya muchos años me dejó patidifuso cuando me contó lo de la Notación Polaca (los de Teleco no solemos saber de estas cosas
y las aprendemos gracias a los informáticos de verdad).

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad