Skip to content

Commit 14737cb

Browse files
committed
update README
1 parent 8707c15 commit 14737cb

File tree

1 file changed

+224
-0
lines changed

1 file changed

+224
-0
lines changed

README.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,227 @@
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+
1225
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2226

3227
## Available Scripts

0 commit comments

Comments
 (0)