Skip to content

Instantly share code, notes, and snippets.

@FongX777
Created July 5, 2019 08:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FongX777/62380cd948f57507c4621cabfda1da9d to your computer and use it in GitHub Desktop.
Save FongX777/62380cd948f57507c4621cabfda1da9d to your computer and use it in GitHub Desktop.
Domain Driven Design - JS code sample
/**
* ---------------------------
* 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