Problem
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; }
}
Solution
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.
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).
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
toICustomerTextDocumentBuilder
- 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 aWithXYZ
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 frompublic
tointernal
, 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 aninternal static
- I’ve also changed
protected
keywords topublic
- 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