|
| 1 | +# Domain driven design with react redux app in typescript |
| 2 | + |
| 3 | +### This POC has multiple goals: |
| 4 | +1. Be able to define an algebra which will describe a subdomain of our business logic. |
| 5 | +2. Provide a definition for the algebra. |
| 6 | +3. Connect algebra to a react redux application. |
| 7 | + |
| 8 | +## 1. Defining the algebra |
| 9 | +We are starting by defining a class which should describe our problem. |
| 10 | +In our example we want to define a call service: |
| 11 | + |
| 12 | +```typescript |
| 13 | +class CallAlgebra {} |
| 14 | +``` |
| 15 | + |
| 16 | +Next we can meditate about the flow of our business logic. In the example we imagining a scenario |
| 17 | +where a customer wish to ask for some help from an agent. So our flow should look something like this: |
| 18 | +customerBackgroundCheck -> askForAgent -> establishCall -> endCall |
| 19 | + -> cancelCall |
| 20 | + |
| 21 | +From the flow we can identify most of the necessary pieces of information needed to define our algebra. |
| 22 | +As we don't really care what are these pieces of information we can just define them as generic parameters. |
| 23 | + |
| 24 | +```typescript |
| 25 | +class CallAlgebra<Customer, Agent, Call> { |
| 26 | +} |
| 27 | +``` |
| 28 | + |
| 29 | +Now we can try to define our collection of atomic functions which the developer should be able to connect together: |
| 30 | + |
| 31 | +```typescript |
| 32 | +class CallAlgebra<Customer, Agent, Call> { |
| 33 | + constructor( |
| 34 | + public customerBackgroundCheck: (c: Customer) => IO<boolean>, |
| 35 | + public askForInterpreter: (c: Customer) => IO<Agent>, |
| 36 | + public establishCall: (c: Customer, a: Agent) => IO<Call>, |
| 37 | + public endCall: (call: Call) => IO<Call>, |
| 38 | + public cancelCall: (call: Call) => IO<Call> |
| 39 | + ) |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +As you see we are keeping our pure and monadic, so we are able to chain our methods to build larger and more complex logic. |
| 44 | + |
| 45 | +```typescript |
| 46 | +class CallAlgebra<Customer, Agent, Call> { |
| 47 | +... |
| 48 | + startACall(customer: Customer): IO<Call> { |
| 49 | + return this.customerBackgroundCheck(customer) |
| 50 | + .chain( |
| 51 | + (isOk) => { |
| 52 | + switch(isOk) { |
| 53 | + case true: return this.askForInterpreter(customer); |
| 54 | + case false: return IO.raise(Error("User didn't pass the background check")) |
| 55 | + } |
| 56 | + } |
| 57 | + ) |
| 58 | + .chain(interpreter => this.establishCall(customer, interpreter)) |
| 59 | + } |
| 60 | + } |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +Now we have every piece to build our algebra, try play with this idea a bit and then we can move to the next step. |
| 65 | + |
| 66 | +## 2. Provide a definition for the algebra |
| 67 | + |
| 68 | +Now that we have a ready algebra we are ready to create an implementation for it (in FP word its called interpreter) |
| 69 | + |
| 70 | +It is made of 2 sub steps. First we need to define our types for our algebras params and then we need to pass our methods implementation: |
| 71 | + |
| 72 | +```typescript |
| 73 | +export type Balance = { |
| 74 | + amount: number |
| 75 | +} |
| 76 | + |
| 77 | +export type Customer = { |
| 78 | + id: string, |
| 79 | + given_name: string, |
| 80 | + family_name: string, |
| 81 | + balance: Balance |
| 82 | +} |
| 83 | + |
| 84 | +export type Interpreter = { |
| 85 | + name: string |
| 86 | +} |
| 87 | + |
| 88 | +export type Call = { |
| 89 | + status: 'cancelled' | 'in_progress' | 'not_started' | 'ended' | 'reconnecting', |
| 90 | + room_id: Option<string>, |
| 91 | + length: number, |
| 92 | + interpreter: Option<Interpreter>, |
| 93 | + customer: Option<Customer> |
| 94 | +} |
| 95 | +``` |
| 96 | +
|
| 97 | +```typescript |
| 98 | +const callAlgebraImplementation = new CallAlgebra<Customer, Agent, Call>( |
| 99 | + // background check |
| 100 | + (c: Customer) => fetchIO<Customer>(`http://localhost:3000/customer/${c.id}`, {}).chain(a => IO.of(() => a.balance.amount >= 0)), |
| 101 | + // ask for interpreter |
| 102 | + () => fetchIO<Agent>('http://localhost:3000/interpreter/1', {}), |
| 103 | + // updateCallInfo |
| 104 | + (call: Call) => IO.of(() => call), |
| 105 | + // establish call |
| 106 | + (c: Customer, i: Interpreter) => { |
| 107 | + const data: Call = { status: 'in_progress', interpreter: validateInterpreter(i), customer: validateCustomer(c), length: 0, room_id: Some("20")}; |
| 108 | + return fetchIO<Call>('http://localhost:3000/calls', { |
| 109 | + method: 'POST', |
| 110 | + headers: { |
| 111 | + 'Content-Type': 'application/json' |
| 112 | + }, |
| 113 | + body: JSON.stringify(data) |
| 114 | + }).map(() => data) |
| 115 | + }, |
| 116 | + // callInProgress |
| 117 | + (call: Call) => call.status === 'in_progress', |
| 118 | + // end call |
| 119 | + (call: Call) => IO.async((sch, cb) => { |
| 120 | + return sch.scheduleOnce(Duration.seconds(1), () => { |
| 121 | + const customerBalance = call.customer.fold( |
| 122 | + () => ({ amount: 0 }), |
| 123 | + customer => customer.balance |
| 124 | + ); |
| 125 | + |
| 126 | + if (call.status === 'in_progress') { |
| 127 | + return cb(Try.of<Call>(() => {...call, status: 'ended'})) |
| 128 | + } else { |
| 129 | + return cb(Failure('Call is not in progress')) |
| 130 | + } |
| 131 | + }) |
| 132 | + }), |
| 133 | + // cancel call |
| 134 | + (call: Call) => { |
| 135 | + if (call.status === 'in_progress') { |
| 136 | + const data: Call = {...call, status: 'cancelled'}; |
| 137 | + return fetchIO<Call>(`http://localhost:3000/calls/1`, { |
| 138 | + method: 'PUT', |
| 139 | + headers: { |
| 140 | + 'Content-Type': 'application/json' |
| 141 | + }, |
| 142 | + body: JSON.stringify(data) |
| 143 | + }).map(() => data) |
| 144 | + } else { |
| 145 | + return IO.of(() => call) |
| 146 | + } |
| 147 | + }, |
| 148 | +) |
| 149 | +``` |
| 150 | + |
| 151 | +## 3. |
| 152 | +Because the IO monad is lazy and referentially transparent we can store its state in redux. |
| 153 | +To make this work we need 2 reducers. One for the IO and the other one for the result of the IO. |
| 154 | + |
| 155 | +```typescript |
| 156 | +export default function ioCallReducer(state: IO<Call> = IO.of(() => initialState), action: actionT): IO<Call> { |
| 157 | + switch(action.type) { |
| 158 | + case 'init': { |
| 159 | + return IO.of(() => initialState); |
| 160 | + } |
| 161 | + case 'startACall': { |
| 162 | + return callAlgebraImplementation.startACall(action.payload.customer); // old state does not mater here |
| 163 | + } |
| 164 | + case 'cancelCall': { |
| 165 | + return state.chain(call => callAlgebraImplementation.cancelCall(call)); |
| 166 | + } |
| 167 | + ... |
| 168 | + default: |
| 169 | + return state; |
| 170 | + } |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +As you see the reducer name starts with `io` this is a must because with this we can detect which reducers |
| 175 | +should are IO monads. Also it is important to mention that its actions should have a field target: |
| 176 | + |
| 177 | +```typescript |
| 178 | +export const startACall: startACallT = customer => { |
| 179 | + return { |
| 180 | + type: 'startACall', |
| 181 | + payload: { customer }, |
| 182 | + target: '@ioCall' |
| 183 | + }; |
| 184 | +}; |
| 185 | +``` |
| 186 | +The target value is @ + the reducer name in state |
| 187 | + |
| 188 | +In the other reducer (called ioReducer) our state is a product of the resulting computations |
| 189 | + |
| 190 | +```typescript |
| 191 | +export type stateT = { |
| 192 | + call: Call |
| 193 | +} |
| 194 | + |
| 195 | +const initialState = { |
| 196 | + call: initialCallState |
| 197 | +} |
| 198 | + |
| 199 | +export default function reducer(state: stateT = initialState, action: actionT): stateT { |
| 200 | + switch (action.type) { |
| 201 | + case 'init': { |
| 202 | + return initialState |
| 203 | + } |
| 204 | + case 'compute@ioCall': { |
| 205 | + return { |
| 206 | + ...state, |
| 207 | + call: action.payload |
| 208 | + } |
| 209 | + } |
| 210 | + // TO add some new: |
| 211 | + // case 'compute@ioUser': { |
| 212 | + // return { |
| 213 | + // ...state, |
| 214 | + // user: action.payload |
| 215 | + // } |
| 216 | + // } |
| 217 | + default: |
| 218 | + return state; |
| 219 | + } |
| 220 | +} |
| 221 | + |
| 222 | +``` |
| 223 | + |
| 224 | + |
1 | 225 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
2 | 226 |
|
3 | 227 | ## Available Scripts
|
|
0 commit comments