├── .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}) =>
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}) =>
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 |

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}) =>
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}) =>
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 |

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