├── .gitignore ├── 1-command ├── cqrs │ ├── __init__.py │ ├── app.py │ ├── command.py │ ├── events.py │ ├── main.py │ ├── query.py │ └── store.py └── ui-kit │ ├── app.js │ └── index.html ├── 2-memento ├── with-command │ ├── .gitignore │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── Button │ │ │ │ ├── index.tsx │ │ │ │ └── style.css │ │ │ ├── Grid │ │ │ │ ├── Tile.css │ │ │ │ ├── Tile.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.css │ │ │ ├── Header │ │ │ │ └── index.tsx │ │ │ ├── Heading │ │ │ │ ├── index.tsx │ │ │ │ └── style.css │ │ │ ├── Section │ │ │ │ ├── index.tsx │ │ │ │ └── style.css │ │ │ ├── YouWon │ │ │ │ ├── index.tsx │ │ │ │ └── style.css │ │ │ └── index.ts │ │ ├── containers │ │ │ ├── CommandManager.tsx │ │ │ ├── Game.tsx │ │ │ └── KeyboardEventHandler │ │ │ │ ├── Command.ts │ │ │ │ └── index.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── theme.css │ │ ├── utils.ts │ │ └── variables.css │ └── tsconfig.json └── with-memento │ ├── .gitignore │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ ├── src │ ├── App.tsx │ ├── components │ │ ├── Button │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── Grid │ │ │ ├── Tile.css │ │ │ ├── Tile.tsx │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── Header │ │ │ └── index.tsx │ │ ├── Heading │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── Section │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── YouWon │ │ │ ├── index.tsx │ │ │ └── style.css │ │ └── index.ts │ ├── containers │ │ ├── Game.tsx │ │ └── KeyboardEventHandler.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── theme.css │ ├── utils.ts │ └── variables.css │ └── tsconfig.json ├── README.md ├── chistmas-caching ├── README.md ├── reading │ ├── package.json │ ├── src │ │ ├── aside-cache-client.js │ │ ├── cache-manager.js │ │ ├── data-access-component.js │ │ ├── inline-cache-client.js │ │ └── resource-manager.js │ └── test │ │ ├── cache-manager.test.js │ │ ├── data-access-component.test.js │ │ ├── index.js │ │ └── resource-manager.test.js └── writing │ ├── requirements.txt │ ├── src │ ├── cache_manager.py │ ├── cache_manager_test.py │ ├── data_accessor.py │ ├── data_accessor_test.py │ ├── main.py │ ├── resource_manager.py │ └── utils.py │ └── tasks.py └── reauthor.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/*.py[cod] 3 | .vscode 4 | .idea 5 | **/node_modules 6 | **/package-lock.json 7 | virtualenv -------------------------------------------------------------------------------- /1-command/cqrs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shikaan/design-patterns/993d72339d2597ef512502b57b1305c1970d820f/1-command/cqrs/__init__.py -------------------------------------------------------------------------------- /1-command/cqrs/app.py: -------------------------------------------------------------------------------- 1 | from command import Deposit 2 | from query import GetAllDeposits, GetLastDeposit 3 | 4 | class BankApplication: 5 | def __init__(self, command_handler, query_handler, write_store, read_store): 6 | self.command_handler = command_handler 7 | self.query_handler = query_handler 8 | self.write_store = write_store 9 | self.read_store = read_store 10 | 11 | def deposit(self, amount): 12 | deposit_command = Deposit(self.write_store, amount) 13 | get_last_deposit_query = GetLastDeposit(self.read_store) 14 | self.command_handler.handle_deposit(deposit_command) 15 | return self.query_handler.handle(get_last_deposit_query) 16 | 17 | def get_all_deposits(self): 18 | get_all_deposits_query = GetAllDeposits(self.read_store) 19 | return self.query_handler.handle(get_all_deposits_query) 20 | -------------------------------------------------------------------------------- /1-command/cqrs/command.py: -------------------------------------------------------------------------------- 1 | from events import Observable 2 | 3 | 4 | class Command: 5 | def __init__(self, receiver): 6 | self.receiver = receiver 7 | 8 | def execute(self): 9 | raise NotImplementedError 10 | 11 | 12 | class Deposit(Command): # In CQRS traditionally you don't use the word Command in commands 13 | "ConcreteCommand" 14 | 15 | def __init__(self, receiver, amount): 16 | self.amount = amount 17 | super(Deposit, self).__init__(receiver) 18 | 19 | def execute(self): 20 | self.receiver.save(self.amount) 21 | 22 | 23 | class CommandHander(Observable): 24 | "Invoker" 25 | 26 | def __init__(self): 27 | self.events = { 28 | 'deposited': 'deposited' 29 | } 30 | super(CommandHander, self).__init__() 31 | 32 | def handle_deposit(self, command): 33 | command.execute() 34 | self.fire(self.events['deposited']) 35 | -------------------------------------------------------------------------------- /1-command/cqrs/events.py: -------------------------------------------------------------------------------- 1 | class Event(object): 2 | def __init__(self, type): 3 | self.type = type 4 | 5 | class Observable(object): 6 | def __init__(self): 7 | self.callbacks = [] 8 | def subscribe(self, callback): 9 | self.callbacks.append(callback) 10 | def fire(self, type, **attrs): 11 | e = Event(type) 12 | e.source = self 13 | for k, v in attrs.items(): 14 | setattr(e, k, v) 15 | for fn in self.callbacks: 16 | fn(e) 17 | -------------------------------------------------------------------------------- /1-command/cqrs/main.py: -------------------------------------------------------------------------------- 1 | from store import WriteStore, ReadStore 2 | from command import CommandHander 3 | from query import QueryHandler 4 | from app import BankApplication 5 | 6 | write_store = WriteStore() 7 | command_handler = CommandHander() 8 | 9 | read_store = ReadStore(command_handler, write_store) 10 | query_handler = QueryHandler(read_store) 11 | 12 | app = BankApplication(command_handler, query_handler, write_store, read_store) 13 | 14 | last_deposit = app.deposit(100) 15 | all_deposits = app.get_all_deposits() 16 | 17 | print('Last deposit:', last_deposit) 18 | print('All deposits:', all_deposits) 19 | -------------------------------------------------------------------------------- /1-command/cqrs/query.py: -------------------------------------------------------------------------------- 1 | class Query: 2 | def __init__(self, receiver): 3 | self.receiver = receiver 4 | 5 | def execute(self): 6 | raise NotImplementedError 7 | 8 | 9 | class GetLastDeposit(Query): 10 | def execute(self): 11 | self.result = self.receiver.entries[-1] 12 | 13 | class GetAllDeposits(Query): 14 | def execute(self): 15 | self.result = self.receiver.entries 16 | 17 | class QueryHandler: 18 | def __init__(self, read_store): 19 | self.read_store = read_store 20 | 21 | def handle(self, query): 22 | query.execute() 23 | return query.result 24 | -------------------------------------------------------------------------------- /1-command/cqrs/store.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | 3 | 4 | class WriteStore: 5 | "Receiver" 6 | 7 | def __init__(self): 8 | self.entries = [] 9 | 10 | def save(self, amount): 11 | entry = { 12 | 'timestamp': time(), 13 | 'amount': amount 14 | } 15 | self.entries.append(entry) 16 | 17 | 18 | class ReadStore: 19 | 20 | def __init__(self, command_handler, write_store): 21 | self.write_store = write_store 22 | self.command_handler = command_handler 23 | self.entries = [] 24 | 25 | self.command_handler.subscribe(self.update) 26 | 27 | def update(self, event): 28 | if event.type == self.command_handler.events['deposited']: 29 | self.entries = self.write_store.entries 30 | -------------------------------------------------------------------------------- /1-command/ui-kit/app.js: -------------------------------------------------------------------------------- 1 | // #=== 2 | // Define participants 3 | 4 | class Command { 5 | /** 6 | * @param {Receiver} receiver 7 | */ 8 | constructor(receiver) { 9 | this.receiver = receiver 10 | } 11 | 12 | execute() { 13 | throw new Error('Subclass must override execute') 14 | } 15 | } 16 | 17 | // Concrete Commands 18 | class OpenAlertCommand extends Command { 19 | execute() { 20 | this.receiver.alert('Gotcha!') 21 | } 22 | } 23 | 24 | class AggregateCommand extends Command { 25 | constructor(...commands) { 26 | super() 27 | this.commands = commands 28 | } 29 | 30 | execute() { 31 | this.commands.forEach(command => command.execute()) 32 | } 33 | } 34 | 35 | // Receiver 36 | class Window { 37 | /** 38 | * @param {string} message 39 | */ 40 | alert(message) { 41 | window.alert(message) 42 | } 43 | } 44 | 45 | // Receiver 46 | class Console { 47 | /** 48 | * @param {string} message 49 | * @param {Node} node 50 | */ 51 | alert(message) { 52 | console.log('ALERT', message) 53 | } 54 | } 55 | 56 | // Invoker 57 | class Button { 58 | /** 59 | * @param {string} label 60 | * @param {Command} command 61 | */ 62 | constructor(label, command) { 63 | this.label = label 64 | this.command = command 65 | this.node = document.createElement('button') 66 | 67 | this.build() 68 | } 69 | 70 | build() { 71 | this.node.innerText = this.label 72 | this.node.onclick = () => this.onClickHandler() 73 | } 74 | 75 | onClickHandler() { 76 | this.command.execute() 77 | } 78 | } 79 | 80 | // Client 81 | class Application { 82 | /** 83 | * @param {Node} node 84 | */ 85 | constructor(node) { 86 | this.node = node 87 | } 88 | 89 | init() { 90 | const windowReceiver = new Window() 91 | const consoleReceiver = new Console() 92 | 93 | const windowAlertCommand = new OpenAlertCommand(windowReceiver) 94 | const consoleAlertCommand = new OpenAlertCommand(consoleReceiver) 95 | 96 | const buttons = [ 97 | new Button('Window', windowAlertCommand), 98 | new Button('Console', consoleAlertCommand), 99 | new Button('Why choose?', new AggregateCommand(windowAlertCommand, consoleAlertCommand)) 100 | ] 101 | 102 | buttons.forEach((button) => this.node.appendChild(button.node)) 103 | } 104 | } 105 | 106 | // #=== 107 | // Execute commands 108 | const appNode = document.getElementById('app') 109 | const application = new Application(appNode) 110 | 111 | application.init() -------------------------------------------------------------------------------- /1-command/ui-kit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | UI Kit 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /2-memento/with-command/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /2-memento/with-command/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-command", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/classnames": "^2.2.6", 7 | "@types/deep-equal": "^1.0.1", 8 | "@types/jest": "23.3.9", 9 | "@types/node": "10.12.10", 10 | "@types/react": "16.7.7", 11 | "@types/react-dom": "16.0.10", 12 | "classnames": "^2.2.6", 13 | "deep-copy": "^1.4.2", 14 | "react": "^16.6.3", 15 | "react-dom": "^16.6.3", 16 | "react-scripts": "2.1.1", 17 | "typescript": "3.1.6" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /2-memento/with-command/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shikaan/design-patterns/993d72339d2597ef512502b57b1305c1970d820f/2-memento/with-command/public/favicon.ico -------------------------------------------------------------------------------- /2-memento/with-command/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /2-memento/with-command/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /2-memento/with-command/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Grid, Header, Heading, Section, YouWon} from "./components"; 3 | import {Game, IGame} from "./containers/Game"; 4 | import {KeyboardEventHandler} from "./containers/KeyboardEventHandler"; 5 | import {CommandManager, ICommandManager} from "./containers/CommandManager"; 6 | 7 | const renderGameChildren = ( 8 | rows: number[][], 9 | selectedTile: number[], 10 | isWinning: boolean, 11 | game: IGame 12 | ) => { 13 | if (isWinning) { 14 | return 15 | } 16 | 17 | return ( 18 | 19 | {renderCommandManagerChildren(rows, selectedTile, isWinning, game)} 20 | 21 | ) 22 | } 23 | 24 | const renderCommandManagerChildren = ( 25 | rows: number[][], 26 | selectedTile: number[], 27 | isWinning: boolean, 28 | game: IGame 29 | ) => (commandManager: ICommandManager, disableUndo: boolean) => ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | 38 | const App = () => { 39 | return ( 40 |
41 |
42 | Sort the tiles 43 | Click on a cell and use keyboard arrows to move the tiles 44 |
45 | {renderGameChildren} 46 |
47 | ) 48 | } 49 | 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames' 3 | import './style.css' 4 | 5 | export const Button: React.FunctionComponent<{ children: string, className?: string, disabled?: boolean, onClick: () => void, primary?: boolean }> = 6 | ({children, className, disabled, onClick, primary}) => { 7 | const classes = classnames('Button', { 8 | 'Button--primary': primary 9 | }) 10 | 11 | return ( 12 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Button/style.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | 4 | .Button { 5 | background: var(--light-color); 6 | border-width: var(--base-border-width); 7 | border-radius: var(--base-border-radius); 8 | box-shadow: none; 9 | margin: var(--base-size); 10 | padding: calc(var(--base-size) * 4) calc(var(--base-size) * 8); 11 | 12 | font-size: var(--h3-font-size); 13 | letter-spacing: var(--h3-letter-spacing); 14 | } 15 | 16 | .Button:disabled { 17 | opacity: .3; 18 | } 19 | 20 | .Button.Button--primary { 21 | background-color: var(--primary-color); 22 | color: var(--light-color); 23 | } 24 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Grid/Tile.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | @import "./style.css"; 4 | 5 | .Tile { 6 | --height: calc(var(--grid-width) / 3); 7 | 8 | background: var(--primary-color); 9 | color: var(--light-color); 10 | flex: 1; 11 | text-align: center; 12 | cursor: pointer; 13 | 14 | line-height: var(--height); 15 | height: var(--height); 16 | } 17 | 18 | .Tile.Tile--transparent { 19 | opacity: 0; 20 | } 21 | 22 | .Tile.Tile--selected { 23 | background: var(--accent-color); 24 | } 25 | 26 | .Tile .Tile-Label { 27 | padding: 0; 28 | margin: 0; 29 | } 30 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Grid/Tile.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from "react"; 2 | import classnames from "classnames"; 3 | 4 | import {Heading} from '../Heading' 5 | 6 | import './Tile.css' 7 | 8 | export const Tile: React.FunctionComponent<{ label: string, selectTile: () => void, isSelected: boolean }> = ({label, selectTile, isSelected}): ReactElement => { 9 | const className = classnames("Tile", { 10 | "Tile--transparent": !label || label === "0", 11 | "Tile--selected": isSelected 12 | }) 13 | 14 | return ( 15 |
16 | {label} 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Grid/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from "react"; 2 | import {Tile} from "./Tile"; 3 | 4 | import './style.css' 5 | 6 | interface IGrid { 7 | rows: number[][], 8 | selectedTile: number[], 9 | selectTile: (tile: number[]) => void 10 | } 11 | 12 | export class Grid extends React.Component { 13 | renderTile = (label: string, key: string, selectTile: () => void, isSelected: boolean): ReactElement => { 14 | return ( 15 | 16 | ) 17 | } 18 | 19 | renderRow = (row: number[], rowIndex: number): ReactElement => { 20 | const [selectedTileRow, selectedTileColumn] = this.props.selectedTile 21 | 22 | return ( 23 |
24 | { 25 | row.map((number, columnIndex) => { 26 | const isSelected = selectedTileRow === rowIndex && selectedTileColumn === columnIndex 27 | const key = `${rowIndex}-${columnIndex}` 28 | const selectTile = () => this.props.selectTile([rowIndex, columnIndex]) 29 | 30 | return this.renderTile(String(number), key, selectTile, isSelected) 31 | }) 32 | } 33 |
34 | ) 35 | } 36 | 37 | render() { 38 | return ( 39 |
40 | {this.props.rows.map(this.renderRow)} 41 |
42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Grid/style.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | 4 | :root { 5 | --grid-border-color: var(--grey-color); 6 | --grid-background-color: var(--dark-color); 7 | --grid-width: calc(var(--base-size) * 70); 8 | } 9 | 10 | .Grid { 11 | border-width: var(--base-border-width); 12 | border-color: var(--grid-border-color); 13 | border-radius: var(--base-border-radius); 14 | border-style: solid; 15 | 16 | background: var(--grid-background-color); 17 | width: var(--grid-width); 18 | margin: auto; 19 | } 20 | 21 | .Grid .Row { 22 | display: flex; 23 | } 24 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react' 2 | 3 | export const Header: React.FunctionComponent<{ children: ReactNode }> = 4 | ({children}) =>
{children}
5 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Heading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames' 3 | 4 | import './style.css' 5 | 6 | export const Heading: React.FunctionComponent<{ children: string, level: number, className?: string }> = 7 | ({children, level, className}) => { 8 | const classes = classnames('Heading', { 9 | 'Heading--h1': level === 1, 10 | 'Heading--h2': level === 2, 11 | 'Heading--h3': level === 3, 12 | }, className) 13 | 14 | return React.createElement(`h${level}`, {className: classes, children}) 15 | } 16 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Heading/style.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | 4 | .Heading.Heading--h1 { 5 | font-size: var(--h1-font-size); 6 | letter-spacing: var(--h1-letter-spacing); 7 | } 8 | 9 | .Heading.Heading--h2 { 10 | font-size: var(--h2-font-size); 11 | letter-spacing: var(--h2-letter-spacing); 12 | } 13 | 14 | .Heading.Heading--h3 { 15 | font-size: var(--h3-font-size); 16 | letter-spacing: var(--h3-letter-spacing); 17 | } 18 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Section/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react' 2 | import './style.css' 3 | 4 | export const Section: React.FunctionComponent<{ children: ReactNode }> = 5 | ({children}) =>
{children}
6 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/Section/style.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | 4 | .Section { 5 | text-align: center; 6 | padding: calc(var(--base-size) * 4); 7 | } 8 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/YouWon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './style.css' 3 | import {Heading} from "../Heading"; 4 | 5 | export const YouWon: React.FunctionComponent = 6 | () => ( 7 |
8 | You won! 9 | Winning gif 12 |
13 | ) 14 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/YouWon/style.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | 4 | .YouWon .YouWon-Gif { 5 | max-width: calc(var(--base-size)*70) 6 | } 7 | -------------------------------------------------------------------------------- /2-memento/with-command/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export {Button} from './Button' 2 | export {Grid} from './Grid' 3 | export {Header} from './Header' 4 | export {Heading} from './Heading' 5 | export {Section} from './Section' 6 | export {YouWon} from './YouWon' 7 | -------------------------------------------------------------------------------- /2-memento/with-command/src/containers/CommandManager.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from "react"; 2 | import {ICommand} from "./KeyboardEventHandler/Command"; 3 | 4 | export interface ICommandManager { 5 | executeCommand: (command: ICommand) => void; 6 | save: (command: ICommand) => void, 7 | undo: () => void 8 | } 9 | 10 | interface ICommandManagerProps { 11 | children: (commandManager: ICommandManager, disableUndo: boolean) => ReactNode 12 | } 13 | 14 | interface ICommandManagerState { 15 | history: ICommand[] 16 | } 17 | 18 | // Invoker 19 | export class CommandManager extends React.Component { 20 | state = { 21 | history: [] 22 | } 23 | 24 | executeCommand = (command: ICommand) => { 25 | this.save(command) 26 | command.do() 27 | } 28 | 29 | save = (command: ICommand): void => { 30 | const {history} = this.state 31 | this.setState({history: [...history, command]}) 32 | } 33 | 34 | undo = (): void => { 35 | const {history} = this.state 36 | const command = history.pop() 37 | 38 | if (command) { 39 | // @ts-ignore 40 | command.undo() 41 | } 42 | 43 | this.setState(({history})) 44 | } 45 | 46 | render() { 47 | const {save, undo, executeCommand} = this 48 | const commandManager: ICommandManager = {save, undo, executeCommand} 49 | 50 | return this.props.children(commandManager, !this.state.history.length) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /2-memento/with-command/src/containers/Game.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react' 2 | import deepCopy from 'deep-copy' 3 | import {shuffle, chunk} from '../utils' 4 | import deepEqual from 'deep-equal' 5 | 6 | // Receiver 7 | interface IGameProps { 8 | children: ( 9 | rows: number[][], 10 | selectedTile: number[], 11 | isWinning: boolean, 12 | game: IGame 13 | ) => ReactNode 14 | } 15 | 16 | export interface IGame { 17 | selectTile: (tile: number[]) => void 18 | moveSelectedTile: (y: number, x: number) => void 19 | } 20 | 21 | export class Game extends React.Component implements IGame { 22 | static ALL_TILES = [1, 2, 3, 4, 5, 6, 7, 8, 0] 23 | static WINNING_COMBINATION = chunk(Game.ALL_TILES, 3) 24 | 25 | state = { 26 | rows: chunk(shuffle(Game.ALL_TILES), 3), 27 | selectedTile: [0, 0], 28 | isWinning: false 29 | } 30 | 31 | moveSelectedTile = (y: number, x: number): void => { 32 | const {selectedTile, rows} = this.state 33 | const [selectedTileY, selectedTileX] = selectedTile 34 | 35 | const [newSelectedTileY, newSelectedTileX] = [selectedTileY + y, selectedTileX + x] 36 | 37 | const isYWithinBoundaries = newSelectedTileY >= 0 && newSelectedTileY < rows.length 38 | const isXWithinBoundaries = newSelectedTileX >= 0 && newSelectedTileX < rows[0].length 39 | const isNextPositionReplaceable = rows[newSelectedTileY] && rows[newSelectedTileY][newSelectedTileX] === 0 40 | 41 | if (isYWithinBoundaries && isXWithinBoundaries && isNextPositionReplaceable) { 42 | const newRows: number[][] = deepCopy(rows) 43 | 44 | const aux = rows[newSelectedTileY][newSelectedTileX] 45 | newRows[newSelectedTileY][newSelectedTileX] = rows[selectedTileY][selectedTileX] 46 | newRows[selectedTileY][selectedTileX] = aux 47 | 48 | const isWinning = deepEqual(newRows, Game.WINNING_COMBINATION) 49 | 50 | this.setState({rows: newRows, selectedTile: [newSelectedTileY, newSelectedTileX], isWinning}) 51 | } 52 | } 53 | 54 | selectTile = (tile: number[]): void => { 55 | const [tileY, tileX] = tile 56 | 57 | // Don't select tile 0 58 | if (this.state.rows[tileY][tileX] !== 0) 59 | this.setState({selectedTile: tile}) 60 | } 61 | 62 | render() { 63 | const {rows, selectedTile, isWinning} = this.state 64 | const {selectTile, moveSelectedTile} = this 65 | const game: IGame = {selectTile, moveSelectedTile} 66 | 67 | return this.props.children(rows, selectedTile, isWinning, game) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /2-memento/with-command/src/containers/KeyboardEventHandler/Command.ts: -------------------------------------------------------------------------------- 1 | import {IGame} from "../Game"; 2 | 3 | export interface ICommand { 4 | do: () => void, 5 | undo: () => void 6 | } 7 | 8 | export class MoveUpCommand implements ICommand { 9 | game: IGame 10 | 11 | constructor(game: IGame) { 12 | this.game = game; 13 | } 14 | 15 | do() { 16 | this.game.moveSelectedTile(-1, 0) 17 | } 18 | 19 | undo() { 20 | this.game.moveSelectedTile(1, 0) 21 | } 22 | } 23 | 24 | export class MoveDownCommand implements ICommand { 25 | game: IGame 26 | 27 | constructor(game: IGame) { 28 | this.game = game; 29 | } 30 | 31 | do() { 32 | this.game.moveSelectedTile(1, 0) 33 | } 34 | 35 | undo() { 36 | this.game.moveSelectedTile(-1, 0) 37 | } 38 | } 39 | 40 | export class MoveRightCommand implements ICommand { 41 | game: IGame 42 | 43 | constructor(game: IGame) { 44 | this.game = game; 45 | } 46 | 47 | do() { 48 | this.game.moveSelectedTile(0, 1) 49 | } 50 | 51 | undo() { 52 | this.game.moveSelectedTile(0, -1) 53 | } 54 | } 55 | 56 | export class MoveLeftCommand implements ICommand { 57 | game: IGame 58 | 59 | constructor(game: IGame) { 60 | this.game = game; 61 | } 62 | 63 | do() { 64 | this.game.moveSelectedTile(0, -1) 65 | } 66 | 67 | undo() { 68 | this.game.moveSelectedTile(0, 1) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /2-memento/with-command/src/containers/KeyboardEventHandler/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {IGame} from "../Game"; 3 | import {ICommandManager} from '../CommandManager'; 4 | import {ICommand, MoveUpCommand, MoveDownCommand, MoveRightCommand, MoveLeftCommand} from './Command'; 5 | 6 | // Client 7 | interface IKeyboardEventHandlerProps { 8 | game: IGame, 9 | commandManager: ICommandManager, 10 | } 11 | 12 | export class KeyboardEventHandler extends React.Component { 13 | static ARROW_EVENT_KEY = { 14 | UP: 'ArrowUp', 15 | DOWN: 'ArrowDown', 16 | LEFT: 'ArrowLeft', 17 | RIGHT: 'ArrowRight' 18 | } 19 | 20 | createCommand = (event: KeyboardEvent): ICommand => { 21 | const {game} = this.props 22 | 23 | switch (event.key) { 24 | case KeyboardEventHandler.ARROW_EVENT_KEY.UP: 25 | return new MoveUpCommand(game) 26 | case KeyboardEventHandler.ARROW_EVENT_KEY.DOWN: 27 | return new MoveDownCommand(game) 28 | case KeyboardEventHandler.ARROW_EVENT_KEY.RIGHT: 29 | return new MoveRightCommand(game) 30 | case KeyboardEventHandler.ARROW_EVENT_KEY.LEFT: 31 | default: 32 | return new MoveLeftCommand(game) 33 | } 34 | } 35 | 36 | handleKeyDown = (event: KeyboardEvent): void => { 37 | const command = this.createCommand(event) 38 | this.props.commandManager.executeCommand(command) 39 | } 40 | 41 | componentDidMount() { 42 | document.addEventListener("keydown", this.handleKeyDown) 43 | } 44 | 45 | componentWillUnmount() { 46 | document.removeEventListener("keydown", this.handleKeyDown) 47 | } 48 | 49 | render() { 50 | return null 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /2-memento/with-command/src/index.css: -------------------------------------------------------------------------------- 1 | @import "./variables.css"; 2 | 3 | html { 4 | font-size: var(--base-font-size); 5 | } 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 11 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 12 | sans-serif; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | font-size: var(--base-font-size); 16 | } 17 | -------------------------------------------------------------------------------- /2-memento/with-command/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /2-memento/with-command/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /2-memento/with-command/src/theme.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #3f51b5; 3 | --accent-color: #03A9F4; 4 | --light-color: white; 5 | --dark-color: black; 6 | --grey-color: #E0E0E0; 7 | } 8 | -------------------------------------------------------------------------------- /2-memento/with-command/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const shuffle = (a: Array): Array => { 2 | for (let i = a.length - 1; i > 0; i--) { 3 | const j = Math.floor(Math.random() * (i + 1)); 4 | [a[i], a[j]] = [a[j], a[i]]; 5 | } 6 | return a; 7 | } 8 | 9 | export const chunk = (a: Array, chunkSize: number): Array> => { 10 | return a.reduce((accumulator: Array>, item: T, index: number) => { 11 | const chunkIndex = Math.floor(index / chunkSize) 12 | 13 | if (!accumulator[chunkIndex]) { 14 | accumulator[chunkIndex] = [] // start a new chunk 15 | } 16 | 17 | accumulator[chunkIndex].push(item) 18 | 19 | return accumulator 20 | }, []) 21 | } 22 | -------------------------------------------------------------------------------- /2-memento/with-command/src/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --base-size: 4px; 3 | 4 | 5 | /* TYPOGRAPHY */ 6 | --base-font-size: calc(var(--base-size) * 2); 7 | 8 | --h1-font-size: 6rem; 9 | --h1-letter-spacing: -1.5; 10 | 11 | --h2-font-size: 3.75rem; 12 | --h2-letter-spacing: -0.5; 13 | 14 | --h3-font-size: 3rem; 15 | --h3-letter-spacing: 0; 16 | 17 | /* BORDER */ 18 | --base-border-width: calc(var(--base-size) / 2); 19 | --base-border-radius: calc(var(--base-size) * 2); 20 | } 21 | -------------------------------------------------------------------------------- /2-memento/with-command/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "allowJs": true, 5 | "skipLibCheck": false, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": [ 18 | "src" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /2-memento/with-memento/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /2-memento/with-memento/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-command", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/classnames": "^2.2.6", 7 | "@types/deep-equal": "^1.0.1", 8 | "@types/jest": "23.3.9", 9 | "@types/node": "10.12.10", 10 | "@types/react": "16.7.7", 11 | "@types/react-dom": "16.0.10", 12 | "classnames": "^2.2.6", 13 | "deep-copy": "^1.4.2", 14 | "react": "^16.6.3", 15 | "react-dom": "^16.6.3", 16 | "react-scripts": "2.1.1", 17 | "typescript": "3.1.6" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /2-memento/with-memento/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shikaan/design-patterns/993d72339d2597ef512502b57b1305c1970d820f/2-memento/with-memento/public/favicon.ico -------------------------------------------------------------------------------- /2-memento/with-memento/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /2-memento/with-memento/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Grid, Header, Heading, Section, YouWon} from "./components"; 3 | import {Game, ISnapshot} from "./containers/Game"; 4 | import {KeyboardEventHandler} from "./containers/KeyboardEventHandler"; 5 | 6 | const renderGameChildren = ( 7 | rows: number[][], 8 | selectedTile: number[], 9 | isWinning: boolean, 10 | selectTile: (tile: number[]) => void, 11 | moveSelectedTile: (y: number, x: number) => void, 12 | takeSnapshot: () => ISnapshot, 13 | restoreSnapshot: (snapshot: ISnapshot) => void 14 | ) => { 15 | if (isWinning) { 16 | return 17 | } 18 | 19 | return ( 20 | 24 | {renderKeyboardEventHandlerChildren(rows, selectedTile, isWinning, selectTile)} 25 | 26 | ) 27 | } 28 | 29 | const renderKeyboardEventHandlerChildren = ( 30 | rows: number[][], 31 | selectedTile: number[], 32 | isWinning: boolean, 33 | selectTile: (tile: number[]) => void 34 | ) => (undo: () => void, disableUndo: boolean) => ( 35 | 36 | 37 | 38 | 39 | ) 40 | 41 | const App = () => { 42 | return ( 43 |
44 |
45 | Sort the tiles 46 | Click on a cell and use keyboard arrows to move the tiles 47 |
48 | {renderGameChildren} 49 |
50 | ) 51 | } 52 | 53 | 54 | export default App; 55 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames' 3 | import './style.css' 4 | 5 | export const Button: React.FunctionComponent<{ children: string, className?: string, disabled?: boolean, onClick: () => void, primary?: boolean }> = 6 | ({children, className, disabled, onClick, primary}) => { 7 | const classes = classnames('Button', { 8 | 'Button--primary': primary 9 | }) 10 | 11 | return ( 12 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Button/style.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | 4 | .Button { 5 | background: var(--light-color); 6 | border-width: var(--base-border-width); 7 | border-radius: var(--base-border-radius); 8 | box-shadow: none; 9 | margin: var(--base-size); 10 | padding: calc(var(--base-size) * 4) calc(var(--base-size) * 8); 11 | 12 | font-size: var(--h3-font-size); 13 | letter-spacing: var(--h3-letter-spacing); 14 | } 15 | 16 | .Button:disabled { 17 | opacity: .3; 18 | } 19 | 20 | .Button.Button--primary { 21 | background-color: var(--primary-color); 22 | color: var(--light-color); 23 | } 24 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Grid/Tile.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | @import "./style.css"; 4 | 5 | .Tile { 6 | --height: calc(var(--grid-width) / 3); 7 | 8 | background: var(--primary-color); 9 | color: var(--light-color); 10 | flex: 1; 11 | text-align: center; 12 | cursor: pointer; 13 | 14 | line-height: var(--height); 15 | height: var(--height); 16 | } 17 | 18 | .Tile.Tile--transparent { 19 | opacity: 0; 20 | } 21 | 22 | .Tile.Tile--selected { 23 | background: var(--accent-color); 24 | } 25 | 26 | .Tile .Tile-Label { 27 | padding: 0; 28 | margin: 0; 29 | } 30 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Grid/Tile.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from "react"; 2 | import classnames from "classnames"; 3 | 4 | import {Heading} from '../Heading' 5 | 6 | import './Tile.css' 7 | 8 | export const Tile: React.FunctionComponent<{ label: string, selectTile: () => void, isSelected: boolean }> = ({label, selectTile, isSelected}): ReactElement => { 9 | const className = classnames("Tile", { 10 | "Tile--transparent": !label || label === "0", 11 | "Tile--selected": isSelected 12 | }) 13 | 14 | return ( 15 |
16 | {label} 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Grid/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from "react"; 2 | import {Tile} from "./Tile"; 3 | 4 | import './style.css' 5 | 6 | interface IGrid { 7 | rows: number[][], 8 | selectedTile: number[], 9 | selectTile: (tile: number[]) => void 10 | } 11 | 12 | export class Grid extends React.Component { 13 | renderTile = (label: string, key: string, selectTile: () => void, isSelected: boolean): ReactElement => { 14 | return ( 15 | 16 | ) 17 | } 18 | 19 | renderRow = (row: number[], rowIndex: number): ReactElement => { 20 | const [selectedTileRow, selectedTileColumn] = this.props.selectedTile 21 | 22 | return ( 23 |
24 | { 25 | row.map((number, columnIndex) => { 26 | const isSelected = selectedTileRow === rowIndex && selectedTileColumn === columnIndex 27 | const key = `${rowIndex}-${columnIndex}` 28 | const selectTile = () => this.props.selectTile([rowIndex, columnIndex]) 29 | 30 | return this.renderTile(String(number), key, selectTile, isSelected) 31 | }) 32 | } 33 |
34 | ) 35 | } 36 | 37 | render() { 38 | return ( 39 |
40 | {this.props.rows.map(this.renderRow)} 41 |
42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Grid/style.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | 4 | :root { 5 | --grid-border-color: var(--grey-color); 6 | --grid-background-color: var(--dark-color); 7 | --grid-width: calc(var(--base-size) * 70); 8 | } 9 | 10 | .Grid { 11 | border-width: var(--base-border-width); 12 | border-color: var(--grid-border-color); 13 | border-radius: var(--base-border-radius); 14 | border-style: solid; 15 | 16 | background: var(--grid-background-color); 17 | width: var(--grid-width); 18 | margin: auto; 19 | } 20 | 21 | .Grid .Row { 22 | display: flex; 23 | } 24 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react' 2 | 3 | export const Header: React.FunctionComponent<{ children: ReactNode }> = 4 | ({children}) =>
{children}
5 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Heading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames' 3 | 4 | import './style.css' 5 | 6 | export const Heading: React.FunctionComponent<{ children: string, level: number, className?: string }> = 7 | ({children, level, className}) => { 8 | const classes = classnames('Heading', { 9 | 'Heading--h1': level === 1, 10 | 'Heading--h2': level === 2, 11 | 'Heading--h3': level === 3, 12 | }, className) 13 | 14 | return React.createElement(`h${level}`, {className: classes, children}) 15 | } 16 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Heading/style.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | 4 | .Heading.Heading--h1 { 5 | font-size: var(--h1-font-size); 6 | letter-spacing: var(--h1-letter-spacing); 7 | } 8 | 9 | .Heading.Heading--h2 { 10 | font-size: var(--h2-font-size); 11 | letter-spacing: var(--h2-letter-spacing); 12 | } 13 | 14 | .Heading.Heading--h3 { 15 | font-size: var(--h3-font-size); 16 | letter-spacing: var(--h3-letter-spacing); 17 | } 18 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Section/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react' 2 | import './style.css' 3 | 4 | export const Section: React.FunctionComponent<{ children: ReactNode }> = 5 | ({children}) =>
{children}
6 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/Section/style.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | 4 | .Section { 5 | text-align: center; 6 | padding: calc(var(--base-size) * 4); 7 | } 8 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/YouWon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './style.css' 3 | import {Heading} from "../Heading"; 4 | 5 | export const YouWon: React.FunctionComponent = 6 | () => ( 7 |
8 | You won! 9 | Winning gif 12 |
13 | ) 14 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/YouWon/style.css: -------------------------------------------------------------------------------- 1 | @import "../../theme.css"; 2 | @import "../../variables.css"; 3 | 4 | .YouWon .YouWon-Gif { 5 | max-width: calc(var(--base-size)*70) 6 | } 7 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export {Button} from './Button' 2 | export {Grid} from './Grid' 3 | export {Header} from './Header' 4 | export {Heading} from './Heading' 5 | export {Section} from './Section' 6 | export {YouWon} from './YouWon' 7 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/containers/Game.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react' 2 | import deepCopy from 'deep-copy' 3 | import {shuffle, chunk} from '../utils' 4 | import deepEqual from 'deep-equal' 5 | 6 | // Memento 7 | export interface ISnapshot { 8 | getState(): { rows: number[][], selectedTile: number[] } 9 | } 10 | 11 | class Snapshot implements ISnapshot { 12 | private rows: number[][]; 13 | private selectedTile: number[]; 14 | 15 | constructor(rows: number[][], selectedTile: number[]) { 16 | this.rows = rows 17 | this.selectedTile = selectedTile 18 | 19 | Object.freeze(this) 20 | } 21 | 22 | getState() { 23 | const {rows, selectedTile} = this 24 | return {rows, selectedTile} 25 | } 26 | } 27 | 28 | // Originator 29 | interface IGame { 30 | children: ( 31 | rows: number[][], 32 | selectedTile: number[], 33 | isWinning: boolean, 34 | selectTile: (tile: number[]) => void, 35 | moveSelectedTile: (y: number, x: number) => void, 36 | takeSnapshot: () => ISnapshot, 37 | restoreSnapshot: (snapshot: ISnapshot) => void 38 | ) => ReactNode 39 | } 40 | 41 | export class Game extends React.Component { 42 | static ALL_TILES = [1, 2, 3, 4, 5, 6, 7, 8, 0] 43 | static WINNING_COMBINATION = chunk(Game.ALL_TILES, 3) 44 | 45 | state = { 46 | rows: chunk(shuffle(Game.ALL_TILES), 3), 47 | selectedTile: [0, 0], 48 | isWinning: false 49 | } 50 | 51 | moveSelectedTile = (y: number, x: number): void => { 52 | const {selectedTile, rows} = this.state 53 | const [selectedTileY, selectedTileX] = selectedTile 54 | 55 | const [newSelectedTileY, newSelectedTileX] = [selectedTileY + y, selectedTileX + x] 56 | 57 | const isYWithinBoundaries = newSelectedTileY >= 0 && newSelectedTileY < rows.length 58 | const isXWithinBoundaries = newSelectedTileX >= 0 && newSelectedTileX < rows[0].length 59 | const isNextPositionReplaceable = rows[newSelectedTileY] && rows[newSelectedTileY][newSelectedTileX] === 0 60 | 61 | if (isYWithinBoundaries && isXWithinBoundaries && isNextPositionReplaceable) { 62 | const newRows: number[][] = deepCopy(rows) 63 | 64 | const aux = rows[newSelectedTileY][newSelectedTileX] 65 | newRows[newSelectedTileY][newSelectedTileX] = rows[selectedTileY][selectedTileX] 66 | newRows[selectedTileY][selectedTileX] = aux 67 | 68 | const isWinning = deepEqual(newRows, Game.WINNING_COMBINATION) 69 | 70 | this.setState({rows: newRows, selectedTile: [newSelectedTileY, newSelectedTileX], isWinning}) 71 | } 72 | } 73 | 74 | selectTile = (tile: number[]): void => { 75 | const [tileY, tileX] = tile 76 | 77 | // Don't select tile 0 78 | if (this.state.rows[tileY][tileX] !== 0) 79 | this.setState({selectedTile: tile}) 80 | } 81 | 82 | takeSnapshot = (): ISnapshot => { 83 | const state = deepCopy(this.state) 84 | 85 | return new Snapshot(state.rows, state.selectedTile) 86 | } 87 | 88 | restoreSnapshot = (snapshot: ISnapshot): void => { 89 | this.setState(snapshot.getState()) 90 | } 91 | 92 | render() { 93 | const {rows, selectedTile, isWinning} = this.state 94 | 95 | return this.props.children( 96 | rows, 97 | selectedTile, 98 | isWinning, 99 | this.selectTile, 100 | this.moveSelectedTile, 101 | this.takeSnapshot, 102 | this.restoreSnapshot 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/containers/KeyboardEventHandler.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | import deepEqual from 'deep-equal'; 3 | 4 | import {ISnapshot} from "./Game"; 5 | 6 | // Caretaker 7 | interface IKeyboardEventHandler { 8 | takeSnapshot: () => T, 9 | restoreSnapshot: (snapshot: T) => void, 10 | moveSelectedTile: (y: number, x: number) => void, 11 | children: (undo: () => void, disableUndo: boolean) => ReactNode 12 | } 13 | 14 | export class KeyboardEventHandler extends React.Component> { 15 | static ARROW_EVENT_KEY = { 16 | UP: 'ArrowUp', 17 | DOWN: 'ArrowDown', 18 | LEFT: 'ArrowLeft', 19 | RIGHT: 'ArrowRight' 20 | } 21 | 22 | state = { 23 | history: [] 24 | } 25 | 26 | handleKeyDown = (event: KeyboardEvent): void => { 27 | this.save() 28 | 29 | switch (event.key) { 30 | case KeyboardEventHandler.ARROW_EVENT_KEY.UP: 31 | return this.props.moveSelectedTile(-1, 0) 32 | case KeyboardEventHandler.ARROW_EVENT_KEY.DOWN: 33 | return this.props.moveSelectedTile(1, 0) 34 | case KeyboardEventHandler.ARROW_EVENT_KEY.RIGHT: 35 | return this.props.moveSelectedTile(0, 1) 36 | case KeyboardEventHandler.ARROW_EVENT_KEY.LEFT: 37 | return this.props.moveSelectedTile(0, -1) 38 | default: 39 | return 40 | } 41 | } 42 | 43 | save = (): void => { 44 | const {history} = this.state 45 | const entry = this.props.takeSnapshot() 46 | 47 | const lastHistoryEntry: ISnapshot = history[history.length - 1] 48 | 49 | // TODO: prevent dummy history entry in case of no-action movement as first event 50 | if (lastHistoryEntry) { 51 | const lastHistoryEntryState = lastHistoryEntry.getState() 52 | const currentEntryState = entry.getState() 53 | 54 | if (!deepEqual(lastHistoryEntryState.rows, currentEntryState.rows)) { 55 | this.setState({history: [...history, entry]}) 56 | } 57 | } else { 58 | this.setState({history: [...history, entry]}) 59 | } 60 | } 61 | 62 | undo = (): void => { 63 | const {history} = this.state 64 | const lastHistoryEntry = history.pop() 65 | 66 | if (lastHistoryEntry) { 67 | this.props.restoreSnapshot(lastHistoryEntry) 68 | this.setState({history}) 69 | } 70 | } 71 | 72 | componentDidMount() { 73 | document.addEventListener("keydown", this.handleKeyDown) 74 | } 75 | 76 | componentWillUnmount() { 77 | document.removeEventListener("keydown", this.handleKeyDown) 78 | } 79 | 80 | render() { 81 | return this.props.children(this.undo, !this.state.history.length) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/index.css: -------------------------------------------------------------------------------- 1 | @import "./variables.css"; 2 | 3 | html { 4 | font-size: var(--base-font-size); 5 | } 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 11 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 12 | sans-serif; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | font-size: var(--base-font-size); 16 | } 17 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/theme.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #3f51b5; 3 | --accent-color: #03A9F4; 4 | --light-color: white; 5 | --dark-color: black; 6 | --grey-color: #E0E0E0; 7 | } 8 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const shuffle = (a: Array): Array => { 2 | for (let i = a.length - 1; i > 0; i--) { 3 | const j = Math.floor(Math.random() * (i + 1)); 4 | [a[i], a[j]] = [a[j], a[i]]; 5 | } 6 | return a; 7 | } 8 | 9 | export const chunk = (a: Array, chunkSize: number): Array> => { 10 | return a.reduce((accumulator: Array>, item: T, index: number) => { 11 | const chunkIndex = Math.floor(index / chunkSize) 12 | 13 | if (!accumulator[chunkIndex]) { 14 | accumulator[chunkIndex] = [] // start a new chunk 15 | } 16 | 17 | accumulator[chunkIndex].push(item) 18 | 19 | return accumulator 20 | }, []) 21 | } 22 | -------------------------------------------------------------------------------- /2-memento/with-memento/src/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --base-size: 4px; 3 | 4 | 5 | /* TYPOGRAPHY */ 6 | --base-font-size: calc(var(--base-size) * 2); 7 | 8 | --h1-font-size: 6rem; 9 | --h1-letter-spacing: -1.5; 10 | 11 | --h2-font-size: 3.75rem; 12 | --h2-letter-spacing: -0.5; 13 | 14 | --h3-font-size: 3rem; 15 | --h3-letter-spacing: 0; 16 | 17 | /* BORDER */ 18 | --base-border-width: calc(var(--base-size) / 2); 19 | --base-border-radius: calc(var(--base-size) * 2); 20 | } 21 | -------------------------------------------------------------------------------- /2-memento/with-memento/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "allowJs": true, 5 | "skipLibCheck": false, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": [ 18 | "src" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # design-patterns 2 | Examples of usage of design patterns in real life code 3 | 4 | These are the reference resources for [this series of articles](https://dev.to/shikaan/design-patterns-in-web-development-2gbp) 5 | -------------------------------------------------------------------------------- /chistmas-caching/README.md: -------------------------------------------------------------------------------- 1 | Caching Patterns 2 | === 3 | 4 | This sub-repo is divided into folders, one per episode: 5 | 6 | - [`reading`](./reading) 7 | - [`writing`](./writing) 8 | 9 | ## Reading 10 | 11 | In the reading part you can find a simple example of an application fetching 12 | comics from XKCD. 13 | 14 | Since the only changing component between inline cache and cache aside was 15 | the client, I just added two different clients. 16 | 17 | To run the the applications 18 | 19 | ``` 20 | npm run start:inline 21 | ``` 22 | 23 | ``` 24 | npm run start:aside 25 | ``` 26 | 27 | You can launch tests with 28 | 29 | ``` 30 | npm test 31 | ``` 32 | 33 | ## Writing 34 | 35 | In the writing part the implementation is done in Python in which we are 36 | faking long operation using timers. 37 | 38 | The easiest way to launch the project is using [`invoke`](http://www.pyinvoke.org/). 39 | 40 | ``` 41 | invoke init 42 | invoke install 43 | invoke start 44 | ``` 45 | 46 | If you want to launch tests you can run 47 | 48 | ``` 49 | invoke test 50 | ``` -------------------------------------------------------------------------------- /chistmas-caching/reading/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inline-cache", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start:inline": "node ./src/inline-cache-client.js", 8 | "start:aside": "node ./src/aside-cache-client.js", 9 | "test": "titef ./test" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "phin": "^3.2.0" 15 | }, 16 | "devDependencies": { 17 | "sinon": "^7.2.2", 18 | "titef": "^2.1.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /chistmas-caching/reading/src/aside-cache-client.js: -------------------------------------------------------------------------------- 1 | const XKCDClient = require('./data-access-component') 2 | const CacheManager = require('./cache-manager') 3 | 4 | class Application { 5 | constructor() { 6 | this.cacheManager = new CacheManager() 7 | this.xkcdClient = new XKCDClient() 8 | } 9 | 10 | async main() { 11 | console.log(`Getting last 5 XKCD comics from XKCD API...`) 12 | console.time('XKCD API') 13 | 14 | const result = await this.xkcdClient.getLastComics(5) 15 | this.cacheManager.set(5, result) 16 | 17 | console.timeEnd('XKCD API') 18 | 19 | console.log('Result:\n', result) 20 | 21 | console.log('Getting last 2 XKCD comics using Cache...') 22 | console.time('Cache') 23 | 24 | const newResult = (await this.cacheManager.get(5)).slice(0, 2) 25 | 26 | console.timeEnd('Cache') 27 | 28 | console.log('Result:\n', newResult) 29 | } 30 | } 31 | 32 | 33 | (new Application()).main() 34 | -------------------------------------------------------------------------------- /chistmas-caching/reading/src/cache-manager.js: -------------------------------------------------------------------------------- 1 | const CACHE_LAYER = new Map() 2 | 3 | class CacheManager { 4 | /** 5 | * Gets cached entry associated with key, Returns null if missing. 6 | * 7 | * @param {string} key 8 | * @returns {any|null} 9 | */ 10 | get(key) { 11 | if (!CACHE_LAYER.has(key)) { 12 | return null 13 | } 14 | 15 | return CACHE_LAYER.get(key) 16 | } 17 | 18 | /** 19 | * Saves a key in cache 20 | * 21 | * @param {string} key 22 | * @param {any} entry 23 | * @param {number} [expiration=0] - in seconds 24 | */ 25 | set(key, entry, expiration = 0) { 26 | if (!key) { 27 | throw new RangeError(`Cannot set entry without a key. Expected a key, got ${key}`) 28 | } 29 | 30 | CACHE_LAYER.set(key, entry) 31 | 32 | if (expiration) { 33 | setTimeout(() => { 34 | CACHE_LAYER.delete(key) 35 | }, expiration * 1000) 36 | } 37 | } 38 | } 39 | 40 | module.exports = CacheManager -------------------------------------------------------------------------------- /chistmas-caching/reading/src/data-access-component.js: -------------------------------------------------------------------------------- 1 | const { URL } = require('url') 2 | const p = require('phin') 3 | 4 | class XKCDClient { 5 | constructor(httpClient = p) { 6 | this.httpClient = httpClient 7 | this.baseURL = 'https://xkcd.com' 8 | } 9 | 10 | /** 11 | * Creates URL object for the XKCD comic identified by the number 12 | * 13 | * @private 14 | * @param {number} number 15 | * @returns {URL} 16 | */ 17 | buildXKCDComicsURL(number = 0) { 18 | const path = number > 0 ? `/${number}/info.0.json` : '/info.0.json' 19 | return new URL(path, this.baseURL) 20 | } 21 | 22 | /** 23 | * Performs an HTTP Get call and returns response body 24 | * 25 | * @private 26 | * @param {URL} url 27 | * @return {Promise} 28 | */ 29 | async httpGet(url) { 30 | const { body } = await this.httpClient({ url: url.href, parse: 'json' }) 31 | 32 | return body 33 | } 34 | 35 | /** 36 | * Get last issued comic 37 | * 38 | * @private 39 | * @return {Promise} 40 | */ 41 | getLastComic() { 42 | const url = this.buildXKCDComicsURL() 43 | 44 | return this.httpGet(url) 45 | } 46 | 47 | async getLastComics(amount = 10) { 48 | const firstRequest = this.getLastComic() 49 | const { num: firstNumber } = await firstRequest 50 | 51 | const requests = [firstRequest] 52 | 53 | for (let i = 1; i < amount; i++) { 54 | const url = this.buildXKCDComicsURL(firstNumber - i) 55 | 56 | requests.push(this.httpGet(url)) 57 | } 58 | 59 | return Promise.all(requests) 60 | } 61 | } 62 | 63 | module.exports = XKCDClient -------------------------------------------------------------------------------- /chistmas-caching/reading/src/inline-cache-client.js: -------------------------------------------------------------------------------- 1 | const XKCDClient = require('./data-access-component') 2 | const ResourceManager = require('./resource-manager') 3 | const CacheManager = require('./cache-manager') 4 | 5 | class Application { 6 | constructor() { 7 | const cacheManager = new CacheManager() 8 | const xkcdClient = new XKCDClient() 9 | this.resourceManager = new ResourceManager(cacheManager, xkcdClient) 10 | } 11 | 12 | 13 | async main() { 14 | console.log('Getting last 3 XKCD comics from XKCD API...') 15 | console.time('XKCD API') 16 | 17 | console.log(await this.resourceManager.getLastComics(3)) 18 | 19 | console.timeEnd('XKCD API') 20 | 21 | console.log('Getting last 3 XKCD comics from Cache...') 22 | console.time('Cache') 23 | 24 | console.log(await this.resourceManager.getLastComics(3)) 25 | 26 | console.timeEnd('Cache') 27 | } 28 | } 29 | 30 | 31 | (new Application()).main() 32 | -------------------------------------------------------------------------------- /chistmas-caching/reading/src/resource-manager.js: -------------------------------------------------------------------------------- 1 | class ResourceManager { 2 | constructor(cacheManager, xkcdClient) { 3 | this.cacheManager = cacheManager 4 | this.xkcdClient = xkcdClient 5 | } 6 | 7 | buildCacheKey(numberOfComics) { 8 | return `${numberOfComics}.LAST_COMICS` 9 | } 10 | 11 | async getLastComics(amount = 1) { 12 | const cacheKey = this.buildCacheKey(amount) 13 | const cacheValue = this.cacheManager.get(cacheKey) 14 | 15 | if (cacheValue) { 16 | return cacheValue 17 | } 18 | 19 | const result = await this.xkcdClient.getLastComics(amount) 20 | this.cacheManager.set(cacheKey, result) 21 | 22 | return result 23 | } 24 | } 25 | 26 | module.exports = ResourceManager -------------------------------------------------------------------------------- /chistmas-caching/reading/test/cache-manager.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const CacheManager = require('../src/cache-manager') 3 | 4 | suite('CacheManager', () => { 5 | suite('set', () => { 6 | spec('throws if missing key', () => { 7 | const cacheManager = new CacheManager() 8 | assert.throws(() => { 9 | cacheManager.set(null, 10) 10 | }) 11 | }) 12 | 13 | spec('saves the entry', () => { 14 | const key = 'key' 15 | const value = 'value' 16 | const cacheManager = new CacheManager() 17 | cacheManager.set(key, value) 18 | 19 | assert.deepEqual(cacheManager.get(key), value) 20 | }) 21 | 22 | spec('saves the entry with expiration', async () => { 23 | const key = 'key' 24 | const value = 'value' 25 | const expiration = 2 26 | const cacheManager = new CacheManager() 27 | cacheManager.set(key, value, expiration) 28 | 29 | assert.deepEqual(cacheManager.get(key), value) 30 | 31 | await setTimeout(() => { 32 | assert.deepEqual(cacheManager.get(key), null) 33 | }, expiration * 1000) 34 | }) 35 | }) 36 | 37 | suite('get', () => { 38 | spec('returns null if key is not existing', () => { 39 | const cacheManager = new CacheManager() 40 | 41 | const result = cacheManager.get('idonotexist') 42 | 43 | assert.deepEqual(result, null) 44 | }) 45 | spec('returns null if key is missing', () => { 46 | const cacheManager = new CacheManager() 47 | 48 | const result = cacheManager.get() 49 | 50 | assert.deepEqual(result, null) 51 | }) 52 | spec('returns cached value for key', () => { 53 | const cacheManager = new CacheManager() 54 | const key = 'key' 55 | const value = 'value' 56 | cacheManager.set(key, value) 57 | 58 | const result = cacheManager.get(key) 59 | 60 | assert.deepEqual(result, value) 61 | }) 62 | }) 63 | }) -------------------------------------------------------------------------------- /chistmas-caching/reading/test/data-access-component.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { spy } = require('sinon') 3 | const XKCDClient = require('../src/data-access-component') 4 | 5 | suite('Data Access Component', () => { 6 | suite('buildXKCDComicsURL', () => { 7 | spec('builds last comic URL with no params', () => { 8 | const xkcdClient = new XKCDClient() 9 | const result = xkcdClient.buildXKCDComicsURL() 10 | 11 | assert.ok(result instanceof URL, 'result not an instance of URL') 12 | assert.ok(result.pathname.includes('info.0.json'), 'missing `info.0.json` fragment') 13 | assert.ok(!(/\d{2,}/.test(result.pathname)), 'result includes more than a number') 14 | }) 15 | 16 | spec('builds last comic URL with zero param', () => { 17 | const xkcdClient = new XKCDClient() 18 | const result = xkcdClient.buildXKCDComicsURL(0) 19 | 20 | assert.ok(result instanceof URL, 'result not an instance of URL') 21 | assert.ok(result.pathname.includes('info.0.json'), 'missing `info.0.json` fragment') 22 | assert.ok(!(/\d{2,}/.test(result.pathname)), 'result includes more than a number') 23 | }) 24 | 25 | spec('builds #number comic with #number param', () => { 26 | const number = 120 27 | const xkcdClient = new XKCDClient() 28 | 29 | const result = xkcdClient.buildXKCDComicsURL(number) 30 | 31 | assert.ok(result instanceof URL, 'result not an instance of URL') 32 | assert.ok(result.pathname.includes('info.0.json'), 'missing `info.0.json` fragment') 33 | assert.ok(result.pathname.includes(number), 'result does not include number') 34 | }) 35 | }) 36 | 37 | suite('httpGet', () => { 38 | spec('calls httpClient', () => { 39 | const urlString = 'http://www.google.com/' 40 | const httpClientSpy = spy(async () => ({ body: 'whatever' })) 41 | const xkcdClient = new XKCDClient(httpClientSpy) 42 | 43 | xkcdClient.httpGet(new URL(urlString)) 44 | 45 | assert.ok(httpClientSpy.called) 46 | assert.deepEqual(httpClientSpy.args[0][0], { url: urlString, parse: 'json' }) 47 | }) 48 | 49 | spec('returns body', async () => { 50 | const urlString = 'http://www.google.com/' 51 | const body = { foo: 'bar' } 52 | const httpClientSpy = spy(async () => ({ body })) 53 | const xkcdClient = new XKCDClient(httpClientSpy) 54 | 55 | const result = await xkcdClient.httpGet(new URL(urlString)) 56 | 57 | assert.deepEqual(result, body) 58 | }) 59 | }) 60 | 61 | suite('getLastComics', () => { 62 | spec('returns a list of right length', async () => { 63 | const length = 10 64 | 65 | const httpClientSpy = spy(async () => ({ body: new Date() })) 66 | 67 | const xkcdClient = new XKCDClient(httpClientSpy) 68 | const result = await xkcdClient.getLastComics(length) 69 | 70 | assert.equal(result.length, length) 71 | }) 72 | }) 73 | }) -------------------------------------------------------------------------------- /chistmas-caching/reading/test/index.js: -------------------------------------------------------------------------------- 1 | require('./data-access-component.test') 2 | require('./cache-manager.test') 3 | require('./resource-manager.test') -------------------------------------------------------------------------------- /chistmas-caching/reading/test/resource-manager.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { spy } = require('sinon') 3 | 4 | const ResourceManager = require('../src/resource-manager') 5 | 6 | const getCacheManagerStub = () => { 7 | return { 8 | get: spy(), 9 | set: spy() 10 | } 11 | } 12 | 13 | const getXKCDClientStub = () => { 14 | return { 15 | getLastComics: spy() 16 | } 17 | } 18 | 19 | suite('Resource Manager', () => { 20 | suite('getLastComics', () => { 21 | spec('fetch entries from XKCDClient if resource is not cached', async () => { 22 | const cacheManagerStub = getCacheManagerStub() 23 | const xkcdClientStub = getXKCDClientStub() 24 | const resourceManager = new ResourceManager(cacheManagerStub, xkcdClientStub) 25 | 26 | await resourceManager.getLastComics(1) 27 | 28 | assert.ok(cacheManagerStub.get.called) 29 | assert.ok(cacheManagerStub.set.called) 30 | assert.ok(xkcdClientStub.getLastComics.called) 31 | }) 32 | 33 | spec('fetch entries from CacheManager if resource is cached',() => { 34 | const cacheManagerStub = getCacheManagerStub() 35 | cacheManagerStub.get = spy(() => 'not-null') 36 | 37 | const xkcdClientStub = getXKCDClientStub() 38 | const resourceManager = new ResourceManager(cacheManagerStub, xkcdClientStub) 39 | 40 | resourceManager.getLastComics(1) 41 | 42 | assert.ok(cacheManagerStub.get.called) 43 | assert.ok(!cacheManagerStub.set.called) 44 | assert.ok(!xkcdClientStub.getLastComics.called) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /chistmas-caching/writing/requirements.txt: -------------------------------------------------------------------------------- 1 | atomicwrites==1.2.1 2 | attrs==18.2.0 3 | more-itertools==4.3.0 4 | pluggy==0.8.0 5 | py==1.7.0 6 | pytest==4.0.2 7 | pytest-asyncio==0.9.0 8 | six==1.12.0 9 | -------------------------------------------------------------------------------- /chistmas-caching/writing/src/cache_manager.py: -------------------------------------------------------------------------------- 1 | from threading import Timer 2 | from utils import trace 3 | 4 | class CacheManager: 5 | def __init__(self): 6 | self.cache = {} 7 | 8 | @trace 9 | def set(self, key, value, expiration=0): 10 | if not key: 11 | raise ReferenceError("Cannot set entry without a key. Expected a key, got None" + str(key)) 12 | 13 | self.cache[key] = value 14 | 15 | if expiration: 16 | timeout = Timer(expiration, self.__delete, [key]) 17 | timeout.start() 18 | 19 | @trace 20 | def get(self, key): 21 | if not key: 22 | raise ReferenceError("Cannot get entry without a key. Expected a key, got " + str(key)) 23 | 24 | if key not in self.cache.keys(): 25 | return None 26 | 27 | return self.cache[key] 28 | 29 | def __delete(self, key): 30 | if not key: 31 | raise ReferenceError("Cannot delete entry without a key. Expected a key, got " + str(key)) 32 | 33 | del self.cache[key] -------------------------------------------------------------------------------- /chistmas-caching/writing/src/cache_manager_test.py: -------------------------------------------------------------------------------- 1 | from pytest import raises, mark 2 | from cache_manager import CacheManager 3 | 4 | class TestCacheManager: 5 | 6 | def test_set_missing_key(self): 7 | "Expected to throw" 8 | 9 | cache = CacheManager() 10 | 11 | with raises(ReferenceError): 12 | cache.set(key=None, value='value', expiration=1) 13 | 14 | def test_set_missing_value(self): 15 | "Expected to set an empty key" 16 | 17 | cache = CacheManager() 18 | 19 | cache.set(key='key', value=None, expiration=1) 20 | 21 | assert not cache.get('key') 22 | 23 | @mark.skip(reason="No clue how to test this") 24 | def test_set_missing_expiration(self): 25 | "Expected to have a non expiring entry" 26 | assert False 27 | 28 | def test_set_has_value(self): 29 | "Expected to find the required value" 30 | 31 | cache = CacheManager() 32 | 33 | cache.set(key='key', value='value', expiration=1) 34 | 35 | assert cache.get('key') == 'value' 36 | 37 | def test_get_missing_key(self): 38 | "Expected to throw" 39 | 40 | cache = CacheManager() 41 | 42 | with raises(ReferenceError): 43 | cache.get(key=None) 44 | 45 | def test_get_non_existent_key(self): 46 | "Expected to return None" 47 | 48 | cache = CacheManager() 49 | 50 | assert not cache.get('idonotexist') 51 | 52 | def test_get_existent_key(self): 53 | "Expected to return None" 54 | 55 | cache = CacheManager() 56 | key = 'key' 57 | value = 'value' 58 | 59 | cache.set(key,value) 60 | 61 | assert cache.get(key) == value 62 | -------------------------------------------------------------------------------- /chistmas-caching/writing/src/data_accessor.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from time import sleep 4 | from uuid import uuid1 5 | from utils import trace 6 | 7 | 8 | class TrulyAwesomeBankAPIClient: 9 | def __init__(self): 10 | self.__database = {} 11 | 12 | @trace 13 | async def save_transaction(self, transaction): 14 | if not transaction['id']: 15 | raise ReferenceError('Unable to identify entity. Please provide an id') 16 | 17 | await asyncio.sleep(2) 18 | self.__database[transaction['id']] = transaction 19 | 20 | return transaction 21 | 22 | @trace 23 | async def read_transaction_by_id(self, transaction_id): 24 | await asyncio.sleep(1.5) 25 | if transaction_id in self.__database.keys(): 26 | return self.__database[transaction_id] 27 | return None 28 | -------------------------------------------------------------------------------- /chistmas-caching/writing/src/data_accessor_test.py: -------------------------------------------------------------------------------- 1 | from data_accessor import TrulyAwesomeBankAPIClient 2 | from pytest import mark 3 | 4 | 5 | class TestDataAccessor: 6 | 7 | @mark.asyncio 8 | async def test_save_transaction(self): 9 | data = TrulyAwesomeBankAPIClient() 10 | transaction = { 11 | 'id': 'name', 12 | 'value': 10 13 | } 14 | 15 | entry = await data.save_transaction(transaction) 16 | 17 | assert data._TrulyAwesomeBankAPIClient__database[entry['id']] == entry 18 | 19 | @mark.asyncio 20 | async def test_read_transaction(self): 21 | data = TrulyAwesomeBankAPIClient() 22 | transaction = { 23 | 'id': 'name', 24 | 'value': 10 25 | } 26 | 27 | entry = await data.save_transaction(transaction) 28 | 29 | assert await data.read_transaction_by_id(entry['id']) == entry 30 | -------------------------------------------------------------------------------- /chistmas-caching/writing/src/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from cache_manager import CacheManager 4 | from resource_manager import ResourceManager 5 | from data_accessor import TrulyAwesomeBankAPIClient 6 | 7 | async def main(): 8 | cache_manager = CacheManager() 9 | truly_awesome_bank_API_client = TrulyAwesomeBankAPIClient() 10 | resource_manager = ResourceManager(cache_manager, truly_awesome_bank_API_client) 11 | 12 | transaction = { 13 | 'type': 'PAYMENT', 14 | 'amount': 100, 15 | 'currency': 'EUR' 16 | } 17 | 18 | print('=======================') 19 | print('| WRITE THROUGH |') 20 | print('=======================') 21 | 22 | print('>>> Save transaction') 23 | entry = await resource_manager.save_with_write_through(transaction) 24 | print('>>> Get transaction') 25 | await resource_manager.fetch_transaction_by_id(entry['id']) 26 | 27 | print('=======================') 28 | print('| WRITE BEHIND |') 29 | print('=======================') 30 | 31 | print('>>> Save transaction') 32 | entry = await resource_manager.save_with_write_behind(transaction) 33 | print('>>> Get transaction') 34 | await resource_manager.fetch_transaction_by_id(entry['id']) 35 | 36 | print('') 37 | print('--------------------------------------------') 38 | print('| AWESOME BANK DATABASE (before sync) |') 39 | print('--------------------------------------------') 40 | print(truly_awesome_bank_API_client._TrulyAwesomeBankAPIClient__database) 41 | print('') 42 | 43 | # wait for synchronization 44 | await asyncio.sleep(10) 45 | 46 | print('') 47 | print('--------------------------------------------') 48 | print('| AWESOME BANK DATABASE (after sync) |') 49 | print('--------------------------------------------') 50 | print(truly_awesome_bank_API_client._TrulyAwesomeBankAPIClient__database) 51 | print('') 52 | 53 | loop = asyncio.get_event_loop() 54 | loop.run_until_complete(main()) 55 | loop.close() -------------------------------------------------------------------------------- /chistmas-caching/writing/src/resource_manager.py: -------------------------------------------------------------------------------- 1 | from threading import Timer 2 | from uuid import uuid1 3 | from asyncio import ensure_future, sleep 4 | 5 | 6 | class ResourceManager: 7 | 8 | def __init__(self, cache_manager, truly_awesome_bank_API_client): 9 | self.cache_manager = cache_manager 10 | self.truly_awesome_bank_API_client = truly_awesome_bank_API_client 11 | 12 | def __create_entry(self, transaction): 13 | return { 14 | 'id': uuid1(), 15 | 'transaction': transaction 16 | } 17 | 18 | async def save_with_write_through(self, transaction): 19 | entry = self.__create_entry(transaction) 20 | 21 | self.cache_manager.set(entry['id'], entry) 22 | return await self.truly_awesome_bank_API_client.save_transaction(entry) 23 | 24 | async def save_with_write_behind(self, transaction): 25 | entry = self.__create_entry(transaction) 26 | 27 | async def delay_synchronization(entry): 28 | await sleep(5) 29 | await self.truly_awesome_bank_API_client.save_transaction(entry) 30 | 31 | synchronization = ensure_future(delay_synchronization(entry)) 32 | 33 | self.cache_manager.set(entry['id'], entry) 34 | return entry 35 | 36 | async def fetch_transaction_by_id(self, transaction_id): 37 | entry = self.cache_manager.get(transaction_id) 38 | 39 | if entry: 40 | return entry 41 | 42 | return await self.truly_awesome_bank_API_client.read_transaction_by_id(transaction_id) 43 | -------------------------------------------------------------------------------- /chistmas-caching/writing/src/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | def trace(func): 4 | def wrapper(*args, **kwargs): 5 | now = datetime.now().time() 6 | print(f"[{now}]", func.__qualname__) 7 | return func(*args, **kwargs) 8 | return wrapper -------------------------------------------------------------------------------- /chistmas-caching/writing/tasks.py: -------------------------------------------------------------------------------- 1 | from invoke import task 2 | from os.path import join 3 | 4 | virtualenv_folder = './virtualenv' 5 | 6 | @task(help="Launch the project") 7 | def start(c): 8 | with c.prefix("source " + join(virtualenv_folder, "bin/activate")): 9 | c.run("python3 src/main.py") 10 | 11 | @task(help="Initialize virtualenv for the current project") 12 | def init(c): 13 | c.run("mkdir -p " + virtualenv_folder) 14 | c.run("virtualenv " + virtualenv_folder) 15 | 16 | @task(help={"module": "Module to be added as dependency"}) 17 | def add(c, module): 18 | with c.prefix("source " + join(virtualenv_folder, "bin/activate")): 19 | c.run("pip3 install " + module) 20 | c.run("pip3 freeze > requirements.txt") 21 | 22 | @task(help="Install project's dependencies") 23 | def install(c): 24 | with c.prefix("source " + join(virtualenv_folder, "bin/activate")): 25 | c.run("pip3 install -r ./requirements.txt") 26 | 27 | @task(help="Launch test suite") 28 | def test(c): 29 | with c.prefix("source " + join(virtualenv_folder, "bin/activate")): 30 | c.run("py.test ./src/**_test.py") -------------------------------------------------------------------------------- /reauthor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git filter-branch --env-filter ' 4 | OLD_EMAIL="manuel.spagnolo@gmail.com" 5 | CORRECT_NAME="Manuel Spagnolo" 6 | CORRECT_EMAIL="spagnolo.manu@gmail.com" 7 | if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ] 8 | then 9 | export GIT_COMMITTER_NAME="$CORRECT_NAME" 10 | export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL" 11 | fi 12 | if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ] 13 | then 14 | export GIT_AUTHOR_NAME="$CORRECT_NAME" 15 | export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL" 16 | fi 17 | ' --tag-name-filter cat -- --branches --tags 18 | --------------------------------------------------------------------------------