26 août 2016

Design Pattern Composite en Python 3

Design & Code

Dr. Ruijing

HU

Image de pattern.

26 août 2016

Design Pattern Composite en Python 3

Design & Code

Dr. Ruijing

HU

Image de pattern.

26 août 2016

Design Pattern Composite en Python 3

Design & Code

Dr. Ruijing

HU

Image de pattern.

1. Contexte Financier

De nombreuses applications financières gèrent des comptes de clients nécessitant des consultations de soldes des différents produits financiers ou sous-comptes. Par exemple, le compte financier fait partie de la balance des paiements d'un pays. Il mesure les variations de la propriété internationale d'actifs appartenant à des particuliers, des entreprises, au gouvernement ou à sa banque centrale. Les actifs incluent les investissements directs, les titres comme les actions et obligations, et les matières premières telles que l'or, et les devises. Le compte financier rapporte la variation totale des avoirs internationaux détenus. Il indique si le nombre d'actifs détenus a augmenté ou diminué, sans préciser le montant total des actifs. Une augmentation signifie que des capitaux étrangers entrent dans le pays, tandis qu'une diminution indique que des capitaux nationaux sortent vers des marchés étrangers. Le compte financier comprend deux sous-comptes principaux. Le premier présente la propriété nationale des actifs étrangers, mesurant les sorties d'argent pour acheter des actifs étrangers, et le second démontre la propriété étrangère des actifs nationaux, mesurant les entrées d'argent pour acheter des actifs domestiques. La seule différence entre ces sous-comptes réside dans l'origine de la propriété des actifs.

Le sous-compte de la propriété nationale des actifs étrangers se divise en trois types de comptes : réserves privées, gouvernementales et de la banque centrale. Quelle que soit l'entité propriétaire, une augmentation soustrait du compte financier. Les sous-comptes des propriétaires privés incluent les particuliers et les entreprises, et peuvent être classifiés par les comptes de dépôt dans des banques étrangères, des prêts aux étrangers, des titres de sociétés étrangères, des investissements directs réalisés à l'étranger, et des matières premières détenues dans d'autres pays. Les comptes des propriétaires gouvernementaux, principalement fédéraux, incluent principalement de l'or et des devises étrangères détenues en réserve, ainsi que la position de réserve au Fonds Monétaire International (FMI). La banque centrale peut posséder ces mêmes actifs, à l'exception de la position de réserve au FMI, et détient également des swaps de devises avec d'autres banques centrales.

Le sous-compte de la propriété étrangère d'actifs nationaux se divise en actifs privés et officiels étrangers. Lorsque les étrangers augmentent leur propriété des actifs d'un pays, le montant s'ajoute au compte financier. Les comptes domestiques incluent les dépôts détenus par des étrangers dans les banques nationales, les prêts accordés par des banques étrangères, les achats privés étrangers de titres d'État, les titres de sociétés détenus par des étrangers, les investissements directs étrangers, et d'autres dettes envers les étrangers. Les comptes officiels étrangers incluent les actifs détenus par des gouvernements ou banques centrales étrangères, ainsi que les expéditions nettes de la monnaie nationale aux gouvernements étrangers.

Le compte financier est une composante cruciale de la balance des paiements. Il s'ajoute à la balance des paiements lorsqu'il est positif et que des capitaux étrangers entrent pour acheter des actifs, et il soustrait de la balance des paiements lorsque des capitaux nationaux sortent pour acheter des actifs étrangers. Si le compte financier enregistre un excédent significatif, il peut compenser un déficit commercial. En cas de déficit commercial compensé par un excédent du compte financier, cela indique que le pays vend ses actifs pour payer des biens et services étrangers.

Ainsi, l'accumulation des soldes de tous les sous-comptes dans cette structure hiérarchique devient cruciale, car l'ajout ou le retrait d'un compte pourrait radicalement changer l'application si elle se basait uniquement sur une simple somme des sous-comptes élémentaires.

En informatique, l'algorithme « diviser pour régner » [1] est adapté à la sommation arborescente, calculant récursivement ou directement tous les sous-comptes du niveau le plus bas au plus haut. Les modèles de patron de conception structurelle [2] offrent des méthodes pour assembler des objets et classes dans des structures complexes, tout en maintenant flexibilité et efficacité.

Le patron de conception structurel composite permet d'organiser les objets en arborescence pour les traiter intégralement comme des objets individuels, évitant ainsi de redéfinir le traitement pour chaque individu. Le principe est d'identifier les classes feuille (scalaires ou primitives) et les classes composite (vecteur ou conteneur) comme illustré dans la figure 1, avec un composant de base nécessaire pour rendre toutes les classes concrètes interchangeables. Le parcours récursif des classes composite jusqu'aux classes feuille appelle de manière homogène l'opération de sommation pour obtenir le résultat final. Ainsi, bien que le nombre de comptes ne soit pas connu à l'avance et puisse évoluer dynamiquement, la définition du calcul cumulatif des soldes reste identique pour chaque compte dans l'arborescence.

Dans cet article, nous mettrons en œuvre le patron de conception composite en Python 3, pour résoudre le problème de l'accumulation dans une structure arborescente d'entités complexes, tout en respectant le principe SOLID [3].

2. Conception de composite

Le diagramme UML de la figure 2 illustre le développement du patron de conception composite. Le modèle de notre application peut être représenté sous forme d'arborescence, avec deux classes : la feuille et le composite. Les composites doivent contenir à la fois des feuilles simples et d'autres composites, et tous les comptes partagent une interface commune : le composant de base. Grâce à cette interface, le client n'a pas besoin de connaître la classe concrète des objets manipulés.

Toutes les classes concrètes ont une relation « est un » avec le composant de base, et les composites se couplent à l'interface par composition récursive. Les composites comprennent des instances en hiérarchie, héritant du composant de base. Les composites délèguent la plupart de leurs opérations à leurs classes fille via le polymorphisme en Python 3. Les méthodes d'ajout et de suppression d'éléments sont définies dans les composites.

La structure de composite suit correctement le principe ouvert/fermé de SOLID [3]. De nouveaux types d'éléments peuvent être introduits sans casser le code existant.

Il faut noter que placer les opérations concrètes dans l'interface du composant de base violerait le principe de ségrégation d'interface de SOLID [3], car les méthodes seraient vides dans la classe feuille, même si le client pourrait traiter tous les éléments de la même manière.

Dans notre contexte, l'opération du composant de base est d'obtenir le solde total de compte. Deux implémentations suffisent pour le modèle. Les comptes des classes feuilles fournissent directement les valeurs, tandis que les comptes des classes composites calculent récursivement les sommes des soldes de leurs sous-comptes.

Le client instancie les classes du niveau bas vers le haut, insérant progressivement les objets dans l'arborescence. Une fois la structure de composite créée pour tous les comptes, on peut appeler la méthode de solde d'une instance pour obtenir le résultat correspondant.

3. Développement de composite

Dans notre exemple, nous analysons l'économie en suivant l'évolution des soldes des comptes financiers en euros d'un pays entre 2001 et 2010. Pour simplifier, nous initialisons chaque compte feuille avec un montant fictif de 1 milliard d'euros en 2001, augmentant de 1 million d'euros chaque année. Nous instancions ces objets avant de les insérer dans les composites. Si les devises diffèrent, nous utilisons la librairie currency_converter pour convertir les montants. Enfin, le client obtient les soldes sur dix ans dans une boucle for, dessinant leur évolution avec matplotlib.

Pour démontrer l'avantage de notre structure face aux changements dynamiques, supposons que le pays suspende sa collaboration avec le FMI en supprimant son compte de réserve entre 2003 et 2008, avant de le rétablir en 2009.

Nous nous appuyons sur l'interface IAccount proposant des méthodes abstraites [4] aux comptes simples et complexes. La classe SimpleAccount connaît directement le solde par conversion de devise, tandis que la classe ComplexAccount somme récursivement les soldes de ses sous-comptes. Le calcul n'atteint chaque nœud qu'une seule fois, rendant le cache [5] inutile.

Les méthodes d'ajout et de suppression de comptes dans la classe ComplexAccount permettent de gérer les changements dynamiques. Dans notre cas, le compte « imf reserve government account » disparaît entre 2003 et 2008.

# composite.py
from abc import ABC, abstractmethod
from currency_converter import CurrencyConverter
from datetime import date
from matplotlib import pyplot as plt
from typing import List, Optional, Tuple
ACCOUNT_CCY = 'EUR'
class IAccount(ABC):
    @abstractmethod
    def get_balance(self) -> Tuple[float, str]:
        """
        get balance
        :return: tuple of the amount and currency
        """
        raise NotImplementedError
    @property
    @abstractmethod
    def amount(self) -> float:
        """
        get account amount
        :return: account amount
        """
        raise NotImplementedError
    @property
    @abstractmethod
    def currency(self) -> str:
        """
        get account currency
        :return: account accurency
        """
        raise NotImplementedError
    @property
    @abstractmethod
    def name(self) -> str:
        """
        get the account name
        :return: account name
        """
        raise NotImplementedError
    @property
    @abstractmethod
    def acc_date(self) -> date:
        """
        get the account writtent date
        :return: written date
        """
        raise NotImplementedError
class SimpleAccount(IAccount):
    """
    Simple Account (Leaf Node in Tree Structure)
    """
    def __init__(self,
                 acc_date: date,
                 name: str,
                 amount: float,
                 currency: str = ACCOUNT_CCY):
        """
        initialize simple account (leaf account)
        :param acc_date: account written date
        :param name: account name
        :param amount: account amount
        :param currency: account currency
        """
        self._name = name
        self._amount = amount
        self._currency = currency
        self._acc_date = acc_date
        self._convertor = CurrencyConverter()
    def get_balance(self) -> Tuple[float, str]:
        """
        get balance
        :return: tuple of the amount and currency
        """
        if self._currency == ACCOUNT_CCY:
            return self._amount, self._currency
        return self._convertor.convert(
            self._amount, self._currency, ACCOUNT_CCY,
            date=self._acc_date), ACCOUNT_CCY
    @property
    def amount(self) -> float:
        """
        get account amount
        :return: account amount
        """
        return self._amount
    @property
    def currency(self) -> str:
        """
        get account currency
        :return: account accurency
        """
        return self._currency
    @property
    def name(self) -> str:
        """
        get the account name
        :return: account name
        """
        return self._name
    @property
    def acc_date(self) -> date:
        """
        get the account writtent date
        :return: written date
        """
        return self._acc_date
class ComplexAccount(IAccount):
    """
    Complex Account (Inner Node in Tree Structure)
    """
    def __init__(self,
                 acc_date: date,
                 name: str,
                 accounts: Optional[List[IAccount]] = None,
                 currency: str = ACCOUNT_CCY):
        """
        initialize simple account (leaf account)
        :param acc_date: account written date
        :param name: account name
        :param accounts: comprised accounts in the complex account
        :param currency: currency
        """
        self._name = name
        self._currency = currency
        self._accounts = [] if accounts is None else accounts
        self._acc_date = acc_date
        self._convertor = CurrencyConverter()
    def add_account(self, acc: IAccount) -> bool:
        """
        add a new account into complex account
        :param acc: account to add
        :return: is successfully added?
        """
        if acc in self.accounts:
            print(f"{acc.name} has already existed in the account.")
            return False
        self._accounts.append(acc)
        return True
    def delete_account(self, acc_name: str) -> bool:
        """
        remove an account by its name from complex account
        :param acc_name: account name to delete
        :return: is successfully deleted?
        """
        if isinstance(self, SimpleAccount):
            return False
        for acc in self.accounts:
            if acc.name != acc_name:
                if isinstance(acc, ComplexAccount):
                    if acc.delete_account(acc_name):
                        return True
            else:
                self.accounts.remove(acc)
                return True
        return False
    def convert_amount_currency(self, amount: float,
                                origin_ccy: str,
                                target_ccy: str) -> float:
        """
        convert amount from origin currency to target currency
        :param amount: amount
        :param origin_ccy: origin currency
        :param target_ccy: target currency
        :return: converted amount
        """
        if origin_ccy == target_ccy:
            return amount
        return self._convertor.convert(amount, origin_ccy, target_ccy,
                                       self._acc_date)
    def get_balance(self) -> Tuple[float, str]:
        """
        get balance
        :return: tuple of the amount and currency
        """
        balance = 0
        for acc in self._accounts:
            balance += self.convert_amount_currency(acc.amount,
                                                    acc.currency,
                                                    self._currency)
        return balance, self._currency
    @property
    def amount(self) -> float:
        """
        get account amount
        :return: account amount
        """
        return self.get_balance()[0]
    @property
    def currency(self) -> str:
        """
        get account currency
        :return: account currency
        """
        return self._currency
    @property
    def name(self) -> str:
        """
        get the account name
        :return: account name
        """
        return self._name
    @property
    def acc_date(self) -> date:
        """
        get the account writtent date
        :return: written date
        """
        return self._acc_date
    @property
    def accounts(self) -> List[IAccount]:
        """
        get comprised account in the complex account
        :return: list of accounts in the complex account
        """
        return self._accounts
if __name__ == "__main__":
    financial_acc_balances = {}
    fictive_delta = 1e6
    for i, year in enumerate(range(2001, 2011)):
        print("-----------------------------------------------------------")
        account_date = date(year, 12, 31)
        print(f"account date: {account_date.strftime('%Y-%m-%d')}")
        # simple accounts
        foreign_owned_in_domestic_deposit_private_acc = SimpleAccount(
            account_date,
            'foreign owned in domestic deposit in private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        domestic_loan_to_foreign_private_acc = SimpleAccount(
            account_date,
            'domestic loan to foreign in private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        domestic_purchased_foreign_security_private_acc = SimpleAccount(
            account_date,
            'domestic purchased foreign security in private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        direct_invest_in_foreign_country_private_acc = SimpleAccount(
            account_date,
            'direct investment in foreign country in private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        commodity_private_acc = SimpleAccount(
            account_date,
            'commodity in private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        foreign_owned_in_domestic_deposit_gov_acc = SimpleAccount(
            account_date,
            'foreign owned in domestic deposit in government account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        domestic_loan_to_foreign_gov_acc = SimpleAccount(
            account_date,
            'domestic loan to foreign in government account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        domestic_purchased_foreign_security_gov_acc = SimpleAccount(
            account_date,
            'domestic purchased foreign security in government account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        direct_invest_in_foreign_country_gov_acc = SimpleAccount(
            account_date,
            'direct investment in foreign country in government account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        commodity_gov_acc = SimpleAccount(
            account_date,
            'commodity in government account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        imf_reserve_gov_acc = SimpleAccount(
            account_date,
            'imf reserve government account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        foreign_owned_in_domestic_deposit_cb_acc = SimpleAccount(
            account_date,
            'foreign owned in domestic deposit in central bank account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        domestic_loan_to_foreign_cb_acc = SimpleAccount(
            account_date,
            'domestic loan to foreign in central bank account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        domestic_purchased_foreign_security_cb_acc = SimpleAccount(
            account_date,
            'domestic purchased foreign security in central bank account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        direct_invest_in_foreign_country_cb_acc = SimpleAccount(
            account_date,
            'direct investment in foreign country in central bank account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        commodity_cb_acc = SimpleAccount(
            account_date,
            'commodity in central bank account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        currency_swap_cb_acc = SimpleAccount(
            account_date,
            'currency swap central bank account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        foreign_gov_shipment_acc = SimpleAccount(
            account_date,
            'foreign government shipment account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        foreign_cb_shipment_acc = SimpleAccount(
            account_date,
            'foreign central bank shipment account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        domestic_owned_in_foreign_deposit_fo_acc = SimpleAccount(
            account_date,
            'domestic owned in foreign deposit in foreign official account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        foreign_loan_to_domestic_fo_acc = SimpleAccount(
            account_date,
            'foreign loan to domestic in foreign official account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        foreign_purchased_domestic_obligation_fo_acc = SimpleAccount(
            account_date,
            'foreign purchased domestic obligation '
            'in foreign official account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        foreign_purchased_domestic_security_fo_acc = SimpleAccount(
            account_date,
            'foreign purchased domestic security in foreign official account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        direct_invest_in_domestic_country_fo_acc = SimpleAccount(
            account_date,
            'direct investment in domestic country '
            'in foreign official account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        debt_owed_to_foreign_country_fo_acc = SimpleAccount(
            account_date,
            'debt owed to foreign country in foreign official account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        hard_asset_fo_acc = SimpleAccount(
            account_date,
            'hard asset in foreign official account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        nation_currency_fo_acc = SimpleAccount(
            account_date,
            'nation currency in foreign official account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        domestic_owned_in_foreign_deposit_fp_acc = SimpleAccount(
            account_date,
            'domestic owned in foreign deposit in foreign private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        foreign_loan_to_domestic_fp_acc = SimpleAccount(
            account_date,
            'foreign loan to domestic in foreign private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        foreign_purchased_domestic_obligation_fp_acc = SimpleAccount(
            account_date,
            'foreign purchased domestic obligation '
            'in foreign private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        foreign_purchased_domestic_security_fp_acc = SimpleAccount(
            account_date,
            'foreign purchased domestic security in foreign private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        direct_invest_in_domestic_country_fp_acc = SimpleAccount(
            account_date,
            'direct investment in domestic country '
            'in foreign private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        debt_owed_to_foreign_country_fp_acc = SimpleAccount(
            account_date,
            'debt owed to foreign country in foreign private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        hard_asset_fp_acc = SimpleAccount(
            account_date,
            'hard asset in foreign private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        nation_currency_fp_acc = SimpleAccount(
            account_date,
            'nation currency in foreign private account',
            1e9 + i * fictive_delta,
            'EUR',
        )
        # complex accounts
        domestic_private_acc = ComplexAccount(
            account_date,
            'domestic private account',
            [
                foreign_owned_in_domestic_deposit_private_acc,
                domestic_loan_to_foreign_private_acc,
                domestic_purchased_foreign_security_private_acc,
                direct_invest_in_foreign_country_private_acc,
                commodity_private_acc,
            ],
            'EUR',
        )
        government_acc = ComplexAccount(
            account_date,
            'government account',
            [
                foreign_owned_in_domestic_deposit_gov_acc,
                domestic_loan_to_foreign_gov_acc,
                domestic_purchased_foreign_security_gov_acc,
                direct_invest_in_foreign_country_gov_acc,
                commodity_gov_acc,
                imf_reserve_gov_acc,
            ]
        )
        cb_acc = ComplexAccount(
            account_date,
            'government account',
            [
                foreign_owned_in_domestic_deposit_cb_acc,
                domestic_loan_to_foreign_cb_acc,
                domestic_purchased_foreign_security_cb_acc,
                direct_invest_in_foreign_country_cb_acc,
                commodity_cb_acc,
                currency_swap_cb_acc,
            ]
        )
        domestic_ownership_acc = ComplexAccount(
            account_date,
            'domestic ownership account',
            [
                domestic_private_acc,
                government_acc,
                cb_acc,
            ],
            'EUR',
        )
        currency_net_shipment_acc = ComplexAccount(
            account_date,
            'currency net shipment acc account',
            [
                foreign_gov_shipment_acc,
                foreign_cb_shipment_acc,
            ],
            'EUR',
        )
        foreign_official_acc = ComplexAccount(
            account_date,
            'foreign official account',
            [
                currency_net_shipment_acc,
                domestic_owned_in_foreign_deposit_fo_acc,
                foreign_loan_to_domestic_fo_acc,
                foreign_purchased_domestic_obligation_fo_acc,
                foreign_purchased_domestic_security_fo_acc,
                direct_invest_in_domestic_country_fo_acc,
                debt_owed_to_foreign_country_fo_acc,
                hard_asset_fo_acc,
                nation_currency_fo_acc,
            ],
            'EUR',
        )
        foreign_private_acc = ComplexAccount(
            account_date,
            'foreign official account',
            [
                domestic_owned_in_foreign_deposit_fp_acc,
                foreign_loan_to_domestic_fp_acc,
                foreign_purchased_domestic_obligation_fp_acc,
                foreign_purchased_domestic_security_fp_acc,
                direct_invest_in_domestic_country_fp_acc,
                debt_owed_to_foreign_country_fp_acc,
                hard_asset_fp_acc,
                nation_currency_fp_acc,
            ],
            'EUR',
        )
        foreign_ownership_acc = ComplexAccount(
            account_date,
            'foreign ownership account',
            [
                foreign_official_acc,
                foreign_private_acc,
            ],
            'EUR',
        )
        financial_acc = ComplexAccount(
            account_date,
            'financial account',
            [
                domestic_ownership_acc,
                foreign_ownership_acc,
            ],
            ACCOUNT_CCY,
        )
        # state perturbation stops the collaboration with IMF from 2003 to 2008
        if year >= 2003:
            is_deleted = financial_acc.delete_account(
                'imf reserve government account'
            )
            assert is_deleted
            if is_deleted:
                print("imf reserve government account is deleted...")
            assert financial_acc.delete_account(
                'imf reserve government account'
            ) is False
        if year > 2008:
            is_added = government_acc.add_account(
                imf_reserve_gov_acc
            )
            assert is_added
            if is_added:
                print(f"{imf_reserve_gov_acc.name} is added...")
            assert government_acc.add_account(
                imf_reserve_gov_acc
            ) is False
        financial_acc_balances[account_date] = financial_acc.amount
        print("-----------------------------------------------------------")
    # references
    balances = {date(2001, 12, 31): 35000000000.0,
                date(2002, 12, 31): 35035000000.0,
                date(2003, 12, 31): 34068000000.0,
                date(2004, 12, 31): 34102000000.0,
                date(2005, 12, 31): 34136000000.0,
                date(2006, 12, 31): 34170000000.0,
                date(2007, 12, 31): 34204000000.0,
                date(2008, 12, 31): 34238000000.0,
                date(2009, 12, 31): 35280000000.0,
                date(2010, 12, 31): 35315000000.0}
    assert financial_acc_balances == balances
    plt.plot([ad.strftime('%Y') for ad in financial_acc_balances.keys()]


Les tests unitaires confirment les résultats attendus. Les comptes ne peuvent être ajoutés en double, ni supprimés s'ils sont inexistants.

Voici les résultats affichés dans la console, avec les rapports sauvegardés en image au format PNG.