Consider two builder functions: one is used to build a shopping receipt, which has a name, date and list of items; a second is used to build each shopping item within a receipt. A simple implementation is as follows:
function Receipt(builder) {
return {
Name: builder.Name,
Date: builder.Date,
Items: builder.Items
};
}
function ReceiptBuilder() {
function WithName(name) {
this.Name = name;
return this;
}
function WithDate(dt) {
this.Date = dt;
return this;
}
function WithItem(item) {
this.Items = this.Items || [];
this.Items.push(item);
return this;
}
function build() {
return Receipt(this);
}
return {
WithName,
WithDate,
WithItem,
build
}
}
function Item(builder) {
return {
Type: builder.Type,
Price: builder.Price
};
}
function ItemBuilder() {
function WithType(type) {
this.Type = type;
return this;
}
function WithPrice(Price) {
this.Price = Price;
return this;
}
function build() {
return Item(this);
}
return {
WithPrice,
WithType,
build
}
}
We can then build a receipt as follows:
let receipt = ReceiptBuilder()
.WithName('Shopping')
.WithDate('01-01-01')
.WithItem(ItemBuilder().
WithPrice(1.99).
WithType('eggs').build())
.build()
Is it possible to refactor to allow the second builder to "collapse" into the first somehow, to achieve the following (or something similar) construction call? I.e. it would be nice to "hide" the ItemBuilder somehow.
ReceiptBuilder()
.WithName('Shopping')
.WithDate('01-01-01')
.WithItem()
.WithPrice(1.99)
.WithType('eggs')
.build()
With minimal changes, the WithItem()
method can start up an ItemBuilder and chain from it. There needs to be some signifier that the item is finished, which will chain back into the ReceiptBuilder. Here this is a method called endBuildingItem()
:
function WithItem() {
this.Items = this.Items || [];
const receiptBuilder = this;
return Object.assign(ItemBuilder(), {
endBuildingItem() {
receiptBuilder.Items.push(this.build());
return receiptBuilder;
}
});
}
const receipt1 = ReceiptBuilder()
.WithName('Shopping')
.WithDate('01-01-01')
.WithItem()
.WithPrice(1.99)
.WithType('eggs')
.endBuildingItem()
.build();
console.log(receipt1);
const receipt2 = ReceiptBuilder()
.WithName('Shopping')
.WithDate('01-01-01')
.WithItem()
.WithPrice(2.99)
.WithType('milk')
.endBuildingItem()
.WithItem()
.WithPrice(3.99)
.WithType('bread')
.endBuildingItem()
.build();
console.log(receipt2);
function Receipt(builder) {
return {
Name: builder.Name,
Date: builder.Date,
Items: builder.Items
};
}
function ReceiptBuilder() {
function WithName(name) {
this.Name = name;
return this;
}
function WithDate(dt) {
this.Date = dt;
return this;
}
function WithItem() {
this.Items = this.Items || [];
const receiptBuilder = this;
return Object.assign(ItemBuilder(), {
endBuildingItem() {
receiptBuilder.Items.push(this.build());
return receiptBuilder;
}
});
}
function build() {
return Receipt(this);
}
return {
WithName,
WithDate,
WithItem,
build
}
}
function Item(builder) {
return {
Type: builder.Type,
Price: builder.Price
};
}
function ItemBuilder() {
function WithType(type) {
this.Type = type;
return this;
}
function WithPrice(Price) {
this.Price = Price;
return this;
}
function build() {
return Item(this);
}
return {
WithPrice,
WithType,
build
}
}
.as-console-wrapper { max-height: 100% !important; }
An alternative implementation would be to put the endBuildingItem()
method in ItemBuilder and expect an instance of ReceiptBuilder to be passed into it:
const receipt1 = ReceiptBuilder()
.WithName('Shopping')
.WithDate('01-01-01')
.WithItem()
.WithPrice(1.99)
.WithType('eggs')
.endBuildingItem()
.build();
console.log(receipt1);
const receipt2 = ReceiptBuilder()
.WithName('Shopping')
.WithDate('01-01-01')
.WithItem()
.WithPrice(2.99)
.WithType('milk')
.endBuildingItem()
.WithItem()
.WithPrice(3.99)
.WithType('bread')
.endBuildingItem()
.build();
console.log(receipt2);
function Receipt(builder) {
return {
Name: builder.Name,
Date: builder.Date,
Items: builder.Items
};
}
function ReceiptBuilder() {
function WithName(name) {
this.Name = name;
return this;
}
function WithDate(dt) {
this.Date = dt;
return this;
}
function WithItem() {
this.Items = this.Items || [];
return ItemBuilder(this);
}
function build() {
return Receipt(this);
}
return {
WithName,
WithDate,
WithItem,
build
}
}
function Item(builder) {
return {
Type: builder.Type,
Price: builder.Price
};
}
function ItemBuilder(receiptBuilder) {
function WithType(type) {
this.Type = type;
return this;
}
function WithPrice(Price) {
this.Price = Price;
return this;
}
function build() {
return Item(this);
}
function endBuildingItem() {
receiptBuilder.Items.push(this.build());
return receiptBuilder;
}
return {
WithPrice,
WithType,
build,
endBuildingItem,
}
}
.as-console-wrapper { max-height: 100% !important; }