1 août 2022

Fabrique – Design Patterns

De nombreuses applications financières doivent persister des données après leurs traitements algorithmiques. Par exemple, la gestion des portefeuilles demande d’enregistrer localement les résultats des ordres venant de différentes sources externes après le nettoyage des données dans les systèmes de stockage très variés : le système de gestion de base de données relationnelles (SGBDR), le système de gestion de base de données orienté objet (OODBMS), les documents XML, les fichiers Excel, le service Web, LDAP, etc. L’utilisateur pourrait appeler une méthode dans un module commun, qui contient tous les types d’enregistrement, en choisissant une manière de stockage désirée au gré du besoin. Néanmoins, si elle comprend tous ces types d’implémentations concrètes dont chacune propose une interface spécifique, elle ne sera pas facile à être maintenue ni étendue aux nouvelles fonctionnalités de persistance chez le client ultérieurement. Car cela viole le principe de responsabilité unique dans SOLID [1], qui stipule qu’un module, une classe ou même une méthode doit avoir une responsabilité unique et bien définie. Il ne devrait faire qu’une seule chose et n’avoir qu’une seule raison de changer. Sinon, quoi qu’elle soit dérisoire ou importante, une mise à jour sur une seule façon de stockage pourrait introduire la modification de toute la classe, voire tout le module, en risquant d’introduire les erreurs désastreuses.

Du coup en face de tel défi, le patron de conception dans la famille créatrice [2] « l’usine » : la fabrique (factory method en anglais) joue un rôle majeur et efficace en construisant tous les types d’objet qui s’occupe d’une persistance particulière à la sortie de résultat. Du fait, il y a une super classe abstraite de persistance avec plusieurs sous-classes concrètes et en fonction de l’argument d’entrée, la méthode de création de la classe factory va déterminer et retourner l’une des sous-classes. Cette conception retire la responsabilité de l’instanciation du programme chez le client aux sous-classes dans la fabrique. Sinon, au gré des contextes, elle pourrait renvoyer plusieurs fois la même instance [3, 4], ou renvoyer une sous-classe plutôt qu’un objet de ce type exact.

Conception

Le diagramme ULM illustre deux composants principaux dans le patron de conception Fabrique en Python 3, dont le 1e est un point d’accès de classe Factory qui décide et retourne l’objet final et le 2e comprend plusieurs classes concrètes dérivant d’une même interface, qui s’occupent d’instancier les objets.

Diagramme Patron de conception Fabrique Python 3 - Design patterns factory

Le client qui cherche à créer un objet accède à la classe Factory, dans laquelle la méthode statique make est appelée avec le nom de l’objet désiré. Cette méthode statique connaît tous les types d’instance des classes dérivant de l’interface en commun. Dès son analyse du besoin du client, la méthode renvoie directement la référence de l’objet demandé, lorsque cet objet est construit par la méthode de classe create implémentée dans une sous-classe concrète de l’interface abstraite.

Par conséquent, le principe de responsabilité unique est bien satisfait en faveur de la conception.

Développement

En Python 3 afin de distinguer clairement les types d’objet lors des appels dans le code, on met en place une classe énumérée PersistType qui donne tous les noms de persistance. Par exemple, dans cette application rudimentaire on a trois types de persistance dont les libraires en Python sont disponibles pour les ordres : XML [5], SGBDR [6], OODBMS [7].

Afin de simplifier la démonstration, on garde simplement les attributs basiques dans la classe Order pour les instances d’ordres. Du fait, à part les identifiants uniques pour chacun, on définit la date d’exécution, le montant d’exécution et la devise chez l’ordre. En plus, dans cet exemple le portefeuille ne contient que dix ordres exécutés il y a un jour. Les montants des ordres à la source sont générés de la façon aléatoire à la distribution uniforme [8] entre -100000,0 et 100000,0, lorsque leurs devises sont toujours en euro. Sinon, comme OODBMS demande un héritage de persistance dans sa librairie pour les ordres, on conçoit la classe OrderZo pour s’adapter à son objet d’enregistrement.

Dans un premier temps, on développe le 1e composant dans le schéma de la conception. En tant qu’un point d’accès, la classe PersistanceFactory est mise au point pour fournir les objets de persistance dont on a besoin. En sachant le nom du type de persistance, on y appelle la méthode statique make qui vérifie les conditions. Grâce à cette méthode, on peut trouver la classe de persistance pertinente à poursuivre l’instanciation concrète chez le 2e composant de la conception.

En effet, toutes ces classes de persistance ayant une méthode de classe create pour créer ses propres objets sont concrètement implémentées à partir de la interface abstraite IPersistance. Sinon, on y met en œuvre une méthode persist qui est responsable d’enregistrer les données des ordres pendant nos tests. Dès qu’une persistance instanciée sera été retournée par la méthode make chez la classe PersistanceFactory, la mise en persistance des données s’apprête à avoir lieu si la méthode persist est appelée.

# resistance_factory.py
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from random import uniform 
from xml.dom import minidom
from xml.etree import ElementTree as ET 
from ZODB import FileStorage
import BTrees.OOBTree
import enum
import persistent.list
import pprint
import sqlite3
import transaction
import ZODB

pp = pprint.PrettyPrinter(indent=4)


class PersistType(enum.Enum):
    """ An Enumerate Class to distinguish persistence """
    XML = 1
    SGBDR = 2
    OODBMS = 3


class IPersistence(ABC):
    """ An Abstract Class Interface (Persistence) """
    name = None

    @classmethod
    @abstractmethod
    def create(cls):
        """ An abstract interface method to create persistence """
        pass

    @abstractmethod
    def persist(self, orders: list):
        """
        An abstract interface method to persist orders

        Attributes
        ----------
        orders : list
        The orders to stock.
        """
        pass


class Order(object):
    def __init__(self):
        self._order_id = None
        self._pricing_date = None
        self._amount = None
        self._currency = None

    @property
    def order_id(self):
        return self._order_id

    @property
    def pricing_date(self):
        return self._pricing_date

    @property
    def amount(self):
        return self._amount

    @property
    def currency(self):
        return self._currency

    @order_id.setter
    def order_id(self, value):
        self._order_id = value

    @pricing_date.setter
    def pricing_date(self, value):
        self._pricing_date = value

    @amount.setter
    def amount(self, value):
        self._amount = value

    @currency.setter
    def currency(self, value):
        self._currency = value

    def __repr__(self):
        message = f"<{type(self).__name__}: "
        message += "; ".join(f"{k}, {v}" for k, v in vars(self).items())
        message += '>'
        return message


class OrderZo(Order, persistent.Persistent):
    def __init__(self, odr: Order):
        """
        :param odr: Order
        to adapt the ZODB persistent object
        """
        super().__init__()
        self._order_id = odr.order_id
        self._pricing_date = odr.pricing_date
        self._amount = odr.amount
        self._currency = odr.currency


class PersistenceOfXML(IPersistence):
    """ A Concrete XML Class that implements the IPersistence interface """
    name = PersistType.XML

    def __init__(self):
        self._engine = 'XML Engine'
        self.root = ET.Element('orders')

    def persist(self, orders: list):
        for order in orders:
            order_et = ET.SubElement(self.root, 'order')
            o_id = ET.SubElement(order_et, 'order_id')
            o_id.text = str(order.order_id)
            pricing_date = ET.SubElement(order_et, 'pricing_date')
            pricing_date.text = order.pricing_date.strftime(
                '%Y/%m/%d %H:%M:%S.%f')
            amount = ET.SubElement(order_et, 'amount')
            amount.text = str(order.amount)
            currency = ET.SubElement(order_et, 'currency')
            currency.text = order.currency
        xml_str = minidom.parseString(
            ET.tostring(self.root)).toprettyxml(
            indent="   "
        )
        with open("XML.xml", "w") as f:
            f.write(xml_str)
        pp.pprint(f"populating")
        pp.pprint(orders)
        pp.pprint(f"by {self._engine}...")

    @classmethod
    def create(cls) -> IPersistence:
        print('XML persistence will be ready...')
        return cls()


class PersistenceOfSGBDR(IPersistence):
    """ A Concrete SGBDR Class that implements the IPersistence interface """
    name = PersistType.SGBDR

    def __init__(self):
        self._engine = 'SGBDR Engine'
        self.conn = None
        try:
            self.conn = sqlite3.connect('SGBDR.db')
        except Exception as e:
            print(e)
        cur = self.conn.cursor()
        # Drop any existing table with the same name
        try:
            drop_table = "DROP TABLE ORDERS"
            cur.execute(drop_table)
        except Exception as e:
            print(e)
        # Create a table in the disk file based database
        create_table = (f"CREATE TABLE IF NOT EXISTS ORDERS("
                        f"ORDER_ID INTEGER PRIMARY KEY, "
                        f"PRICING_DATE DATETIME NOT NULL, "
                        f"AMOUNT NUMERIC(10,8), "
                        f"CURRENCY CHARACTER(3))")

        cur.execute(create_table)
        # commit the table to db
        self.conn.commit()

    def persist(self, orders: list):
        cur = self.conn.cursor()
        for order in orders:
            sql = '''
            INSERT INTO ORDERS(ORDER_ID, PRICING_DATE, AMOUNT, CURRENCY)
            VALUES(?,?,?,?)
            '''
            cur.execute(sql, (order.order_id,
                              order.pricing_date,
                              order.amount,
                              order.currency))
        self.conn.commit()
        self.conn.close()
        pp.pprint(f"populating")
        pp.pprint(orders)
        pp.pprint(f"by {self._engine} ending in {cur.lastrowid}...")

    @classmethod
    def create(cls) -> IPersistence:
        print('SGBDR persistence will be ready...')
        return cls()


class PersistenceOfOODBMS(IPersistence):
    """ A Concrete OODBMS Class that implements the IPersistence interface """
    name = PersistType.OODBMS

    def __init__(self):
        self._engine = 'OODBMS Engine'
        storage = FileStorage.FileStorage('OODBMS.fs')
        db = ZODB.DB(storage)
        connection = db.open()
        self.root = connection.root

    def persist(self, orders: list):
        self.root.orders = BTrees.OOBTree.BTree()
        self.root.orders = [OrderZo(z) for z in orders]
        transaction.commit()
        pp.pprint(f"populating")
        pp.pprint(orders)
        pp.pprint(f"by {self._engine}...")

    @classmethod
    def create(cls) -> IPersistence:
        print('OODBMS persistence will be ready...')
        return cls()


class PersistenceFactory(object):
    """ The Persistence Factory Class """

    @staticmethod
    def make(name) -> IPersistence:
        """ A static method to get a concrete product """
        if name == PersistType.XML:
            return PersistenceOfXML.create()
        if name == PersistType.SGBDR:
            return PersistenceOfSGBDR.create()
        if name == PersistType.OODBMS:
            return PersistenceOfOODBMS.create()


if __name__ == '__main__':
    # The Client
    for i, t in enumerate(PersistType):
        print(f"In Phase {i}: ")
        persistence = PersistenceFactory().make(t)
        order_samples = []
        for order_id in range(10):
            o = Order()
            o.order_id = order_id
            o.pricing_date = datetime.today() - timedelta(days=1)
            o.amount = uniform(-10000.0, 10000.0)
            o.currency = 'EUR'
            order_samples.append(o)
        persistence.persist(order_samples)
        assert persistence.name is t

Les tests unitaires se valident par les types des objets créés au gré du besoin, tandis que les affichages des différentes phases de persistance pour dix ordres sont mis en lumière en-dessous :

In Phase 0: 
XML persistence will be ready...
'populating'
[   <Order: _order_id, 0; _pricing_date, 2022-03-12 22:57:18.577233; _amount, 8682.064027577868; _currency, EUR>,
    <Order: _order_id, 1; _pricing_date, 2022-03-12 22:57:18.577271; _amount, 1178.6677560764638; _currency, EUR>,
    <Order: _order_id, 2; _pricing_date, 2022-03-12 22:57:18.577280; _amount, 877.1820266699797; _currency, EUR>,
    <Order: _order_id, 3; _pricing_date, 2022-03-12 22:57:18.577285; _amount, 3557.339177963937; _currency, EUR>,
    <Order: _order_id, 4; _pricing_date, 2022-03-12 22:57:18.577290; _amount, -8528.180149274358; _currency, EUR>,
    <Order: _order_id, 5; _pricing_date, 2022-03-12 22:57:18.577294; _amount, -4527.9443459321665; _currency, EUR>,
    <Order: _order_id, 6; _pricing_date, 2022-03-12 22:57:18.577299; _amount, -6062.7853847754905; _currency, EUR>,
    <Order: _order_id, 7; _pricing_date, 2022-03-12 22:57:18.577303; _amount, 1449.8770822999832; _currency, EUR>,
    <Order: _order_id, 8; _pricing_date, 2022-03-12 22:57:18.577308; _amount, -1708.4810066747305; _currency, EUR>,
    <Order: _order_id, 9; _pricing_date, 2022-03-12 22:57:18.577317; _amount, 5091.879830019116; _currency, EUR>]
'by XML Engine...'
In Phase 1: 
SGBDR persistence will be ready...
'populating'
[   <Order: _order_id, 0; _pricing_date, 2022-03-12 22:57:18.588848; _amount, 8990.32508880787; _currency, EUR>,
    <Order: _order_id, 1; _pricing_date, 2022-03-12 22:57:18.588875; _amount, -7029.962047408302; _currency, EUR>,
    <Order: _order_id, 2; _pricing_date, 2022-03-12 22:57:18.588884; _amount, -6521.53303624182; _currency, EUR>,
    <Order: _order_id, 3; _pricing_date, 2022-03-12 22:57:18.588891; _amount, 8693.092444597798; _currency, EUR>,
    <Order: _order_id, 4; _pricing_date, 2022-03-12 22:57:18.588897; _amount, 9584.87989883318; _currency, EUR>,
    <Order: _order_id, 5; _pricing_date, 2022-03-12 22:57:18.588903; _amount, -8554.88392848472; _currency, EUR>,
    <Order: _order_id, 6; _pricing_date, 2022-03-12 22:57:18.588908; _amount, 3109.2992332277263; _currency, EUR>,
    <Order: _order_id, 7; _pricing_date, 2022-03-12 22:57:18.588914; _amount, -7536.156871450268; _currency, EUR>,
    <Order: _order_id, 8; _pricing_date, 2022-03-12 22:57:18.588919; _amount, 8377.004299016604; _currency, EUR>,
    <Order: _order_id, 9; _pricing_date, 2022-03-12 22:57:18.588925; _amount, 4520.661799666674; _currency, EUR>]
'by SGBDR Engine ending in 9...'
In Phase 2: 
OODBMS persistence will be ready...
'populating'
[   <Order: _order_id, 0; _pricing_date, 2022-03-12 22:57:18.595025; _amount, 4467.882449831253; _currency, EUR>,
    <Order: _order_id, 1; _pricing_date, 2022-03-12 22:57:18.595049; _amount, 5956.087440158884; _currency, EUR>,
    <Order: _order_id, 2; _pricing_date, 2022-03-12 22:57:18.595058; _amount, 7233.788601777396; _currency, EUR>,
    <Order: _order_id, 3; _pricing_date, 2022-03-12 22:57:18.595065; _amount, 8032.730099866159; _currency, EUR>,
    <Order: _order_id, 4; _pricing_date, 2022-03-12 22:57:18.595072; _amount, 118.40014380573666; _currency, EUR>,
    <Order: _order_id, 5; _pricing_date, 2022-03-12 22:57:18.595079; _amount, 2196.295149587426; _currency, EUR>,
    <Order: _order_id, 6; _pricing_date, 2022-03-12 22:57:18.595087; _amount, 580.9771003834958; _currency, EUR>,
    <Order: _order_id, 7; _pricing_date, 2022-03-12 22:57:18.595093; _amount, 9547.969637860293; _currency, EUR>,
    <Order: _order_id, 8; _pricing_date, 2022-03-12 22:57:18.595100; _amount, 3679.870172622952; _currency, EUR>,
    <Order: _order_id, 9; _pricing_date, 2022-03-12 22:57:18.595108; _amount, -4532.329828675801; _currency, EUR>]
'by OODBMS Engine...'

Comme tous les types de persistance sont testés par trois itérations dans le programme, les objets de persistance XML, SGBDR et OODBMS sont instanciés l’un après l’autre avant leurs enregistrements de données des ordres.

Le ficher XML.xml stocke les ordres ainsi :

<?xml version="1.0" ?>
<orders>
   <order>
      <order_id>0</order_id>
      <pricing_date>2022/03/12 22:57:18.577233</pricing_date>
      <amount>8682.064027577868</amount>
      <currency>EUR</currency>
   </order>
   <order>
      <order_id>1</order_id>
      <pricing_date>2022/03/12 22:57:18.577271</pricing_date>
      <amount>1178.6677560764638</amount>
      <currency>EUR</currency>
   </order>
   <order>
      <order_id>2</order_id>
      <pricing_date>2022/03/12 22:57:18.577280</pricing_date>
      <amount>877.1820266699797</amount>
      <currency>EUR</currency>
   </order>
   <order>
      <order_id>3</order_id>
      <pricing_date>2022/03/12 22:57:18.577285</pricing_date>
      <amount>3557.339177963937</amount>
      <currency>EUR</currency>
   </order>
   <order>
      <order_id>4</order_id>
      <pricing_date>2022/03/12 22:57:18.577290</pricing_date>
      <amount>-8528.180149274358</amount>
      <currency>EUR</currency>
   </order>
   <order>
      <order_id>5</order_id>
      <pricing_date>2022/03/12 22:57:18.577294</pricing_date>
      <amount>-4527.9443459321665</amount>
      <currency>EUR</currency>
   </order>
   <order>
      <order_id>6</order_id>
      <pricing_date>2022/03/12 22:57:18.577299</pricing_date>
      <amount>-6062.7853847754905</amount>
      <currency>EUR</currency>
   </order>
   <order>
      <order_id>7</order_id>
      <pricing_date>2022/03/12 22:57:18.577303</pricing_date>
      <amount>1449.8770822999832</amount>
      <currency>EUR</currency>
   </order>
   <order>
      <order_id>8</order_id>
      <pricing_date>2022/03/12 22:57:18.577308</pricing_date>
      <amount>-1708.4810066747305</amount>
      <currency>EUR</currency>
   </order>
   <order>
      <order_id>9</order_id>
      <pricing_date>2022/03/12 22:57:18.577317</pricing_date>
      <amount>5091.879830019116</amount>
      <currency>EUR</currency>
   </order>
</orders>

Dans la base de Sqlite3, nous pouvons saisir toutes les données dans la table ORDERS :

sqlite> sqlite> SELECT * FROM ORDERS;
0|2022-03-12 22:57:18.588848|8990.32508880787|EUR
1|2022-03-12 22:57:18.588875|-7029.9620474083|EUR
2|2022-03-12 22:57:18.588884|-6521.53303624182|EUR
3|2022-03-12 22:57:18.588891|8693.0924445978|EUR
4|2022-03-12 22:57:18.588897|9584.87989883318|EUR
5|2022-03-12 22:57:18.588903|-8554.88392848472|EUR
6|2022-03-12 22:57:18.588908|3109.29923322773|EUR
7|2022-03-12 22:57:18.588914|-7536.15687145027|EUR
8|2022-03-12 22:57:18.588919|8377.0042990166|EUR
9|2022-03-12 22:57:18.588925|4520.66179966667|EUR

Les résultats gérés chez OODBMS peuvent être décodés ainsi :

{'_order_id': 0, '_pricing_date': datetime.datetime(2022, 3, 12, 22, 57, 18, 595025), '_amount': 4467.882449831253, '_currency': 'EUR'}
{'_order_id': 1, '_pricing_date': datetime.datetime(2022, 3, 12, 22, 57, 18, 595049), '_amount': 5956.087440158884, '_currency': 'EUR'}
{'_order_id': 2, '_pricing_date': datetime.datetime(2022, 3, 12, 22, 57, 18, 595058), '_amount': 7233.788601777396, '_currency': 'EUR'}
{'_order_id': 3, '_pricing_date': datetime.datetime(2022, 3, 12, 22, 57, 18, 595065), '_amount': 8032.730099866159, '_currency': 'EUR'}
{'_order_id': 4, '_pricing_date': datetime.datetime(2022, 3, 12, 22, 57, 18, 595072), '_amount': 118.40014380573666, '_currency': 'EUR'}
{'_order_id': 5, '_pricing_date': datetime.datetime(2022, 3, 12, 22, 57, 18, 595079), '_amount': 2196.295149587426, '_currency': 'EUR'}
{'_order_id': 6, '_pricing_date': datetime.datetime(2022, 3, 12, 22, 57, 18, 595087), '_amount': 580.9771003834958, '_currency': 'EUR'}
{'_order_id': 7, '_pricing_date': datetime.datetime(2022, 3, 12, 22, 57, 18, 595093), '_amount': 9547.969637860293, '_currency': 'EUR'}
{'_order_id': 8, '_pricing_date': datetime.datetime(2022, 3, 12, 22, 57, 18, 595100), '_amount': 3679.870172622952, '_currency': 'EUR'}
{'_order_id': 9, '_pricing_date': datetime.datetime(2022, 3, 12, 22, 57, 18, 595108), '_amount': -4532.329828675801, '_currency': 'EUR'}

De cette façon, on réussit à réaliser les deux composants principaux dans le patron de conception Fabrique en Python 3, qui surmonte la violation du principe de responsabilité unique pour la persistance des ordres chez l’utilisateur montrée au début de l’article. Désormais on pourrait enrichir à l’aise autant de types d’objet que l’on veut aux contextes plus compliqués, tout en ne rajoutant les conditions de justification et les classes dérivées que dans les deux composants respectivement, lorsque les anciennes fonctionnalités opérationnelles ne risquent pas d’être impactées dans les applications.

Conclusion

Dans cet article, on met en évidence la nécessité du patron de conception Fabrique contre l’issue de persistance dans notre contexte en question. Son diagramme ULM comprend deux principaux composants qui respectent le principe de responsabilité unique. Ce patron de conception créateur est implémenté et testé en Python 3 pour une application rudimentaire, lorsque l’enrichissement des nouvelles créations pourrait passer à l’échelle dans le code en sécurité.

Références

[1] https://www.invivoo.com/lart-clean-code-environnement-java/
[2] https://www.invivoo.com/design-patterns-patrons-conception/
[3] https://www.invivoo.com/singleton-design-patterns-part1/
[4] https://www.invivoo.com/singletons-design-patterns-partie-2/
[5] https://www.tutorialspoint.com/python/python_xml_processing.htm
[6] https://docs.python.org/3/library/sqlite3.html
[7] https://zodb.org/en/latest/
[8] https://docs.python.org/fr/3/library/random.html

Expertise Design & Code

$

Python, Java, C++, C# et Front-End

Découvrez nos formations

$

Parcourir le catalogue

Boostez votre carrière !

$

Nos offres pour Développeurs

Tous les mois recevez nos derniers articles !

Try X4B now !

Découvrez gratuitement XComponent for Business. Notre solution logicielle BizDevOps !

Écrit par Dr. Ruijing HU

0 Comments

Submit a Comment

Votre adresse e-mail ne sera pas publiée.