Created
July 5, 2019 08:40
-
-
Save FongX777/62380cd948f57507c4621cabfda1da9d to your computer and use it in GitHub Desktop.
Domain Driven Design - JS code sample
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* --------------------------- | |
* Domain Layer | |
* --------------------------- | |
*/ | |
class Order { | |
/** | |
* | |
* @param {Object} prop | |
* @param {string} prop.id | |
* @param {string} prop.userId | |
* @param {string} prop.storeId | |
* @param {string} prop.orderNo | |
* @param {Order.STATUS_ENUM} prop.status | |
* @param {[OrderItem]} prop.items | |
* @param {string} prop.createdAt | |
* @param {string} prop.updatedAt | |
*/ | |
constructor({ | |
id, | |
userId, | |
storeId, | |
orderNo, | |
status, | |
items, | |
createdAt, | |
updatedAt, | |
}) { | |
this.id = id; | |
this.userId = userId; | |
this.storeId = storeId; | |
this.orderNo = orderNo; | |
this.status = status; | |
this.items = items; | |
this.createdAt = createdAt; | |
this.updatedAt = updatedAt; | |
this._getNowIso = () => new Date().toISOString(); | |
} | |
setGetNowIso(_getNowIso) { | |
this._getNowIso = _getNowIso; | |
} | |
cancel() { | |
if ( | |
this.status === Order.STATUS_ENUM.CANCELED || | |
this.status === Order.STATUS_ENUM.CLOSED | |
) { | |
throw new Error(); | |
} | |
this.status = Order.STATUS_ENUM.CANCELED; | |
} | |
close() { | |
if ( | |
this.status === Order.STATUS_ENUM.CANCELED || | |
this.status === Order.STATUS_ENUM.CLOSED | |
) { | |
throw new Error(); | |
} | |
this.status = Order.STATUS_ENUM.CLOSED; | |
} | |
static create({ id, userId, storeId, orderNo, items }) { | |
return new Order({ | |
id, | |
userId, | |
storeId, | |
orderNo, | |
status: Order.STATUS_ENUM.CREATED, // business rule | |
items, | |
createdAt: new Date().toISOString(), | |
updatedAt: new Date().toISOString(), | |
}); | |
} | |
} | |
/** @enum {string} - status of the order */ | |
Order.STATUS_ENUM = { | |
CREATED: 'CREATED', | |
CLOSED: 'CLOSED', | |
CANCELED: 'CANCELED', | |
}; | |
class OrderItem { | |
/** | |
* @param {Object} prop | |
* @param {string} prop.orderId | |
* @param {string} prop.variantId | |
* @param {OrderItem.TYPE_ENUM} prop.type | |
* @param {string} prop.name | |
* @param {number} prop.price | |
* @param {number} prop.count | |
*/ | |
constructor({ orderId, variantId, type, name, price, count }) { | |
if (type === OrderItem.TYPE_ENUM.PRODUCT && price < 0) { | |
throw new Error(); | |
} else if (type === OrderItem.TYPE_ENUM.GIFT && price !== 0) { | |
throw new Error(); | |
} | |
this.orderId = orderId; | |
this.type = type; | |
this.variantId = variantId; | |
this.name = name; | |
this.price = price; | |
this.count = count; | |
} | |
/** | |
* @returns {number} | |
*/ | |
// @ts-ignore | |
get total() { | |
const total = this.total * this.count; | |
return this.type === OrderItem.TYPE_ENUM.DISCOUNT ? -1 * total : total; | |
} | |
changePrice(price) { | |
return new OrderItem({ ...this, price }); | |
} | |
/** | |
* @param {Object} prop | |
* @param {string} prop.orderId | |
* @param {string|null} prop.variantId | |
* @param {string} prop.name | |
* @param {number} prop.price | |
* @param {number} prop.count | |
*/ | |
static createProductTypeItem({ orderId, variantId, name, price, count }) { | |
return new OrderItem({ | |
orderId, | |
type: OrderItem.TYPE_ENUM.PRODUCT, | |
name, | |
price, | |
count, | |
variantId, | |
}); | |
} | |
/** | |
* @param {Object} prop | |
* @param {string} prop.orderId | |
* @param {string} prop.variantId | |
* @param {string} prop.name | |
* @param {number} prop.count | |
*/ | |
static createGiftTypeItem({ orderId, variantId, name, count }) { | |
return new OrderItem({ | |
orderId, | |
type: OrderItem.TYPE_ENUM.GIFT, | |
name, | |
price: 0, | |
count, | |
variantId, | |
}); | |
} | |
/** | |
* @param {Object} prop | |
* @param {string} prop.orderId | |
* @param {string} prop.name | |
* @param {number} prop.price | |
*/ | |
static createDiscountTypeItem({ orderId, name, price }) { | |
return new OrderItem({ | |
orderId, | |
type: OrderItem.TYPE_ENUM.DISCOUNT, | |
name, | |
price, | |
count: 1, | |
variantId: null, | |
}); | |
} | |
/** | |
* @param {Object} prop | |
* @param {string} prop.orderId | |
* @param {string} prop.name | |
* @param {number} prop.price | |
*/ | |
static createShipmentFeeTypeItem({ orderId, name, price = 0 }) { | |
return new OrderItem({ | |
orderId, | |
type: OrderItem.TYPE_ENUM.SHIPMENT_FEE, | |
name, | |
price, | |
count: 1, | |
variantId: null, | |
}); | |
} | |
/** | |
* @param {Object} prop | |
* @param {string} prop.orderId | |
* @param {string} prop.name | |
* @param {number} prop.price | |
*/ | |
static createPaymentFeeTypeItem({ orderId, name, price = 0 }) { | |
return new OrderItem({ | |
orderId, | |
type: OrderItem.TYPE_ENUM.PAYMENT_FEE, | |
name, | |
price, | |
count: 1, | |
variantId: null, | |
}); | |
} | |
} | |
/** @enum {string} - type of the order item */ | |
OrderItem.TYPE_ENUM = { | |
PRODUCT: 'PRODUCT', | |
DISCOUNT: 'DISCOUNT', | |
GIFT: 'GIFT', | |
SHIPMENT_FEE: 'SHIPMENT_FEE', | |
PAYMENT_FEE: 'PAYMENT_FEE', | |
}; | |
module.exports = { | |
Order, | |
OrderItem, | |
}; | |
/** | |
* --------------------------- | |
* Domain Layer Testing | |
* --------------------------- | |
*/ | |
const test = require('ava'); | |
const create_testcases = []; | |
const create_testMacro = (t, input) => { | |
Order.create(input); | |
t.pass(); | |
}; | |
create_testcases.forEach(tc => | |
// @ts-ignore | |
test(tc.description, create_testMacro, tc.input) | |
); | |
const cancel_testcases = []; | |
const cancel_testMacro = (t, input) => { | |
const o = Order.create(input); | |
o.cancel(); | |
t.is(o.status, Order.STATUS_ENUM.CANCELED); | |
}; | |
cancel_testcases.forEach(tc => | |
// @ts-ignore | |
test(tc.description, cancel_testMacro, tc.input) | |
); | |
/** | |
* --------------------------- | |
* Repository Layer | |
* * a repository class | |
* * a toXXX helper function | |
* * a toDb helper function | |
* --------------------------- | |
*/ | |
class OrderRepository { | |
/** * @param {Object} db */ | |
constructor(db) { | |
this.db = db; | |
} | |
/** | |
* @param {string} id | |
* @returns {Promise<Order>} | |
*/ | |
async getOrderById(id) { | |
const dbData = await this.db.query('SELECT data FROM order WHERE id = $1', [ | |
id, | |
]); | |
return toOrder(dbData); | |
} | |
/** | |
* @param {Order} order | |
* @returns {Promise<Order>} | |
*/ | |
async insertOrder(order) { | |
const dbData = await this.db.query( | |
'INSERT INTO order (data) VALUES ($1) RETURNING data', | |
[toDb(order)] | |
); | |
return toOrder(dbData); | |
} | |
/** | |
* @param {string} id | |
* @param {Order} order | |
* @returns {Promise<Order>} | |
*/ | |
async updateOrder(id, order) { | |
const dbData = await this.db.query( | |
'UPDATE order set data = ($1) WHERE id = $2 RETURNING data', | |
[toDb(order), id] | |
); | |
return toOrder(dbData); | |
} | |
} | |
/** | |
* @param {Object} data | |
* @returns {Order} | |
*/ | |
function toOrder(data) { | |
return new Order({ | |
id: data.id, | |
userId: data.userId, | |
storeId: data.storeId, | |
orderNo: data.orderNo, | |
status: data.status, | |
items: data.items.map(item => new OrderItem(item)), | |
createdAt: data.createdAt, | |
updatedAt: data.updatedAt, | |
}); | |
} | |
/** | |
* @param {Order} order | |
* @returns {Object} | |
*/ | |
function toDb(order) { | |
return { orderNo: order.orderNo, ...order }; | |
} | |
/** | |
* --------------------------- | |
* Application Layer | |
* --------------------------- | |
*/ | |
const uuidV4 = require('uuid/v4'); | |
/** | |
* Use case1: createOrder | |
* @param {Object} param | |
* @param {Object} param.input | |
* @param {OrderRepository} param.orderRepo | |
* @returns {Promise<Order>} | |
*/ | |
async function createOrder({ input, orderRepo }) { | |
const { | |
userId, | |
storeId, | |
orderNo, | |
products, | |
payment, | |
shipment, | |
discount, | |
} = input; | |
const orderId = uuidV4(); | |
const order = Order.create({ | |
userId, | |
storeId, | |
orderNo, | |
id: orderId, | |
items: [ | |
OrderItem.createPaymentFeeTypeItem({ orderId, ...payment }), | |
OrderItem.createShipmentFeeTypeItem({ orderId, ...shipment }), | |
OrderItem.createDiscountTypeItem({ orderId, ...discount }), | |
...products.map(p => OrderItem.createProductTypeItem({ orderId, ...p })), | |
], | |
}); | |
await orderRepo.insertOrder(order); | |
return order; | |
} | |
/** | |
* Use case 2: cancel order | |
* @param {Object} param | |
* @param {string} param.id | |
* @param {OrderRepository} param.orderRepo | |
* @returns {Promise<Order>} | |
*/ | |
async function cancelOrder({ id, orderRepo }) { | |
const order = await orderRepo.getOrderById(id); | |
order.cancel(); | |
await orderRepo.updateOrder(id, order); | |
return order; | |
} | |
/** | |
* --------------------------- | |
* Entry point | |
* --------------------------- | |
*/ | |
async function entryPoint() { | |
const input = { | |
userId: '11111111-1111-1111-1111-111111111111', | |
storeId: '22222222-2222-2222-2222-222222222222', | |
orderNo: 'ORDER0001', | |
products: [ | |
{ name: 'A', price: 1000, count: 1, variantId: '1' }, | |
{ name: 'B', price: 50, count: 10, variantId: '2' }, | |
], | |
payment: { name: 'Credit Card', price: 10 }, | |
shipment: { name: 'CVS', price: 100 }, | |
discount: { name: 'Summer Discount', price: 50 }, | |
}; | |
const db = {}; | |
const order = await createOrder({ | |
input, | |
orderRepo: new OrderRepository(db), | |
}); | |
await cancelOrder({ id: order.id, orderRepo: new OrderRepository(db) }); | |
} | |
entryPoint(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment