Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/typescript/ts-stock_management-01_base/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
coverage
9 changes: 9 additions & 0 deletions examples/typescript/ts-stock_management-01_base/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Stock management

Code smells:
- Data class
- Shotgun surgery

## Setup
- Install dependencies with `yarn install`
- Execute test with `yarn test`
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
}
19 changes: 19 additions & 0 deletions examples/typescript/ts-stock_management-01_base/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "step_shotgun_surgery",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "jest",
"test-coverage": "jest --coverage"
},
"devDependencies": {
"@babel/core": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"@babel/preset-typescript": "^7.13.0",
"@types/jest": "^26.0.22",
"babel-jest": "^26.6.3",
"jest": "^26.6.3",
"typescript": "^4.2.4"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export class AggregateRoot {
private domainEvents: Array<any>

constructor() {
this.domainEvents = []
}

pullDomainEvents(): Array<any> {
const domainEvents = this.domainEvents.slice()
this.domainEvents = []

return domainEvents
}

record(event: any): void {
this.domainEvents.push(event)
}
}
66 changes: 66 additions & 0 deletions examples/typescript/ts-stock_management-01_base/src/FlowerStock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { LowStock } from "./LowStock"
import { AggregateRoot } from "./AggregateRoot"
import { NotEnoughStock } from "./NotEnoughStock"
import { OutOfStock } from "./OutOfStock"

export class FlowerStock extends AggregateRoot {

private flowers: Array<string>

constructor(flowers: Array<string> = []) {
super()
const flowerSet = new Set<string>(flowers)
this.flowers = Array.from(flowerSet)
}

total() {
return this.flowers.length
}

add(flowerId: string): void {
const flowerSet = new Set<string>(this.flowers)
flowerSet.add(flowerId)
this.flowers = Array.from(flowerSet)
}

get(): string {
if (this.flowers.length === 0) {
throw new OutOfStock()
}
const currentStock = this.flowers.length

const flowerId = this.flowers.shift()

if (currentStock >= 50 && this.flowers.length < 50) {
this.record(new LowStock('low flower stock'))
}

return flowerId
}

getMultiple(amount: number): Array<string> {
if (this.flowers.length < amount) {
throw new NotEnoughStock()
}

const currentStock = this.flowers.length

const flowerIds: Array<string> = this.flowers.splice(0, amount)

if (currentStock >= 50 && this.flowers.length < 50) {
this.record(new LowStock(`low flower stock produced by high demand: ${amount}`))
}

return flowerIds
}

removeDeadFlowers(deadFlowers: Array<string>): void {
const currentStock = this.flowers.length

this.flowers = this.flowers.filter(flowerId => !deadFlowers.includes(flowerId))

if (currentStock >= 50 && this.flowers.length < 50) {
this.record(new LowStock(`low flower stock produced by ${deadFlowers.length} dead flowers`))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class LowStock {
constructor(public readonly reason: string) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class NotEnoughStock extends Error {
constructor() {
super('Not enough stock')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class OutOfStock extends Error {
constructor() {
super('Out of stock')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { FlowerStock } from "../src/FlowerStock"
import { LowStock } from "../src/LowStock"
import { NotEnoughStock } from "../src/NotEnoughStock"
import { OutOfStock } from "../src/OutOfStock"

describe('flower stock management', () => {

it('should add a flower to the stock', () => {
const stockFlowerIds = ['1', '2']
const stock = new FlowerStock(stockFlowerIds)

stock.add('3')

expect(stock.total()).toBe(3)
})

it('should add the same flower multiple times', () => {
const stock = new FlowerStock()

stock.add('3')
stock.add('3')

expect(stock.total()).toBe(1)
})

it('get a single flower from the stock', () => {
const flowerIds = ['1', '2']
const stock = new FlowerStock(flowerIds)

const flowerId = stock.get()

expect(flowerId).toContain('1')
expect(stock.total()).toBe(1)
})

it('should reach low stock when getting a single flower from a stock of 50', () => {
const stockFlowerIds = makeFlowerIds(50)
const stock = new FlowerStock(stockFlowerIds)

stock.get()

const domainEvents = stock.pullDomainEvents()
expect(domainEvents.length).toBe(1)
const lowStockEvent = domainEvents[0] as LowStock
expect(lowStockEvent.reason).toBe('low flower stock')
})

it('should not reach low stock when getting a single flower from a stock greater than 50', () => {
const flowerIds = makeFlowerIds(51)
const stock = new FlowerStock(flowerIds)

stock.get()

expect(stock.pullDomainEvents().length).toBe(0)
})

it('should throw an error when trying to get a single flower from an empty stock', () => {
const stock = new FlowerStock()

expect(() => stock.get()).toThrow(OutOfStock)
})

it('should get multiple flowers from the stock', () => {
const stockFlowerIds = ['1', '2', '3']
const stock = new FlowerStock(stockFlowerIds)

const flowerIds = stock.getMultiple(2)

expect(flowerIds).toEqual(['1', '2'])
expect(stock.total()).toBe(1)
})

it('should reach low stock when getting multiple flowers and the limit of 50 is exceeded', () => {
const lowStockLevel = 50
const unitsToLowStock = 5
const unitsToGetFromTheStock = 6
const totalStock = lowStockLevel + unitsToLowStock
const stockFlowerIds = makeFlowerIds(totalStock)
const stock = new FlowerStock(stockFlowerIds)

stock.getMultiple(unitsToGetFromTheStock)

const domainEvents = stock.pullDomainEvents()
expect(domainEvents.length).toBe(1)
const lowStockEvent = domainEvents[0] as LowStock
expect(lowStockEvent.reason).toBe('low flower stock produced by high demand: 6')
})

it('should not reach low stock when getting multiple flowers and the limit of 50 is not exceeded', () => {
const lowStockLimit = 50
const unitsToLowStock = 5
const unitsToGetFromTheStock = 5
const totalStock = lowStockLimit + unitsToLowStock
const flowerIds = makeFlowerIds(totalStock)
const stock = new FlowerStock(flowerIds)

stock.getMultiple(unitsToGetFromTheStock)

expect(stock.pullDomainEvents().length).toBe(0)
})

it('should get a multiple flowers from an empty stock', () => {
const stock = new FlowerStock()

expect(() => stock.getMultiple(2)).toThrow(NotEnoughStock)
})

it('remove dead flowers from the stock', () => {
const stockFlowerIds = ['1', '2', '3']
const stock = new FlowerStock(stockFlowerIds)

const deadFlowerIds = ['1', '3']
stock.removeDeadFlowers(deadFlowerIds)

expect(stock.total()).toBe(1)
const aliveFlower = '2'
expect(stock.get()).toBe(aliveFlower)
})


it('should reach low stock when removing dead flowers and the limit of 50 is exceeded', () => {
const lowStockLevel = 50
const unitsToLowStock = 5
const totalDeadFlowers = 6
const totalStock = lowStockLevel + unitsToLowStock
const stockFlowerIds = makeFlowerIds(totalStock)
const stock = new FlowerStock(stockFlowerIds)

const deadFlowers = makeFlowerIds(totalDeadFlowers)
stock.removeDeadFlowers(deadFlowers)

const domainEvents = stock.pullDomainEvents()
expect(domainEvents.length).toBe(1)
const lowStockEvent = domainEvents[0] as LowStock
expect(lowStockEvent.reason).toBe('low flower stock produced by 6 dead flowers')
})

it('should not reach low stock when removing flowers and the limit of 50 is not exceeded', () => {
const lowStockLimit = 50
const unitsToLowStock = 5
const totalDeadFlowers = 5
const totalStock = lowStockLimit + unitsToLowStock
const flowerIds = makeFlowerIds(totalStock)
const stock = new FlowerStock(flowerIds)

const deadFlowers = makeFlowerIds(totalDeadFlowers)
stock.removeDeadFlowers(deadFlowers)

expect(stock.pullDomainEvents().length).toBe(0)
})

function makeFlowerIds(amount: number) {
return Array<string>(amount).fill('1').map((x, y) => (x + y) as string)
}
})
Loading