Desarrollo de frameworks
De Wiki de EGC
Revisión del 18:50 13 ene 2021 de Jagalindo (discusión | contribuciones) (→Preparación para práctica sobre creación de frameworks)
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
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 pluggins
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
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.
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")
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.