Created
May 27, 2017 10:30
-
-
Save eiriklv/9e843af9f8543bfc7d5dfaccc2323863 to your computer and use it in GitHub Desktop.
Data modeling an online store
This file contains hidden or 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
/** | |
* Modeling an online store with categories, sub-categories and products | |
* | |
* NOTE: The important thing is to be able to | |
* describe any requirement with the chosen data model | |
* | |
* NOTE: Another important thing is conventional interfaces | |
* and impedance matching. Data should just flow through the | |
* functions without needing to conform to many different interfaces | |
*/ | |
/** | |
* Product model | |
*/ | |
const product = { | |
type: 'phone', | |
id: 'nokia-3310', | |
name: 'Nokia 3310', | |
description: 'Most amazing phone.....', | |
manufacturer: 'nokia', | |
categories: ['phone'], | |
price: 399, | |
inventory: 2, | |
specifications: { | |
storageSize: 8, | |
screenSize: 4.3, | |
}, | |
}; | |
/** | |
* Alternative product model (with color variants) | |
*/ | |
const productWithVariants = { | |
type: 'phone-with-color-variants', | |
id: 'samsung-s7-edge', | |
name: 'Samsung S7 Edge', | |
description: 'Amazing phone.....', | |
manufacturer: 'samsung', | |
categories: ['phone'], | |
variants: [{ | |
id: 'samsung-s7-edge-black', | |
color: 'black', | |
inventory: 5, | |
price: 799, | |
}, { | |
id: 'samsung-s7-edge-gold', | |
color: 'white', | |
inventory: 7, | |
price: 899, | |
}], | |
specifications: { | |
storageSize: 32, | |
screenSize: 7.9, | |
}, | |
}; | |
/** | |
* Alternative product model (with storage and color variants and discount option) | |
*/ | |
const productWithStorageAndColorVariants = { | |
type: 'phone-with-storage-and-color-variants', | |
id: 'iphone-6s', | |
name: 'iPhone 6S', | |
description: 'Amazing phone.....', | |
manufacturer: 'apple', | |
categories: ['phone'], | |
storageVariants: [{ | |
id: 'iphone-6s-64gb', | |
size: 64, | |
price: 1099, | |
discount: -200, | |
colorVariants: [{ | |
id: 'iphone-6s-64gb-black', | |
color: 'black', | |
inventory: 5, | |
}, { | |
id: 'iphone-6s-64gb-white', | |
color: 'white', | |
inventory: 7, | |
}] | |
}, { | |
id: 'iphone-6s-128gb', | |
size: 128, | |
price: 1299, | |
colorVariants: [{ | |
id: 'iphone-6s-128gb-black', | |
color: 'black', | |
inventory: 3, | |
}, { | |
id: 'iphone-6s-128gb-white', | |
color: 'white', | |
inventory: 9, | |
}] | |
}], | |
}; | |
/** | |
* Manufacturer model | |
*/ | |
const manufacturer = { | |
id: 'apple', | |
name: 'Apple', | |
}; | |
/** | |
* Category model | |
*/ | |
const category = { | |
id: 'phone', | |
name: 'Phones', | |
description: 'Things you never use to call someone with..', | |
}; | |
/** | |
* Example product list | |
*/ | |
const products = [{ | |
type: 'phone-with-storage-and-color-variants', | |
id: 'iphone-6s', | |
name: 'iPhone 6S', | |
description: 'Amazing phone.....', | |
manufacturer: 'apple', | |
categories: ['phone'], | |
storageVariants: [{ | |
id: 'iphone-6s-64gb', | |
size: 64, | |
price: 1099, | |
colorVariants: [{ | |
id: 'iphone-6s-64gb-black', | |
color: 'black', | |
inventory: 5, | |
}, { | |
id: 'iphone-6s-64gb-white', | |
color: 'white', | |
inventory: 7, | |
}] | |
}, { | |
id: 'iphone-6s-128gb', | |
size: 128, | |
price: 1299, | |
colorVariants: [{ | |
id: 'iphone-6s-128gb-black', | |
color: 'black', | |
inventory: 3, | |
}, { | |
id: 'iphone-6s-128gb-white', | |
color: 'white', | |
inventory: 9, | |
}] | |
}], | |
}, { | |
type: 'phone-with-color-variants', | |
id: 'samsung-s7-edge', | |
name: 'Samsung S7 Edge', | |
description: 'Amazing phone.....', | |
manufacturer: 'samsung', | |
categories: ['phone'], | |
variants: [{ | |
id: 'samsung-s7-edge-black', | |
color: 'black', | |
inventory: 5, | |
price: 799, | |
}, { | |
id: 'samsung-s7-edge-gold', | |
color: 'white', | |
inventory: 7, | |
price: 899, | |
}], | |
specifications: { | |
storageSize: 32, | |
screenSize: 7.9, | |
}, | |
}, { | |
type: 'phone', | |
id: 'nokia-3310', | |
name: 'Nokia 3310', | |
description: 'Most amazing phone.....', | |
manufacturer: 'nokia', | |
categories: ['phone'], | |
price: 399, | |
inventory: 2, | |
specifications: { | |
storageSize: 8, | |
screenSize: 4.3, | |
}, | |
}, { | |
type: 'tablet', | |
id: 'ipad-pro-12', | |
name: 'iPad Pro 12', | |
description: 'Amazing tablet.....', | |
manufacturer: 'apple', | |
categories: ['tablet'], | |
price: 1299, | |
inventory: 5, | |
specifications: { | |
storageSize: 128, | |
screenSize: 12.9 | |
}, | |
}, { | |
type: 'tablet', | |
id: 'samsung-tab-a', | |
name: 'Samsung Tab A', | |
description: 'Other amazing tablet.....', | |
manufacturer: 'samsung', | |
categories: ['tablet'], | |
price: 1099, | |
inventory: 15, | |
specifications: { | |
storageSize: 64, | |
}, | |
}, { | |
type: 'cable', | |
id: 'lightning-cable-3m', | |
name: 'Lightning cable (3m)', | |
description: 'Amazing cable.....', | |
manufacturer: 'apple', | |
categories: ['cable'], | |
price: 699, | |
inventory: 10, | |
specifications: { | |
length: 3, | |
}, | |
}, { | |
type: 'cable', | |
id: 'usb-c-cable-5m', | |
name: 'USB-C cable (5m)', | |
description: 'Other amazing cable.....', | |
manufacturer: 'samsung', | |
categories: ['cable'], | |
price: 8, | |
inventory: 50, | |
specifications: { | |
length: 5, | |
}, | |
}, { | |
type: 'protector', | |
id: 'screen-protector-iphone', | |
name: 'iPhone Screen Protector', | |
description: 'Protect your screen with.....', | |
manufacturer: 'apple', | |
categories: ['protector'], | |
price: 20, | |
inventory: 50, | |
specifications: { | |
length: 5, | |
}, | |
}]; | |
/** | |
* Example manufacturer list | |
*/ | |
const manufacturers = [{ | |
id: 'apple', | |
name: 'Apple', | |
}, { | |
id: 'samsung', | |
name: 'Samsung', | |
}, { | |
id: 'nokia', | |
name: 'Nokia', | |
}]; | |
/** | |
* Example category list | |
*/ | |
const categories = [{ | |
id: 'device', | |
name: 'Devices', | |
description: 'Blablabla..' | |
}, { | |
id: 'phone', | |
name: 'Phones', | |
description: 'Blablabla..' | |
}, { | |
id: 'tablet', | |
name: 'Tablets', | |
description: 'Blablabla..' | |
}, { | |
id: 'accessory', | |
name: 'Accessories', | |
description: 'Blablabla..' | |
}, { | |
id: 'charger', | |
name: 'Chargers', | |
description: 'Blablabla..' | |
}, { | |
id: 'protector', | |
name: 'Protectors', | |
description: 'Blablabla..' | |
}]; | |
/** | |
* Example category hierarchy | |
* | |
* How many levels? | |
*/ | |
const categoryHierarchy = [{ | |
id: 'device', | |
children: [ | |
'phone', | |
'tablet', | |
], | |
}, { | |
id: 'accessory', | |
children: [ | |
'charger', | |
'protection', | |
], | |
}]; | |
/** | |
* Example category hierarchy (alternative) | |
*/ | |
const frontPagePromotionCategories = [{ | |
name: 'Phones' | |
categories: ['phone'] | |
limit: 4, | |
}, { | |
name: 'Tablets', | |
categories: ['tablet'] | |
limit: 4, | |
}, { | |
name: 'Accessories', | |
categories: ['charger', 'protector'], | |
limit: 8 | |
}]; | |
/** | |
* Example category hierarchy (alternative) | |
*/ | |
const frontPagePromotionProducts = [{ | |
name: 'Phones' | |
products: ['nokia-3310', 'iphone-6s', 'samsung-s7-edge'], | |
}, { | |
name: 'Tablets', | |
products: ['ipad-pro-12'] | |
}, { | |
name: 'Accessories', | |
products: ['usb-c-cable-5m', 'screen-protector-iphone'], | |
}]; | |
/** | |
* Cart item model | |
*/ | |
const cartItem = { productId: 'nokia-3310' }; | |
/** | |
* Cart item model (alternative) | |
*/ | |
const cartItem = { | |
productId: 'samsung-s7-edge', | |
variantId: 'samsung-s7-edge-gold', | |
}; | |
/** | |
* Cart item model (alternative) | |
*/ | |
const cartItem = { | |
productId: 'iphone-6s', | |
storageVariantId: 'iphone-6s-64gb', | |
colorVariantId: 'iphone-6s-64gb-black', | |
}; | |
/** | |
* Example cart list | |
*/ | |
const cart = [ | |
{ productId: 'nokia-3310' }, | |
{ productId: 'samsung-s7-edge', variantId: 'samsung-s7-edge-gold' }, | |
{ productId: 'iphone-6s', storageVariantId: 'iphone-6s-64gb', colorVariantId: 'iphone-6s-64gb-black' }, | |
]; | |
/** | |
* General utility | |
*/ | |
function getById(collection, id) { | |
return collection.find((item) => item.id === id); | |
} | |
/** | |
* Product selector | |
*/ | |
function getProductById(productId) { | |
return getById(products, productId); | |
} | |
/** | |
* Category selector | |
*/ | |
function getCategoryById(categoryId) { | |
return getById(categories, categoryId); | |
} | |
/** | |
* Manufacturer selector | |
*/ | |
function getManufacturerById(manufacturerId) { | |
return getById(manufacturers, manufacturerId); | |
} | |
/** | |
* Cart item price selector | |
*/ | |
function getCartItemPrice(cartItem) { | |
const priceGetters = { | |
'phone': ({ productId }) => getById(productId).price, | |
'phone-with-color-variants': ({ productId, variantId }) => getById(getById(productId).variants, variantId).price, | |
'phone-with-storage-and-color-variants': ({ productId, storageVariantId, colorVariantId }) => { | |
const selectedVariant = getById(getById(getById(productId).storageVariants, storageVariantId).colorVariants, colorVariantId); | |
const { price = 0, discount = 0 } = selectedVariant; | |
return price + discount; | |
}, | |
default: () => { | |
console.error('Unsupported product type'); | |
return 0; | |
}, | |
}; | |
const product = getProductById(cartItem.productId); | |
return (priceGetters[product.type] || priceGetters.default)(cartItem); | |
} | |
/** | |
* Cart item price selector | |
*/ | |
function getListRepresentationOfProduct(product) { | |
const informationGetters = { | |
'phone': ({ name, description, price }) => ({ name, description, price: [price] }), | |
'phone-with-color-variants': ({ name, description, colorVariants }) => ({ name, description, price: colorVariants.map(({ price }) => price)}), | |
'phone-with-storage-and-color-variants': ({ productId, storageVariantId, colorVariantId }) => { | |
const selectedVariant = getById(getById(productId).storageVariants, storageVariantId).price; | |
const { price = 0, discount = 0 } = selectedVariant; | |
return price + discount; | |
}, | |
default: () => { | |
console.error('Unsupported product type'); | |
return 0; | |
}, | |
}; | |
return (informationGetters[product.type] || informationGetters.default)(cartItem); | |
} | |
[ | |
{ name, description, price: [100] }, | |
{ name, description, price: [100, 500] } | |
] | |
function sum(a, b) { | |
return a + b; | |
} | |
/** | |
* Cart total selector | |
*/ | |
function getCartTotalPrice(cart) { | |
return cart | |
.map(getCartItemPrice) | |
.reduce(sum, 0); | |
} | |
/** | |
* Getting the prices for each item in the cart | |
*/ | |
const prices = cart.map(getCartItemPrice); | |
// [399, 899, 899] | |
/** | |
* Getting the total sum of the cart | |
*/ | |
const total = getCartTotalPrice(cart); | |
// 2197 | |
/** | |
* Listing out products from the category hierarchy | |
* (map + filter + reduce) | |
* | |
* NOTE: Use the url/query as state | |
* | |
* /category/:activeCategoryId/:activeSubCategoryId | |
* | |
* /product/:productId | |
*/ | |
/** | |
* General utility | |
*/ | |
function getByCategory(collection, categories) { | |
return collection.filter((item) => { | |
return item.categories.some((category) => categories.includes(category)) | |
}); | |
} | |
/** | |
* Product selector (by category) | |
*/ | |
function getProductsByCategoryId(categories) { | |
return getByCategory(products, categories); | |
} | |
/** | |
* Set the active (top level) category | |
*/ | |
const activeCategoryId = 'accessory'; | |
/** | |
* Get the data of the active (top level) category | |
*/ | |
const activeCategory = getCategoryById(activeCategoryId); | |
// { name, description } | |
/** | |
* Get the applicable sub-categories | |
*/ | |
const subCategoryIds = activeCategory.children; | |
// ['charger', 'protector'] | |
/** | |
* Set the active sub-category | |
*/ | |
const activeSubCategoryId = 'charger'; | |
/** | |
* Get the data of the active sub-category | |
*/ | |
const activeSubCategory = getCategoryById(activeSubCategoryId); | |
// { name, description } | |
/** | |
* Get the currently active products (that we want to list) | |
* based on the category level chosen | |
*/ | |
const activeProducts = activeSubCategoryId ? | |
getProductsByCategory([activeSubCategoryId]) : | |
getProductsByCategory(subCategoryIds); | |
/** | |
* Getting filter options | |
* (map + reduce) | |
*/ | |
function uniqueReducer(list, item) { | |
return [ | |
...list, | |
...(list.includes(item) ? [] : [list]), | |
]; | |
} | |
const manufacturerOptions = activeProducts | |
.map(({ manufacturer }) => manufacturer) | |
.reduce(uniqueReducer, []) | |
// ['apple', 'samsung'] | |
.map(getManufacturerById) | |
// [{ ... }, { ... }] | |
/** | |
* Set filter values | |
*/ | |
const filters = { | |
manufacturers: ['apple', 'samsung'], | |
} | |
/** | |
* Get the currently visible products based on filtering | |
*/ | |
const visibleProducts = activeProducts | |
.filter(({ manufacturer }) => filters.manufacturers.includes(manufacturer)) | |
/** | |
* Example state | |
* | |
* NOTE: Describing your entire application with data | |
*/ | |
const state = { | |
categoryHierarchy: [...], | |
frontPagePromotionCategories: [...], | |
frontPagePromotionProducts: [...], | |
categories: [...], | |
manufacturers: [...], | |
products: [...], | |
filters: {...}, | |
cart: [], | |
activeCategoryId: '...', // or part of url/query | |
activeSubCategoryId: '...', // or part of url/query | |
activeProductId: '...', // or part of url/query | |
}; | |
/** | |
* Component types | |
*/ | |
class Phone extends React.Component { | |
handleAddToCart() { | |
const { addToCart, productId } = this.props; | |
addToCart({ productId }); | |
} | |
render() { | |
// Handle rendering of a simple phone product | |
} | |
} | |
class PhoneWithColorVariants extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
activeVariantId: '', | |
}; | |
} | |
handleAddToCart() { | |
const { addToCart, productId } = this.props; | |
const { activeVariantId } = this.state; | |
addToCart({ | |
productId, | |
variantId: activeVariantId | |
}); | |
} | |
render() { | |
// Handle rendering of a phone product with color variants | |
} | |
} | |
class PhoneWithStorageAndColorVariants extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
activeStorageVariantId: '', | |
activeColorVariantId: '', | |
}; | |
} | |
// /products/iphone-6s?storageVariant=64gb&colorVariant=black | |
handleAddToCart() { | |
const { addToCart, params: { productId } } = this.props; | |
const { activeStorageVariantId, activeColorVariantId } = this.state; | |
addToCart({ | |
productId, | |
storageVariantId: activeStorageVariantId, | |
colorVariantId: activeColorVariantId, | |
}); | |
} | |
render() { | |
// Handle rendering of a phone product with storage and color variants | |
} | |
} | |
/** | |
* Mapping product types into component/screen types | |
* (mapping + selectors) | |
*/ | |
function getProductComponentByType({ type }) { | |
const productComponents = { | |
'phone': Phone, | |
'phone-with-color-variants': PhoneWithColorVariants, | |
'phone-with-storage-and-color-variants': PhoneWithStorageAndColorVariants, | |
'tablet': Tablet, | |
'cable': Cable, | |
'protector': Accessory, | |
default: NullComponent, | |
}; | |
return productComponents[type] || productComponents.default; | |
} | |
/** | |
* Mapping the entire state into an app with categories, sub-categories, products and a cart | |
*/ | |
const visibleProducts = [...]; | |
const visibleProductElements = visibleProducts | |
.map(getProductComponentByType) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment