Sébastien Putier

Another tech blog, but this one is mine.

Sébastien

5 minutes

Le design pattern Adapter est un pattern structurel qui a pour but de permettre à des systèmes incompatibles de fonctionner ensemble. Cet article montre comment l’utiliser pour créer un système cohérent à partir de 2 composants indépendants.

Note

Cet article fait partie d'une série sur les Design Patterns. Vous trouverez la liste des articles publiés ici.

Le problème

Notre application génère des factures à partir de données stockées quelque part dans une base de données. Elle supporte aujourd’hui les formats .docx (pour l’impression et l’archivage) ainsi que HTML (pour la prévisualisation). Aujourd’hui un manager est venu nous voir en demandant l’intégration du support des PDF, sous prétexte que certains de nos clients veulent recevoir des factures électroniques au lieu de factures papiers. Et bien sur, il ‘voudrait” envoyer les factures en question au plus vite. Par conséquent, pas le temps de faire les choses par nous-mêmes : nous avons besoin d’un composant tiers. Celui que nous avons trouvé n’est pas très cher, convient parfaitement à ce que l’on veut générer, et le vendeur fournit un support de bonne qualité. Super !

Oh… On a quand même un problème : l’interface publique du VendorPdfWriter ne colle pas avec notre code source existant. Nos générateurs de documents, HtmlInvoiceDocumentGenerator et DocxInvoiceDocumentGenerator, implémentent tous deux l’interface IInvoiceDocumentGenerator.

public interface IInvoiceDocumentGenerator
{
    void WriteHeader(string invoiceId);
    void writeOurCompanyInfo(CompanyInfo companyInfo);
    void writeCustomerInfo(CompanyInfo customerInfo);
    void writeInvoiceRow(InvoiceRow invoiceRowData);
    void writeTotalWithTaxes(decimal total);

    void SaveToFile(string fileName);
}

public readonly struct InvoiceRow
{
    public string ProductName { get; }
    public int Quantity { get; }
    public decimal UnitPrice { get; }
    public decimal SalesTaxesPercent { get; }
    public decimal UnitPriceWithTaxes => UnitPrice * (100 + SalesTaxesPercent) / 100;
    public decimal TotalPriceWithTaxes => UnitPriceWithTaxes * Quantity;
}

public readonly struct CompanyInfo
{
    public string Name { get; }
    public string AddressLine1 { get; }
    public string AddressLine2 { get; }
    public string ZipCode { get; }
    public string City { get; }
    public string Country { get; }
}

De l’autre côté, le composant tiers VendorPdfWriter expose des fonctions de plus bas niveau :

public class VendorPdfWriter
{
    public void SetPosition(float xInCm, float yInCm) { /* ... */ }
    public void WriteString(string text, int fontSize = 14) { /* ... */ }
    public void WriteHorizontalLine(decimal lengthInCm) { /* ... */ }

    public void Save(string fileName) { /* ... */ }
    
    /* ... Other methods not relevant to the current article */
}

Intégrer le composant avec le pattern Adapter

Tout d’abord, commençons par essayer de définir correctement le pattern Adapter : il aagit comme un traducteur entre le code et un composant externe. Ce composant peut être une classe “legacy” (historique en bon français) qu’on ne veut/peut pas changer ou un composant tiers. Soit dit en passant, il est possible que vous ayiez déjà utilisé ce pattern qui est parfois décrit sous le nom “wrapper”.

Note - Sujette à débat

Les wrappers et les adapters sont très similaires dans leur implémentation. Leur plus grosse différence réside dans le problème qu'ils permettent de résoudre. Si l'intention est de simplifier l'utilisation d'une type en exposant des fonctions de plus haut niveau, on a affaire à un wrapper. S'il est question de connecter des systèmes qui exposent des interfaces incompatibles, on est face à un Adapter. Parfois, on utilise les deux à la fois (et il semblerait que ce soit ce que l'on s'apprête à faire).

Et pour notre exemple, qu’est ce que ça signifie ? L’Adapter que nous allons écrire implémentera l’interface IInvoiceDocumentGenerator, et il utilisera le type VendorPdfWriter en interne pour faire le job.

public class VendorPdfWriterAdapter : IInvoiceDocumentGenerator
{
    private readonly VendorPdfWriter _pdfWriter;
    private int _rowNumber = 0;

    public VendorPdfWriterAdapter()
        => _pdfWriter = new VendorPdfWriter();

    public void WriteHeader(string invoiceId)
    {
        _pdfWriter.SetPosition(2.0f, 2.0f);
        _pdfWriter.WriteString($"Invoice {invoiceId}", 18);
    }

    public void writeOurCompanyInfo(CompanyInfo companyInfo)
    {
        _pdfWriter.SetPosition(2.0f, 4.0f);
        WriteCompanyInfo(companyInfo);
    }

    public void writeCustomerInfo(CompanyInfo customerInfo)
    {
        _pdfWriter.SetPosition(12.0f, 4.0f);
        WriteCompanyInfo(customerInfo);
    }

    private void WriteCompanyInfo(CompanyInfo info)
    {
        var sb = new StringBuilder();

        sb.Append(info.Name);
        sb.Append(info.AddressLine1);
        if (info.AddressLine2 != null) sb.Append(info.AddressLine2);
        sb.Append($"{info.ZipCode} {info.City}");
        sb.Append(info.Country);

        _pdfWriter.WriteString(sb.ToString());
    }

    public void writeInvoiceRow(InvoiceRow invoiceRowData)
    {
        var yRow = 10.0f + (_rowNumber * 1.0f);

        _pdfWriter.SetPosition(2.0f, yRow);
        _pdfWriter.WriteString(invoiceRowData.ProductName);
        _pdfWriter.SetPosition(10.0f, yRow);
        _pdfWriter.WriteString(invoiceRowData.UnitPrice.ToString());
        _pdfWriter.SetPosition(12.0f, yRow);
        _pdfWriter.WriteString(invoiceRowData.Quantity.ToString());
        _pdfWriter.SetPosition(13.0f, yRow);
        _pdfWriter.WriteString(invoiceRowData.SalesTaxesPercent + "%");
        _pdfWriter.SetPosition(15.0f, yRow);
        _pdfWriter.WriteString(invoiceRowData.TotalPriceWithTaxes.ToString());

        _pdfWriter.SetPosition(2.0f, yRow + 0.9f);
        _pdfWriter.WriteHorizontalLine(17.0m);
    }

    public void writeTotalWithTaxes(decimal total)
    {
        var yRow = 10.0f + (_rowNumber * 1.0f) + 2.0f;

        _pdfWriter.SetPosition(12.0f, yRow);
        _pdfWriter.WriteString("TOTAL :");
        _pdfWriter.SetPosition(15.0f, yRow);
        _pdfWriter.WriteString(total.ToString());
    }

    public void SaveToFile(string fileName)
        => _pdfWriter.Save(fileName);
}

Le type VendorPdfWriter est maintenant masqué derrière son adapter, que nous pouvons intégrer dans le workflow déjà utilisé pour les autres générateur. En plus de ça, comme le composant tiers n’est jamais exposé à l’extérieur de VendorPdfWriterAdapter, les changements fonctionnels potentiels pourront amener sans problème à modifier des éléments techniques (le composant tiers, par exemple) sans que l’application ne subisse d’impact (sauf évidemment au niveau de l’adapter)..

Conclusion

Le pattern Adapter est un moyen simple et efficace pour connecter des systèmes indépendants tout en gardant le code propre et maintenable. Ci-dessous, une petite liste des béfices qu’und éveloppeur eput tirer d’une utilisation consciente du pattern :

  • isolation du composant externe en maquant son utilisant au sin de l’adapter
  • maintainabilité en plaçant toute la complexité dans l’adapter
  • extensibilité en utilisant une interface commune pour les composants internes et externes.
comments powered by Disqus

A propos

• French dev
• Mainly .NET
• Trying to learn everything

Posts récents

Catégories