Sébastien Putier

Another tech blog, but this one is mine.

Sébastien

4 minutes read

The Adapter design pattern is a structural pattern aimed to allow incompatible systems to work together. In this post, I’ll show how to use it to build a consistent system from 2 independent parts.

Note

This post is part of the Design Patterns series. You will find links to the published posts here.

The problem

Our application generates invoices from data stored somewhere in a database. The current document formats we support are .docx (for printing and archiving) and HTML (for previews). Today, a manager came to us asking for PDF format support, as some of our customers want to get electronic invoices instead of paper invoices. And of course, he “would like” to send the related invoices as soon as possible. Consequently, we don’t have much time to do things by ourselves : we need to use a third-party component. The one we found is cheap, perfectly fits our functional needs and is well supported by the vendor. Great !

Oh… we still have a problem : the public interface of the VendorPdfWriter doesn’t match with our existing codebase. Our generators, HtmlInvoiceDocumentGenerator and DocxInvoiceDocumentGenerator, both implement the IInvoiceDocumentGenerator interface.

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; }
}

On the other side, the third-party VendorPdfWriter component exposes lower-level functions :

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 */
}

Integrate the component using the Adapter pattern

First, let’s try to define this pattern appropriately : the Adapter pattern acts as a translator between your own code and a component you work with. This component may be a legacy class you don’t want to (or can’t) change or a third-party component. Also, you may already have used this pattern, as it is sometimes used under the name “wrapper”.

Note - Subject to debate

Wrappers and adapters seem very similar at the implementation level. Their biggest difference lies in the problem being solved. If your intention is to simplify the use of a class by exposing higher-level functions, it seems you're using a wrapper. If you want to connect systems which expose incompatible interfaces, you're using an Adapter. Sometimes, you're using both at the same time (and it seems that's what we're going to do).

What does it means for our example ? The Adapter we’re about to write will implement the IInvoiceDocumentGenerator interface, and it will use the VendorPdfWriter type under the hood to get things done.

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);
}

The VendorPdfWriter type is now hidden behind its adapter, which we can fully integrate into the workflow we already use for the other generators. Moreover, as the third-party component never leaks out of VendorPdfWriterAdapter, any change to the business requirements can safely lead to technical changes without any impact on the application (of course, except the adapter).

Conclusion

The Adapter pattern is a simple and effective way to connect independent systems while keeping the code clean and safe. Here is a list of benefits one can get knowingly using this pattern :

  • external component isolation by hiding the component use only inside the adapter
  • maintainability by defining all of the complexity into the adapter
  • extensibility by using a common interface for external and internal components
comments powered by Disqus

About

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

Recent posts

Categories