Desarrollo de frameworks

De Wiki de EGC
Saltar a: navegación, buscar

Prerrequisitos

Para esta práctica solo será necesario que nos aseguremos de tener instalada una versión igual o mayor a Python 3.8.

Para verificar la versión de Python que tenemos instalada podemos ejecutar:

python -V

Preparación para práctica sobre creación de frameworks

En esta práctica recrearemos un desarrollo de un framework para el analisis de modelos de variabilidad.

Podréis encontrar el código en github aquí

Presentación en pdf aquí

Ejercicio

Iteración 1


  • Análisis: Identificamos los frozen points de nuestro framework
    • Clases del dominio: El dominio sobre el que desarrollaremos nuestro framework es el del analisis de modelos de características. Para ello, comenzaremos por conocer los elementos básicos de los mismos según lo visto en teoría.
    • Gestión de plugins: También analizaremos que partes del framework permitirán extensivilidad basandonos en plugins.
  • Diseño: En este apartado crearemos una clase Feature y una clase Relación para definir los conceptos con los que trabajará el framework

fichero: core/frozen_points_domain.py

  1 from typing import Sequence
  2 #Este modulo añade soporte a clases abstractas
  3 from abc import ABC, abstractmethod
  4 
  5 #Esto es la definición de la clase Relación
  6 class Relation:
  7 
  8     #Esto es un constructor en python
  9     #Cuando colocamos el nombre de la clase entre comillas es la forma de usar tipado fuerte en python. 
 10     #De cara a crear un framework de caja blanca, necesitamos orientación a objetos y tipado de objectos
 11     #Esto está disponible en python desde la versión 3.8 en adelante
 12     def __init__(self, parent: 'Feature', children: Sequence['Feature'], card_min: int, card_max: int):
 13         self.parent = parent
 14         self.children = children
 15         self.card_min = card_min
 16         self.card_max = card_max
 17 
 18     #Este método añade un elemento a la colección de hijos de una relación
 19     def add_child(self, feature: 'Feature'):
 20         self.children.append(feature)
 21 
 22     #Este método verifica la cardinalidad de la relación y el número de hijos para devolver true si es mandatory
 23     def is_mandatory(self) -> bool:
 24         return (len(self.children) == 1 and self.card_max == 1 and self.card_min == 1)
 25 
 26     #Este método verifica la cardinalidad de la relación y el número de hijos para devolver true si es optional
 27     def is_optional(self) -> bool:
 28         return (len(self.children) == 1 and self.card_max == 1 and self.card_min == 0)
 29 
 30     #Este método verifica la cardinalidad de la relación y el número de hijos para devolver true si es or
 31     def is_or(self) -> bool:
 32         return (len(self.children) > 1 and self.card_max == len(self.children) and self.card_min == 1)
 33 
 34     #Este método verifica la cardinalidad de la relación y el número de hijos para devolver true si es alternative
 35     def is_alternative(self) -> bool:
 36         return (len(self.children) > 1 and self.card_max == 1 and self.card_min ==  1)
 37 
 38     #Este método to string en python
 39     def __str__(self):
 40         res = self.parent.name + '[' + str(self.card_min) + ',' + str(self.card_max) + ']'
 41         for _child in self.children:
 42             res += _child.name + ' '
 43         return res
 44 
 45 #Esto es la definición de la clase Feature
 46 class Feature:
 47 
 48     #Este es el método constructor. Aqui definimos que queremos que cada característica tenga un nombre y un conjunto de relaciones. 
 49     def __init__(self, name: str, relations: Sequence['Relation']):
 50         self.name = name
 51         self.relations = relations
 52 
 53     #Este es un metodo auxiliar para añadir una relación a una característica
 54     def add_relation(self, relation: 'Relation'):
 55         self.relations.append(relation)
 56 
 57     #Este método nos devuelve el conjunto de relaciones de una característica
 58     def get_relations(self):
 59         return self.relations
 60 
 61     #Este método to string en python
 62     def __str__(self):
 63         return self.name
 64 
 65 #Esta clase representa a un modelo de características
 66 class FeatureModel():
 67 
 68     #En el constructor solo encontramos una feature raiz
 69     def __init__(self, root: Feature):
 70         self.root = root
 71 
 72     #Este método devuelve el conjunto de relaciones existentes en el modelo
 73     def get_relations(self, feature=None):
 74         relations = []
 75         if not feature:
 76             feature = self.root
 77         for relation in feature.relations:
 78             relations.append(relation)
 79             for _feature in relation.children:
 80                 relations.extend(self.get_relations(_feature))
 81         return relations
 82 
 83     #Este método devuelve el conjunto de features de un modelo
 84     def get_features(self):
 85         features = []
 86         features.append(self.root)
 87         for relation in self.get_relations():
 88             features.extend(relation.children)
 89         return features
 90 
 91     #Este método devuelve la feature identificandola por el nombre (TODO: Implementar con __equals__)
 92     def get_feature_by_name(self, str) -> Feature:
 93         features = self.get_features
 94         for feat in features:
 95             if feat.name == str:
 96                 return feat
 97         raise ElementNotFoundException
 98 
 99     #Este método to string en python
100     def __str__(self) -> str:
101         res = 'root: ' + self.root.name + '\r\n'
102         for i, relation in enumerate(self.get_relations()):
103             res += f'relation {i}: {relation}\r\n'
104         return(res)
105 
106 #This is an abstract Operation. To define abstract methos we rely on ABC module of the core python distribution
107 class Operation(ABC):
108 
109     #This abstract method, executes an operation given a feature model
110     @abstractmethod
111     def execute(self, model: FeatureModel) -> 'Operation':
112         pass

También implementamos el sistema dentro del core para la carga de plugins fichero: core/frozen_points_plugins.py

  1 from types import FunctionType, ModuleType
  2 from typing import List
  3 
  4 # Este modulo nos permite obligar ciertas listas con tipos
  5 from collections import UserList
  6 # Estos modulos nos permiten explorar las clases existentes en distintos plugins 'a la java reflexion' 
  7 from importlib import import_module
  8 from pkgutil import iter_modules
  9 import inspect
 10 
 11 # Importamos las clases del core que vamos a necesitar (blackbox)
 12 from core.frozen_points_domain import Operation
 13 from core.frozen_points_domain import FeatureModel
 14 
 15 # Definimos las posibles rutas a los plugins
 16 PLUGIN_PATHS = [
 17     'plugin',
 18 ]
 19 
 20 # Definimos una coleccion para almacenar los objetos de tipo operacion. 
 21 # Es interesante que las colecciones tipadas funcionan como clases y pueden tener métodos
 22 class Operations(UserList):  # pylint: disable=too-many-ancestors
 23     data: List[Operation]
 24 
 25     def search_by_name(self, name: str) -> Operation:
 26         candidates = filter(lambda op: op.__name__ == name, self.data)
 27         try:
 28             operation = next(candidates, None)
 29         except StopIteration:
 30             raise OperationNotFound
 31         return operation
 32 
 33 # Definimos una clase que represente a un plugin dentro del framwork. 
 34 # Notese que también contiene los modulos y las operaciones que haya en la instalación de python
 35 class Plugin:
 36     def __init__(self, module: ModuleType) -> None:
 37         self.module: ModuleType = module
 38         self.operations: Operations = Operations()
 39 
 40     @property
 41     def name(self):
 42         return self.module.__name__.split('.')[-1]
 43     
 44     # Esta clase añade las operaciones de un modulo
 45     def append_operation(self, operation: Operation) -> None:
 46         self.operations.append(operation)
 47 
 48     def use_operation(self, name: str, src: FeatureModel) -> Operation:
 49         operation = self.operations.search_by_name(name)
 50         return operation().execute(model=src)
 51 
 52 # Definimos una lista de plugins y algunos métodos importantes
 53 class Plugins(UserList):  # pylint: disable=too-many-ancestors
 54     data: List[Plugin]
 55 
 56     # buscamos los plugins que se llamen como queremos
 57     def get_plugin_by_name(self, name: str):
 58         for plugin in self.data:
 59             if plugin.name == name:
 60                 return plugin
 61         raise PluginNotFound
 62 
 63     # devolvemos la lista de plugins            
 64     def get_plugin_names(self) -> List[str]:
 65         return [plugin.name for plugin in self.data]
 66 
 67     #devolvemos las operacione sde un plugin
 68     def get_operations_by_plugin_name(self, plugin_name: str) -> Operations:
 69         try:
 70             plugin = self.get_plugin_by_name(plugin_name)
 71             return plugin.operations
 72         except PluginNotFound:
 73             return Operations()
 74 
 75 #Esta es una clase especial que nos va a permitir enumerar y ejecutar las operaciones que tengamos dentro de los plugins
 76 class DiscoverMetamodels:
 77     
 78     #Cuando creamos la clase, esta inicializa la lista de modulos existentes y llama al discover para buscar plugins y modulos 
 79     def __init__(self):
 80         self.module_paths: List[ModuleType] = list()
 81         for path in PLUGIN_PATHS:
 82             try:
 83                 module: ModuleType = import_module(path)
 84                 self.module_paths.append(module)
 85             except ModuleNotFoundError:
 86                 print('ModuleNotFoundError %s', path)
 87         self.plugins: Plugins = self.discover()
 88 
 89     #Este metodo busca las clases existentes en los módulos encontrados
 90     def search_classes(self, module):
 91         classes = []
 92         for _, file_name, ispkg in iter_modules( module.__path__, module.__name__ + '.' ):
 93             if ispkg:
 94                 classes += self.search_classes(import_module(file_name))
 95             else:
 96                 _file = import_module(file_name)
 97                 classes += inspect.getmembers(_file, inspect.isclass)
 98         return classes
 99 
100     # Este método se encarga de buscar los plugins modulos y clases existentes en el path indicado como variable global
101     def discover(self) -> dict:
102         plugins = Plugins()
103         for pkg in self.module_paths:
104             for _, plugin_name, ispkg in iter_modules(pkg.__path__, pkg.__name__ + '.'):
105                 if not ispkg:
106                     continue
107                 module = import_module(plugin_name)
108                 plugin = Plugin(module=module)
109                 classes = self.search_classes(module)
110                 for _, _class in classes:
111                     if not _class.__module__.startswith(module.__package__):
112                         continue  # Exclude modules not in current package
113                     #!! Fijaos como añadimos a la colección de operaciones cuando la clase operaciones hereda de la clase abstracta !!
114                     inherit = _class.mro()
115                     if Operation in inherit:
116                         plugin.append_operation(_class)   
117                 plugins.append(plugin)
118         return plugins

Finalmente implementaremos un plugin fichero: plugin/count_leafs/count_leafs_op.py

 1 from core.frozen_points_domain import Operation
 2 from core.frozen_points_domain import FeatureModel
 3 
 4 class CountLeafs(Operation):
 5 
 6     def execute(self, model: FeatureModel) -> 'Operation':
 7         result = 0
 8         for feat in model.get_features():
 9             if len(feat.get_relations())==0:
10                 result= result +1
11         return result


  • Instanciación: Finalmente implementaremos una aplicación que consuma tanto nuestros módulos del core como el plugin creado.

fichero:./main.py

 1 from core.frozen_points_domain import *
 2 from core.frozen_points_plugins import DiscoverMetamodels
 3 
 4 # Creamos el manager y lo inicializamos
 5 dm = DiscoverMetamodels()
 6 #Buscamos los plugins disponibles
 7 available_plugins = dm.discover()
 8 
 9 #Creamos un modelo de manera programatica
10 feature_b = Feature('B', [])
11 relation = Relation(parent=None, children=[feature_b], card_min=0, card_max=1)	
12 feature_a = Feature('A', [relation])	
13 relation.parent = feature_a
14 fm = FeatureModel(feature_a)
15 
16 #Imprimimos ese modelo
17 print(fm)
18 
19 #Imprimimos los plugins disponibles
20 print(available_plugins.get_plugin_names())
21 
22 #Buscamos el plugin que acabamos de crear
23 plugin=available_plugins.get_plugin_by_name('count_leafs')
24 
25 #Imprimimos las operaciones de los plugins
26 print(plugin.operations)
27 
28 #Usamos la operacion CountLeafs
29 result=plugin.use_operation('CountLeafs',fm)
30 
31 #Imprimimos el resultado
32 print("El modelo tiene " + result + "Features hojas")

--IMPORTANTE-- Añadir los ficheros __init__.py para que las carpetas core y core_leafs sean modulos Python

Iteración 2


Se ha introducido un nuevo requisito para un nuevo producto en el que se nos pide una operación que cuente el número de productos del modelo. TIEMPO 20 minutos

  • Análisis: Identificamos los frozen points de nuestro framework al solicitarnos la implementación de una operación que nos diga cuantas características tiene un modelo
    • Clases del dominio: Añadiremos las clases necesarias para implementar distintos tipos de serializadores de ficheros.
  • Diseño: En este apartado crearemos un nuevo plugin que implemente esa operación
  • Instanciación: Finalmente implementaremos una aplicación que consuma tanto nuestros módulos del core como el nuevo plugin creado.