3
\$\begingroup\$

I'm testing to use a fluent builder pattern for generating a pdf file using AbcPDF. I've looked at several methods of accomplishing this and I'd be very glad to get some input on what I've written so far to see if I'm going in the right direction or if there's some criticism.

The idea is that I have a builder for each component in the pdf:

  • Footer builder
  • Page numbers builder
  • Index builder
  • Project information builder
  • Customer information builder
  • Document content (the main text) builder

So for example I have an "umbrella" builder for generating a customer document that can contain some or all parts in that list. And then another "umbrella" builder for generating another type of document but contains some parts of the list.

The main idea is to use the builders in the list for generating different types of documents. For example a sales report may contain some main content, footer and page numbers, whilst a customer document also needs customer information and maybe project information.

I've looked at some that use inheritance with recursive generics, and fatected builders but in those examples my "component builders" (ref list above) would be coupled to the sales report builder or customer document builder. At least in those examples I've read(?).

But yeah, the general idea is to have small builders not coupled with anything that can be reused by bigger builders that expose a set of the small builders that are appropriate to have in that context. Or is there a pattern better suited for this goal? Thanks :) And yeah, I don't know if the interfaces are neccessary, but I guess I kept them there to hear about what you guys thought about it.

And the Doc variable is from the ABC pdf nuget package. It's the variable I write everything to. Documentation

public class Main
{
    public void Run()
    {
        CustomerDocumentBuilder.Create()
            .AddCustomerInformation(new CustomerInformation { Name = "customer" })
            .AddProjectInformation(new ProjectInformation { Name = "Project" })
            .AddDocument(new DocumentInformation { Name = "Document text" })
            .AddPageNumbers()
            .AddFooter("Footer text")
            .Build();

    }
}
public class CustomerDocumentBuilder
{
    private readonly Doc _document;

    private CustomerDocumentBuilder()
    {
        _document = new();
    }

    public static CustomerDocumentBuilder Create() => new();

    public void Build()
    {
        _document.Save("C:\\pdf\\textflow.pdf");
    }

    public CustomerDocumentBuilder AddDocument(
        DocumentInformation documentInfo)
    {
        new DocumentBuilder(_document).AddDocument(documentInfo);
        return this;
    }

    public CustomerDocumentBuilder AddCustomerInformation(
        CustomerInformation customerInfo)
    {
        new CustomerBuilder(_document).AddCustomerInformation(customerInfo);
        return this;
    }

    public CustomerDocumentBuilder AddProjectInformation(
        ProjectInformation projectInfo)
    {
        new ProjectInfoBuilder(_document).AddProjectInformation(projectInfo);
        return this;
    }
    public CustomerDocumentBuilder AddFooter(
        string footerText)
    {
        new FooterBuilder(_document).AddFooter(footerText);
        return this;
    }       
    public CustomerDocumentBuilder AddPageNumbers()
    {
        new PageNumbersBuilder(_document).AddPageNumbers();
        return this;
    }
}

public class SalesReportBuilder
{
    private readonly Doc _document;

    private SalesReportBuilder()
    {
        _document = new();
    }

    public static SalesReportBuilder Create() => new();

    public void Build()
    {
        _document.Save("C:\\pdf\\textflow.pdf");
    }

    public SalesReportBuilder AddDocument(
        DocumentInformation documentInfo)
    {
        new DocumentBuilder(_document).AddDocument(documentInfo);
        return this;
    }

    public SalesReportBuilder AddFooter(
        string footerText)
    {
        new FooterBuilder(_document).AddFooter(footerText);
        return this;
    }

    public SalesReportBuilder AddPageNumbers(
        DocumentInformation documentInfo)
    {
        new PageNumbersBuilder(_document).AddPageNumbers();
        return this;
    }

}

public class CustomerBuilder : ICustomerBuilder
{
    private readonly Doc _document;

    public CustomerBuilder(
        Doc document)
    {
        _document = document;
    }

    public ICustomerBuilder AddCustomerInformation(
        CustomerInformation customerInfo)
    {
        _document.AddTextStyled(customerInfo.Name);
        // etc...
        return this;
    }
}

public class DocumentBuilder : IDocumentBuilder
{
    private readonly Doc _document;

    public DocumentBuilder(
        Doc document)
    {
        _document = document;
    }

    public IDocumentBuilder AddDocument(
        DocumentInformation documentInfo)
    {
        _document.AddTextStyled(documentInfo.Name);
        // etc...
        return this;
    }
}

public class ProjectInfoBuilder : IProjectInfoBuilder
{
    private readonly Doc _document;

    public ProjectInfoBuilder(
        Doc document)
    {
        _document = document;
    }

    public IProjectInfoBuilder AddProjectInformation(
        ProjectInformation projectInfo)
    {
        _document.AddTextStyled(projectInfo.Name);
        // etc...
        return this;
    }
}

public class FooterBuilder : IFooterBuilder
{
    private readonly Doc _document;

    public FooterBuilder(
        Doc document)
    {
        _document = document;
    }

    public IFooterBuilder AddFooter(
        string footerText)
    {
        _document.AddTextStyled(footerText);
        // etc...
        return this;
    }
}

public class PageNumbersBuilder : IPageNumbersBuilder
{
    private readonly Doc _document;

    public PageNumbersBuilder(
        Doc document)
    {
        _document = document;
    }

    public IPageNumbersBuilder AddPageNumbers()
    {
        //calculcate pages and insert
        // page number to each pages
        return this;
    }
}

public interface IDocumentBuilder
{
    IDocumentBuilder AddDocument(DocumentInformation documentInfo);
}
public interface ICustomerBuilder
{
    ICustomerBuilder AddCustomerInformation(CustomerInformation customerInfo);
}
public interface IProjectInfoBuilder
{
    IProjectInfoBuilder AddProjectInformation(ProjectInformation projectInfo);
}
public interface IFooterBuilder
{
    IFooterBuilder AddFooter(string footerText);
}
public interface IPageNumbersBuilder
{
    IPageNumbersBuilder AddPageNumbers();
}

public class CustomerInformation
{
    public string Name { get; set; }

}
public class ProjectInformation
{
    public string Name { get; set; }

}
public class DocumentInformation
{
    public string Name { get; set; }

}

Updated example:

    public interface ICustomerTextDocumentBuilder
{
    Doc Build();

    CustomerTextDocumentBuilder WithTextDocument(
        DocumentInformation documentInfo);

    CustomerTextDocumentBuilder WithCustomerInformation(
        CustomerInformation customerInfo);

    CustomerTextDocumentBuilder WithProjectInformation(
        ProjectInformation projectInfo);

    CustomerTextDocumentBuilder WithFooter(
        FooterInformation footerInfo);

    CustomerTextDocumentBuilder WithPageNumbers();
}

public class CustomerTextDocumentBuilder : ICustomerTextDocumentBuilder
{
    private readonly Doc _document;

    private CustomerTextDocumentBuilder()
    {
        _document = new();
    }

    // private footerBuilder;
    public static CustomerTextDocumentBuilder Create() => new();

    public Doc Build()
    {
        return _document;
    }

    public CustomerTextDocumentBuilder WithTextDocument(
        DocumentInformation documentInfo)
    {
        new TextDocumentPdfWriter(_document).AddDocument(documentInfo);
        return this;
    }

    public CustomerTextDocumentBuilder WithCustomerInformation(
        CustomerInformation customerInfo)
    {
        new CustomerInfoPdfWriter(_document).AddCustomerInformation(customerInfo);
        return this;
    }

    public CustomerTextDocumentBuilder WithProjectInformation(
        ProjectInformation projectInfo)
    {
        new ProjectInfoPdfWriter(_document).AddProjectInformation(projectInfo);
        return this;
    }

    public CustomerTextDocumentBuilder WithFooter(
        FooterInformation footerInfo)
    {
        new FooterPdfWriter(_document).AddFooter(footerInfo);
        return this;
    }
    public CustomerTextDocumentBuilder WithPageNumbers()
    {
        new PageNumbersPdfWriter(_document).AddPageNumbers();
        return this;
    }

}

public class CustomerInfoPdfWriter : PdfWriter
{
    private readonly Doc _document;

    public CustomerInfoPdfWriter(
        Doc document)
    {
        _document = document;
    }

    public void AddCustomerInformation(
        CustomerInformation customerInfo)
    {
        // write to doc...
    }
}

public class TextDocumentPdfWriter : PdfWriter
{
    private readonly Doc _document;

    public TextDocumentPdfWriter(
        Doc document)
    {
        _document = document;
    }

    public void AddDocument(
        DocumentInformation documentInfo)
    {
        _document.Width = 4;
        _document.FontSize = 32;
        _document.TextStyle.Justification = 1;
        _document.Rect.String = "100 200 500 600";

        _document.FrameRect();
        var theId = _document.AddTextStyled(documentInfo.Name);

        while (_document.Chainable(theId))
        {
            _document.Page = _document.AddPage();
            _document.FrameRect();
            theId = _document.AddTextStyled("", theId);
        }

    }
}

public class ProjectInfoPdfWriter : PdfWriter
{
    private readonly Doc _document;

    public ProjectInfoPdfWriter(
        Doc document)
    {
        _document = document;
    }

    public void AddProjectInformation(
        ProjectInformation projectInfo)
    {
        _document.Width = 2;
        _document.FontSize = 20;
        _document.TextStyle.Justification = 5;
        _document.Rect.String = "100 650 500 750";

        _document.FrameRect();
        _document.AddText(projectInfo.Name);
    }
}

public class FooterPdfWriter : PdfWriter
{
    private readonly Doc _document;

    public FooterPdfWriter(
        Doc document)
    {
        _document = document;
    }

    private void AddFooterOnPage(FooterInformation footerInfo)
    {
        var top = PDF_Document_page_footer_top;

        _document.TextStyle.HPos = PDF_Text_Align_Left;
        _document.Color.String = "0 0 0";
        _document.FontSize = 7;
        _document.TextStyle.Font = _document.AddFont(PDF_Fonts_Regular);

        const int topMargin = 4;

        SetRect(_document, top - topMargin, 10, 28 + 12, PDF_Document_width - 28);
        _document.AddText(footerInfo.OrganizationName);

        _document.Rect.Top = top - topMargin - 10;
        _document.AddText("Organization number" + " " + footerInfo.OrganizationNumber);

        SetRect(_document,
            top - 4,
            top - 11,
            PDF_Document_Margin,
            PDF_Document_width - PDF_Document_Margin);

        _document.Color.String = PDF_Color_Grey;
        _document.TextStyle.HPos = PDF_Text_Align_Center;
        _document.AddText($"Document generated at {DateTime.UtcNow:dd.MM.yyyy}");
    }

    // Adds footer on all pages
    public void AddFooter(
        FooterInformation footerInfo)
    {
        var theCount = _document.PageCount;

        for (int i = 1; i <= theCount; i++)
        {
            _document.PageNumber = i;
            AddFooterOnPage(footerInfo);
        }
    }
}

public class PageNumbersPdfWriter : PdfWriter
{
    private readonly Doc _document;

    public PageNumbersPdfWriter(
        Doc document)
    {
        _document = document;
    }

    public void AddPageNumbers()
    {
        //calculcate pages and insert
        // page number to each pages
    }
}


public abstract class PdfWriter
{
    protected const string PDF_Fonts_Regular = "Helvetica";
    protected const int PDF_Document_width = 595;
    protected const int PDF_Document_page_footer_top = 42;
    protected const int PDF_Document_Margin = 28;

    protected const string PDF_Color_Grey = "120 144 156";

    protected const double PDF_Text_Align_Left = 0;
    protected const double PDF_Text_Align_Center = 0.5;


    protected static void SetRect(Doc theDoc, double top, double bottom, double left, double right)
    {
        theDoc.Rect.Top = top;
        theDoc.Rect.Bottom = bottom;
        theDoc.Rect.Left = left;
        theDoc.Rect.Right = right;
    }
}


public class FooterInformation
{
    public string OrganizationNumber { get; set; }
    public string OrganizationName { get; set; }
}


public class CustomerInformation
{
    public string Name { get; set; }

}
public class ProjectInformation
{
    public string Name { get; set; }

}
public class DocumentInformation
{
    public string Name { get; set; }

}
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Welcome to Code Review! Could you please share with the following class definitions as well: DocumentInformation, CustomerInformation, ProjectInformation and Doc? \$\endgroup\$ Nov 19, 2021 at 7:54
  • 1
    \$\begingroup\$ Of course! I added them now. They're just very simple dto's currently. But the Doc variable is something that is found the ABCPdf nuget package. I added a reference to the documentation for AbcPDF. But I could have used any other pdf library, since it's the pattern around I'm more concerned about. \$\endgroup\$ Nov 19, 2021 at 10:04
  • \$\begingroup\$ Its not fluent enough. You should replace the class instanceiation like new CustomerInformation \$\endgroup\$
    – Anders
    Nov 19, 2021 at 12:04
  • \$\begingroup\$ @Anders I get your point. In production they wouldn't be instantianted in the function call, but passed in as a parameter for the class containing the builder. All the data would be dto's generated mapped and retrieved from the database. Thoughts on that? \$\endgroup\$ Nov 19, 2021 at 13:35

1 Answer 1

3
\$\begingroup\$

Maybe I misunderstand your intent but I can see lots of boilerplate code.

Builder vs Fluent

There is a common misunderstanding that these concepts are the same. Let's see what wikipedia says about them

The builder pattern is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The intent of the Builder design pattern is to separate the construction of a complex object from its representation.

Source

In software engineering, a fluent interface is an object-oriented API whose design relies extensively on method chaining. Its goal is to increase code legibility by creating a domain-specific language (DSL).

Source

Further readings: 1, 2, 3, 4


Builder

As its name suggests it construct something. In your case it persist something to the disk.

So, from a consumer perspective it should look more like this:

var docBuilder = new CustomerDocumentBuilder();
var doc = docBuilder
    .WithCustomerInformation("customer")
    .WithProjectInformation("Project")
    .WithDocument("Document text")
    .WithPageNumbers()
    .WithFooter("Footer text")
    .Build();

doc.Save("C:\\pdf\\textflow.pdf");

If the usage looks like this, then the implementation requires only two classes

CustomerDocument

public class CustomerDocument
{
    private readonly Doc _doc;

    public CustomerDocument(Doc doc) => _doc = doc;
        
    public void Save(string path) => _doc.Save(path);
}

CustomerDocumentBuilder

public class CustomerDocumentBuilder
{
    private readonly Doc _document = new ();

    public CustomerDocument Build() => new (_document);

    public CustomerDocumentBuilder WithDocument(string documentInfo)
    {
        _document.AddTextStyled(documentInfo);
        return this;
    }

    public CustomerDocumentBuilder WithCustomerInformation(string customerInfo)
    {
        _document.AddTextStyled(customerInfo);
        return this;
    }

    public CustomerDocumentBuilder WithProjectInformation(string projectInfo)
    {
        _document.AddTextStyled(projectInfo);
        return this;
    }

    public CustomerDocumentBuilder WithFooter(string footerText)
    {
        _document.AddTextStyled(footerText);
        return this;
    }

    public CustomerDocumentBuilder WithPageNumbers()
    {
        //calculcate pages and insert
        // page number to each pages
        return this;
    }
}

UPDATE #1: review of the updated code

ICustomerTextDocumentBuilder

This interface is tightly coupled to the implementation

  • Either change the return type from CustomerTextDocumentBuilder to ICustomerTextDocumentBuilder
  • Or get rid of the whole interface

CustomerDocumentBuilder

  • If you really want to have this "facade"/wrapper class then you do NOT need to create a new XYZPdfWriter every time when a WithXYZ is called
  • You can pass it as a method argument rather than a constructor parameter
public class CustomerTextDocumentBuilder
{
    private readonly Doc _document = new();
    private readonly TextDocumentPdfWriter _textDocumentWriter = new();
    private readonly CustomerInfoPdfWriter _customerInfoWriter = new();
    private readonly ProjectInfoPdfWriter _projectInfoWriter = new();
    private readonly FooterPdfWriter _footerWriter = new();
    private readonly PageNumbersPdfWriter _pageNumbersWriter = new();

    public Doc Build() => _document;

    public CustomerTextDocumentBuilder WithTextDocument(
        DocumentInformation documentInfo)
    {
        _textDocumentWriter.AddDocument(_document, documentInfo);
        return this;
    }

    public CustomerTextDocumentBuilder WithCustomerInformation(
        CustomerInformation customerInfo)
    {
        _customerInfoWriter.AddCustomerInformation(_document, customerInfo);
        return this;
    }
    ...
}
internal class TextDocumentPdfWriter
{
    public void AddDocument(Doc document, DocumentInformation documentInfo)
    {
        document.Width = 4;
        document.FontSize = 32;
        document.TextStyle.Justification = 1;
        document.Rect.String = "100 200 500 600";

        document.FrameRect();
        var theId = document.AddTextStyled(documentInfo.Name);

        while (document.Chainable(theId))
        {
            document.Page = document.AddPage();
            document.FrameRect();
            theId = document.AddTextStyled("", theId);
        }
    }
}
...
  • I've changed each XYZPdfWriter class visibility from public to internal, since you have a facade in a front of them, so you don't need to expose them direclty

PdfWriter

  • This is not really a base class rather a constant class
    • So treat it like that
internal static class PdfWriterConstants
{
    public const string PDF_Fonts_Regular = "Helvetica";
    public const int PDF_Document_width = 595;
    public const int PDF_Document_page_footer_top = 42;
    public const int PDF_Document_Margin = 28;
        
    public const string PDF_Color_Grey = "120 144 156";

    public const double PDF_Text_Align_Left = 0;
    public const double PDF_Text_Align_Center = 0.5;

    public static void SetRect(Doc theDoc, double top, double bottom, double left, double right)
    {
        theDoc.Rect.Top = top;
        theDoc.Rect.Bottom = bottom;
        theDoc.Rect.Left = left;
        theDoc.Rect.Right = right;
    }
}
  • I've changed class's access modifier from public abstract to an internal static
  • I've also changed protected keywords to public
  • If you use using static like this:
using static YourNamespace.PdfWriterConstants;
  • then you don't have to prefix each and every member access with the PdfWriterConstants. prefix
  • Also none of the XYZPdfWriter has to be inherited from anything
\$\endgroup\$
3
  • \$\begingroup\$ I updated my example with a more extensive example. I get some of the points you are making. I think that the function AddTextStyled was a bit misleading, since the write functions for each component will in reality be much larger and complex than just calling that one function. I wrote the footer function to exemplify that, and added some more to some of the other functions. What do you think of the updated version? \$\endgroup\$ Nov 19, 2021 at 12:47
  • 1
    \$\begingroup\$ @user1784297 I've updated my answer please check it. \$\endgroup\$ Nov 19, 2021 at 13:52
  • 1
    \$\begingroup\$ This looks just perfect. Really appreciate you taking the time to help me revise it. Cheers! \$\endgroup\$ Nov 19, 2021 at 16:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.