ABAP Objects: Tutorial para implementar el patrón de diseño “decorator”
En este tutorial de ABAP Objects conoceremos conceptualmente los patrones de diseño y aprenderemos cómo implementar, paso a paso, el patrón de diseño "decorator", y probaremos su uso mediante un programa ejemplo en ABAP.
Los patrones de diseño son modelos ("frameworks") de diseño estandarizados, utilizados en la programación orientada a objetos, que buscan ofrecer una solución flexible, prototipada y reusable a ciertos problemas de diseño que ocurren en forma recurrente en un determinado contexto. Cuando se utilizan de manera correcta, los patrones de diseño ayudan a lograr un software reutilizable y mantenible, aumentando la extensibilidad y la portabilidad del sistema.
Los patrones de diseño tienen que ver fundamentalmente con el diseño y la interacción de los objetos, y son utilizados a través de todo el espectro de entornos OO. El presente artículo analizará la implementación en el mundo de ABAP OO, paso a paso, de un patrón de diseño conocido como "Decorator", a través de un ejemplo. Una vez explicada en forma conceptual la solución y su implementación en ABAP se lo testeará en un programa de prueba.
La utilización de patrones de diseño exige conocer las características particulares del entorno OO que se está utilizando; en otras palabras, es necesario conocer a qué paradigma dentro del mundo de objetos pertenece ABAP OO, y algunas consideraciones teóricas del patrón de diseño "decorator" en particular. Como se verá en la próxima sección, no todos los lenguajes OO organizan el conocimiento de la misma manera, y hay cuestiones de jerarquía de clases, herencia, y conducta del compilador que afectan el fucnionamiento de los patrones.
En virtud de ello, antes de desarrollar el ejemplo haremos una breve clasificación de los distintos paradigmas en los lenguajes de programación orientados a objetos. No es el alcance de este tip explicar estos paradigmas en detalle, sino identificar en qué grupo se encuentra ABAP Objects. Si el lector se encuentra familiarizado con los distintos paradigmas de los lenguajes orientados a Objetos, puede omitir la lectura de las dos próximas secciones.
Overview de los diferentes paradigmas de los lenguajes orientados a objetos
Los lenguajes de programación orientados a objetos se pueden clasificar en dos grupos, dependiendo de qué manera organizan el conocimiento:
Por una parte están los lenguajes que organizan el conocimiento de manera jerárquica, mediante clases, y por otra, los que lo hacen a través de prototipos.
El primer caso, por ejemplo, es el de los lenguajes más populares como Java, VB.Net , Smalltalk, C++, Objective-C. Abap OO también pertenece a este grupo. El segundo grupo, que utiliza prototipos, está compuesto por lenguajes menos conocidos como "Self".
Dentro de los lenguajes que conforman el primer grupo se pueden encontrar dos grandes divisiones: lenguajes tipados y no-tipados:
- Los tipados son aquéllos en que el compilador verifica tipos. Cuando se define un atributo o parámetros de entrada o salida del método de una clase en Java por ejemplo, se debe especificar de qué tipo son dichos parámetros.
- En los no-tipados, no existen los tipos. El lenguaje no controla los tipos de variable que declara. Este es el caso de Smalltalk y Objective-C, por ejemplo.
Abap OO pertenece al grupo de los tipados, de hecho, quienes hayan programado en Abap OO y Java, puede apreciar que tienen muchas similitudes y ésto se debe a que ambos pertenecen al mismo paradigma.
Dada la naturaleza de cada grupo, la implementación de un patrón de diseño depende del paradigma al cual pertenezca el lenguaje . En el caso de los tipados, como el compilador verifica tipos, es necesario heredar de una superclase o implementar una interfaz para que dos objetos sean polimórficos. Este es el caso de Abap Objects. En contraste, los lenguajes no tipados, al no tener "tipos", dicha superclase o interfaz NO es necesaria para que dos objetos sean polimórficos.
Generalidades de los patrones de diseño
Como se dijera en la introducción, un patrón de diseño es un modelo de solución reusable y prototipada a un problema de diseño que se plantea una y otra vez en forma recurrente dentro del mundo de la programación orientada a objetos. La definición de un patrón de diseño incluye:
1) Nombre del patrón: Consiste en una o dos palabras que describen el problema de diseño.
2) El problema: Especifica cuándo aplicar el patrón y explica el contexto.
3) La solución: No describe un diseño concreto ni una implementación en particular, ya que el mismo se puede aplicar en diferentes situaciones. En cambio, el patrón provee una descripción abstracta de un problema de diseño y de qué manera un conjunto de elementos (clases y objetos) lo resuelven.
4) Las consecuencias: Son los resultados que se obtienen al aplicar un patrón. Incluyen su impacto en la flexibilidad, extensibilidad y la portabilidad del sistema.
Objetivo y necesidad del patrón de diseño “Decorator”
Supóngase que se desea agregar a la vista de un texto un botón, un “scroll”, un borde, etc. Imaginemos un contexto donde la vista no puede tener un borde por defecto porque tal vez no se lo necesite o se requiera otro tipo de borde.
Programar distintas vistas que tengan las distintas funciones (vista con scroll; vista con scroll y borde; vista con borde, etc) es una tarea ardua y una solución muy poco mantenible.. Esto ocurre, en primer lugar, porque se debería programar una clase diferente para cada tipo de combinación existente, lo cual es insostenible (con 5 elementos adicionales como scroll, borde, input, text box y table existen 31 combinaciones posibles! ). En segundo lugar, si surge una nueva funcionalidad (un button en la vista por ejemplo), es tanto el nuevo código que se debe programar que resulta inmantenible. Heredar el borde de una clase es otra solución incorrecta, ya que todas las subclases heredarían el borde. Esto es inflexible, porque la elección se llevaría a cabo nuevamente de forma estática.
Si bien en el el presente tutorial se va a explicar el patrón "decorator", existen muchos otros como por ejemplo Singleton, Composite, State, Strategy y Template Method. Cada uno propone una solución general para un problema particular recurrente en diseño.
Enunciado del ejercicio a desarrollar
Se requiere armar el pedido de un café para el bar “El almacén del buen Café”. En este bar un café puede ser de diferente tipo: “Cappuccino”, “Café Latte”, “Americano”, “Ristretto”, etc.
Al café en cuestión se le pueden agregar distintos ingredientes, como por ejemplo: “crema”,”chocolate”, “canela”, “leche”, etc. Es válido agregar el mismo ingrediente dos veces (sería algo como doble crema por ejemplo). Cada tipo de café tiene un precio y cada tipo de ingrediente tiene otro. Se requiere obtener el precio total y la descripción total del pedido.
Por ejemplo si el pedido está formado por el café Cappuccino ($11) y los ingredientes crema ($1) y canela ($0.5). La descripción y precio deberían ser: “Cappuccino, crema, canela” y $12.5, respectivamente. Si el mismo ingrediente se encuentra dos veces, no es necesaria la palabra “doble”. Si el pedido consiste en un Ristretto ($15) con doble crema ($1), alcanza con que la descripción sea: “Ristretto, crema, crema” y el precio: $17.
Análisis de cómo se va a utilizar “decorator”
Supongamos que "El almacén del buen Café" tiene un pedido Cappuccino con los ingredientes crema y canela. Podemos pensar que los ingredientes son decoradores del cappuccino. En primer lugar, el cliente desea un Cappuccino, entonces se crea un objeto cappuccino. Luego el cliente quiere crema, entonces se crea dicho objeto ingrediente (crema) y se hace que “decore” al cappuccino. En términos de programación se va a tener un objeto crema que tiene un objeto cappuccino (composición).
Finalmente el cliente quiere canela, entonces se crea el objeto canela y se lo compone con la crema.
El cómputo de la descripción total se obtendrá de manera recursiva como se puede ver en el siguiente diagrama de secuencia:
El cálculo del precio final del pedido se obtendrá de forma análoga. El método get_price() en lugar de retornar la concatenación de dos “strings”, va a devolver la suma de dos precios.
A continuación se ilustra cuáles son las relaciones de las clases que permiten la solución del enunciado:
Implementación de la solución en Abap OO
Planteado el enunciado del problema a resolver, en los próximos pasos se va a implementar la solución en ABAP.
Para ello se van a crear las tres clases, y se definirán los atributos y los métodos correspondientes. Finalmente, se creará un programa de prueba para testear la solución.
Nota: El presente tip supone que el lector ya se encuentra familiarizado en cómo crear clases, definir métodos y atributos. Si éste no es el caso, es recomendable la lectura del tip Tutorial ABAP Objects: Parte 2, en donde aprenderá esos conceptos necesarios para el seguimiento del presente Tutorial. |
En este Tutorial, se crearán las clases de manera global, mediante la transacción SE80 o SE24. De todas maneras, si el lector lo desea, puede optar por llevar a cabo la creación de las mismas de forma local en un programa.
Los pasos para la implementación de la solución son los siguientes:
1) Se crea la clase Z_CAFE en la transacción SE80 o SE24.
2) Una vez creada la clase Z_CAFE, en la solapa de “Interfaces” se declara la interfaz Z_BEBIDA y se hace doble-click para crearla:
3) Luego dentro de la interfaz Z_BEBIDA, se hace click en la solapa “Atributes” y se declaran los atributos description y price como variables de instancia con los tipos asociados que se muestran a continuación:
Para el tipo asociado a price se eligió un elemento de dato que tiene el siguiente formato:
Elegir algún elemento de dato que tenga características similares. Si tiene las tablas de vuelos (SPFLI, SFLIGHT, SBOOK) puede elegir el elemento de dato S_PRICE.
4) Se hace click en la solapa de “Methods” de la interfaz Z_BEBIDA y se declaran los métodos de instancia:
Se selecciona el método GET_DESCRIPTION y se oprime el botón Parameters. Luego, en la nueva pantalla se define el parámetro de retorno R_DESCRIPTION.
Análogamente se define el parámetro de retorno R_PRICE en el método GET_PRICE. Recuerde definir como tipo asociado el mismo tipo con que definió el atributo. En el caso de este tutorial será ZGF_COFFEE_PRICE.
5) Se activa la interfaz Z_BEBIDA.
6) Se regresa a la clase Z_CAFE. Como ésta implementa la interfaz Z_BEBIDA, debe implementar sus métodos. Se dice que la clase Z_CAFE usa los métodos y los atributos de la interfaz Z_BEBIDA.
7) Se prosigue a escribir la implementación de los métodos. Para ello se hace doble-click en Z_BEBIDA~GET_DESCRIPTION .
Implementación:
METHOD z_bebida~get_description.
r_description = z_bebida~description.
ENDMETHOD.
Luego se escribe la implementación del método GET_PRICE de manera análoga.
Implementación:
METHOD z_bebida~get_price.
r_price = z_bebida~price.
ENDMETHOD.
8) Se agrega el método CONSTRUCTOR en la solapa de “Methods” y se definen los siguientes parámetros:
La implementación del método CONSTRUCTOR es la siguiente:
METHOD constructor.
z_bebida~description = i_description.
z_bebida~price = i_price.
ENDMETHOD.
9) Se activa la clase Z_CAFE.
10) Se crea la clase Z_INGREDIENTE. En la solapa ”Interfaces” (al igual que como se hizo con la clase Z_CAFE), se declara la interfaz Z_BEBIDA.
11) Dentro de la clase Z_INGREDIENTE, en la solapa de “Methods” se agregan los métodos CONSTRUCTOR y SET_CAFE_COMPUESTO (los métodos GET_DESCRIPTION y GET_PRICE aparecen automáticamente porque la clase implementa la interfaz, al igual que ocurrió con la clase Z_CAFE). En la solapa de atributos se agrega la variable de instancia cafe_compuesto con el tipo asociado Z_BEBIDA.
La solapa de métodos quedará como se ilustra a continuación:
Y la solapa de los atributos, como sigue:
12) Se procede a definir los parámetros de los cuatro métodos e implementarlos.
12.1) Método CONSTRUCTOR: se realiza exactamente igual al método CONSTRUCTOR de la clase Z_CAFE, con los mismos parámetros y la misma implementación (Puede hacer "copy-paste" del código)
12.2) Método SET_CAFE_COMPUESTO:
Parámetros:
Implementación:
METHOD set_cafe_compuesto.
cafe_compuesto = i_cafe_compuesto.
ENDMETHOD.
12.3) Método GET_DESCRIPTION. En ese caso no hay que definir ningún parámetro ya que los mismos surgen de la interfaz Z_BEBIDA.
Implementación:
METHOD z_bebida~get_description.
DATA: desc TYPE string,
desc_final TYPE string.
desc = cafe_compuesto->get_description( ).
CONCATENATE desc z_bebida~description INTO desc_final
SEPARATED BY ', '.
r_description = desc_final.
ENDMETHOD.
12.4) Método GET_PRICE
Implementación:
METHOD z_bebida~get_price.
DATA: parcial_price TYPE zgf_coffee_price,
final_price TYPE zgf_coffee_price.
parcial_price = cafe_compuesto->get_price( ).
final_price = parcial_price + z_bebida~price.
r_price = final_price.
ENDMETHOD.
Nota: Recuerde incluir el elemento de dato correspondiente para las variables parcial_price y final_price.
13) Active la clase Z_INGREDIENTE como último paso del desarrollo.
14) El lector puede testear las clases como desee. A continuación se muestra el código de un programa Abap de prueba, en donde se va a realizar la composición de los objetos “a mano” para verificar el funcionamiento. Recuerde nuevamente utilizar el tipo de dato correspondiente para la variable price.
Código ABAP de prueba propuesto:
*&---------------------------------------------------------------------*
*& Report Z_PRUEBA_PEDIDO
*&
*&---------------------------------------------------------------------*
*&
*&
*&---------------------------------------------------------------------*
REPORT z_prueba_pedido.
DATA: cappuccino TYPE REF TO z_cafe,
crema TYPE REF TO z_ingrediente,
canela TYPE REF TO z_ingrediente,
description TYPE string,
price TYPE zgf_coffee_price.
"***Se crean los objetos
CREATE OBJECT cappuccino
EXPORTING
i_description = 'Cappuccino'
i_price = '11'.
CREATE OBJECT crema
EXPORTING
i_description = 'crema'
i_price = '1'.
CREATE OBJECT canela
EXPORTING
i_description = 'canela'
i_price = '0.5'.
"Se Realiza la composicion
crema->set_cafe_compuesto(
EXPORTING
i_cafe_compuesto = cappuccino ).
canela->set_cafe_compuesto(
EXPORTING
i_cafe_compuesto = crema ).
"***Se obtiene la descripcion y el precio
description = canela->z_bebida~get_description( ).
price = canela->z_bebida~get_price( ).
WRITE: 'Descripcion: ',description,
/,'Precio: ',price.
Salida resultante por pantalla:
Consideraciones adicionales:
1) Notar que en el programa de prueba, la composición se realizó “a mano” para testear las tres clases. Se recomienda crear una clase "armadora" llamada “Z_ARMADOR_CAFE” que encapsule la composición de los objetos. La forma de implementar la clase queda librada al lector.
2) Notar también que para poder implementar el patrón se requirió que los objetos de la clase CAFE e INGREDIENTE sean polimórficos. En los lenguajes tipados, se necesita heredar de una superclase o implementar una interfaz para que dos objetos sean polimórficos. Es por eso que se usó la interfaz BEBIDA. Se podría haber utilizado una clase abstracta, pero se optó por usar una interfaz ya que no se encuentra una superclase apropiada que tenga como subclases a CAFE e INGREDIENTE. Igualmente si el lector lo desea, puede probar la alternativa de usar una clase abstracta.
3) Observar que no se creó una clase para cada ingrediente y que las mismas heredan de una superclase INGREDIENTE. Se creó una única clase porque todos los ingredientes tienen la misma forma de calcular su costo y su descripción (mismo comportamiento). Suponga ahora que el precio de la leche está dado por 50ml y que el cliente puede decidir cuántos ml de leche quiere. En este caso el precio de la leche va a depender de un atributo adicional que es la cantidad solicitada. Por lo tanto en este caso tiene sentido crear una clase abstracta INGREDIENTE con subclases LECHE y CANELA por ejemplo.
4) Analizar cómo cambia la implementación del patrón en los lenguajes no-tipados como Smalltalk. En ese caso no se necesitaría de la interfaz o clase abstracta BEBIDA, ya que en dichos lenguajes no se requiere de herencia para que dos objetos sean polimórficos. Es por esta razón que se explicó a qué paradigma pertenece Abap OO.
5) Observar que para invocar los métodos get_price() y get_description() se utilizó la forma un_objeto->nombre_interfaz~un_metodo( ). De esta manera queda expuesto el uso de la interfaz, lo cual no es conveniente. Para lograr una mejor encapsulación se recomienda el uso de “alias”. Esto no es más que un alias para poder llamar al método con otro nombre. De esta manera, se puede invocar al método mediante un_objeto->un_metodo( ) sin exponer el uso de la interfaz.
Especialista ABAP |
Copyright 2012 - Teknoda S.A.
IMPORTANTE: “Notas técnicas de SAP ABAP" se envía con frecuencia variable y sin cargo como servicio a nuestros clientes SAP. Contiene notas/tutoriales/artículos técnicos desarrollados en forma totalmente objetiva e independiente. Teknoda es una organización de servicios de tecnología informática y NO comercializa hardware, software ni otros productos. |