Skip to content

Commit 8f87fbc

Browse files
committed
Iteration6: Refactor magic strings and printer patterns
Fix small typo in test
1 parent ed72983 commit 8f87fbc

File tree

5 files changed

+68
-24
lines changed

5 files changed

+68
-24
lines changed

test-pnpm/shopping-cart-kata/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,7 @@ You could start Outside-In **only** if:
199199
- You're willing to mock deeper domain types (`Product`, `CartItem`, etc.)
200200

201201
But in this kata, that’s harder to justify because the logic in the "leaf" types is richer and more test-worthy than in the orchestration layer.
202+
203+
### Summary
204+
205+
Thnanks Emmanuael Valverde, for your cool takle on this [kata](https://www.codurance.com/katas/shopping-cart-kata). There were a few sneaky catches with the pricing. I have done this kata before using simple types and I found myself producing more code this time. I also did outside in, before, mocking and faking bits until I had the final design. I think breaking this up into the seperate phases made it easier to get my head around each concept, so I am glad I did it this way. The thing didn't like is it forced me into a solution by virtue of what I simplified. It may be a good or bad thing, no idea.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
export const TABLE_CONSTANTS = {
3+
LINE_WIDTH: 44,
4+
COLUMN_WIDTHS: {
5+
NAME: 12,
6+
PRICE: 14,
7+
QUANTITY: 8,
8+
},
9+
PADDING_WIDTH: 40,
10+
CURRENCY: '€',
11+
SEPARATORS: {
12+
HORIZONTAL: '-',
13+
VERTICAL: '|',
14+
},
15+
};

test-pnpm/shopping-cart-kata/discount.should.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('Discount should', () => {
1414
expect(discount).toBeNull();
1515
});
1616

17-
it('alculate a discount from a price', () => {
17+
it('calculate a discount from a price', () => {
1818
const discount = Discount.fromCode('PROMO_10')!;
1919
expect(discount.applyTo(100)).toBe(90);
2020
});
Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CartItem } from './cartItem';
2+
import { TABLE_CONSTANTS } from './constants';
23
import { Discount } from './discount';
34

45
export interface TablePrinter {
@@ -11,49 +12,73 @@ export interface TablePrinter {
1112
printHeaderFooter(): string;
1213
}
1314

14-
export class InMemoryTablePrinter implements TablePrinter {
15+
export class DefaultTablePrinter implements TablePrinter {
1516
printHeader(): string {
1617
return [
17-
'--------------------------------------------',
18-
'| Product name | Price with VAT | Quantity |',
19-
'| ------------ | -------------- | -------- |',
18+
this.printLineSeparator(),
19+
`${TABLE_CONSTANTS.SEPARATORS.VERTICAL} Product name ${TABLE_CONSTANTS.SEPARATORS.VERTICAL} Price with VAT ${TABLE_CONSTANTS.SEPARATORS.VERTICAL} Quantity ${TABLE_CONSTANTS.SEPARATORS.VERTICAL}`,
20+
`${TABLE_CONSTANTS.SEPARATORS.VERTICAL} ${'-'.repeat(TABLE_CONSTANTS.COLUMN_WIDTHS.NAME)} ${TABLE_CONSTANTS.SEPARATORS.VERTICAL} ${'-'.repeat(TABLE_CONSTANTS.COLUMN_WIDTHS.PRICE)} ${TABLE_CONSTANTS.SEPARATORS.VERTICAL} ${'-'.repeat(TABLE_CONSTANTS.COLUMN_WIDTHS.QUANTITY)} ${TABLE_CONSTANTS.SEPARATORS.VERTICAL}`,
2021
].join('\n');
2122
}
2223

2324
printHeaderFooter(): string {
24-
return '|------------------------------------------|';
25+
return `${TABLE_CONSTANTS.SEPARATORS.VERTICAL}${'-'.repeat(TABLE_CONSTANTS.LINE_WIDTH - 2)}${TABLE_CONSTANTS.SEPARATORS.VERTICAL}`;
2526
}
2627

2728
printCartItem(item: CartItem): string {
28-
const name = item.product.name.padEnd(12);
29-
const priceWithVat = `${item.product.finalPrice.toFixed(2)} €`.padEnd(14);
30-
const quantity = item.quantity.toString().padEnd(8);
29+
const name = item.product.name.padEnd(TABLE_CONSTANTS.COLUMN_WIDTHS.NAME);
30+
const priceWithVat =
31+
`${this.formatPrice(item.product.finalPrice)} ${TABLE_CONSTANTS.CURRENCY}`.padEnd(
32+
TABLE_CONSTANTS.COLUMN_WIDTHS.PRICE,
33+
);
34+
const quantity = item.quantity.toString().padEnd(TABLE_CONSTANTS.COLUMN_WIDTHS.QUANTITY);
3135

32-
return `| ${name} | ${priceWithVat} | ${quantity} |`;
36+
return `${TABLE_CONSTANTS.SEPARATORS.VERTICAL} ${name} ${TABLE_CONSTANTS.SEPARATORS.VERTICAL} ${priceWithVat} ${TABLE_CONSTANTS.SEPARATORS.VERTICAL} ${quantity} ${TABLE_CONSTANTS.SEPARATORS.VERTICAL}`;
3337
}
3438

3539
printLineSeparator(): string {
36-
return '--------------------------------------------';
40+
return TABLE_CONSTANTS.SEPARATORS.HORIZONTAL.repeat(TABLE_CONSTANTS.LINE_WIDTH);
3741
}
3842

3943
printPromotion(discount: Discount | null): string {
40-
const promotionCode = discount?.code ?? '';
41-
const promotionDescription =
42-
discount != null
43-
? `Promotion: ${discount?.percentage * 100}% off with code ${promotionCode}`
44-
: 'Promotion:';
45-
return [this.printHeaderFooter(), `| ${promotionDescription.padEnd(40)} |`].join('\n');
44+
const promotionDescription = this.formatPromotionDescription(discount);
45+
return [
46+
this.printHeaderFooter(),
47+
`${TABLE_CONSTANTS.SEPARATORS.VERTICAL} ${promotionDescription.padEnd(TABLE_CONSTANTS.PADDING_WIDTH)} ${TABLE_CONSTANTS.SEPARATORS.VERTICAL}`,
48+
].join('\n');
4649
}
4750

4851
printProductCount(items: CartItem[]): string {
49-
const totalItems = items.reduce((total, item) => total + item.quantity, 0);
50-
return `| Total products: ${totalItems.toString().padEnd(24)} |`;
52+
const totalItems = this.calculateTotalItems(items);
53+
return `${TABLE_CONSTANTS.SEPARATORS.VERTICAL} Total products: ${totalItems.toString().padEnd(24)} ${TABLE_CONSTANTS.SEPARATORS.VERTICAL}`;
5154
}
5255

5356
printTotalPrice(items: CartItem[], discount: Discount | null): string {
54-
const totalPrice = items.reduce((total, item) => total + item.LineTotal, 0);
57+
const totalPrice = this.calculateTotalPrice(items);
5558
const discountedAmount = discount ? discount.applyTo(totalPrice) : totalPrice;
56-
const description = `Total price: ${discountedAmount.toFixed(2)} €`;
57-
return `| ${description.padEnd(40)} |`;
59+
const description = `Total price: ${this.formatPrice(discountedAmount)} ${TABLE_CONSTANTS.CURRENCY}`;
60+
61+
return `${TABLE_CONSTANTS.SEPARATORS.VERTICAL} ${description.padEnd(TABLE_CONSTANTS.PADDING_WIDTH)} ${TABLE_CONSTANTS.SEPARATORS.VERTICAL}`;
62+
}
63+
64+
private formatPromotionDescription(discount: Discount | null): string {
65+
if (discount == null) {
66+
return 'Promotion:';
67+
}
68+
69+
const percentage = Math.round(discount.percentage * 100);
70+
return `Promotion: ${percentage}% off with code ${discount.code}`;
71+
}
72+
73+
private calculateTotalItems(items: CartItem[]): number {
74+
return items.reduce((total, item) => total + item.quantity, 0);
75+
}
76+
77+
private calculateTotalPrice(items: CartItem[]): number {
78+
return items.reduce((total, item) => total + item.LineTotal, 0);
79+
}
80+
81+
private formatPrice(price: number): string {
82+
return price.toFixed(2);
5883
}
5984
}

test-pnpm/shopping-cart-kata/shoppingCart.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CartItem } from './cartItem';
22
import { Discount } from './discount';
33
import { Logger } from './logger';
4-
import { InMemoryTablePrinter, TablePrinter } from './printer';
4+
import { DefaultTablePrinter, TablePrinter } from './printer';
55

66
export interface ShoppingCart {
77
addItem(cartItem: CartItem): void;
@@ -12,7 +12,7 @@ export interface ShoppingCart {
1212
export class InMemoryShoppingCart implements ShoppingCart {
1313
private items: CartItem[] = [];
1414
private discounts: Discount[] = [];
15-
private printer: TablePrinter = new InMemoryTablePrinter();
15+
private printer: TablePrinter = new DefaultTablePrinter();
1616

1717
constructor(private logger: Logger) {}
1818

0 commit comments

Comments
 (0)