31/01/2019
Estimer l'autonomie d'une Renault ZOE en ReactJS & Redux
Réécriture de l'appli de calcul de l'autonomie d'une ZOE en utilisant cette fois Redux
Lors de la réécriture de cette petite application ZOE qui me sert de prétexte à explorer les derniers frameworks web, j'essayais de trouver pour chaque nouveau concept rencontré un équivalent dans la vie réelle, pour mieux le comprendre et l'expliquer, et c'est la métaphore du train électrique s'est imposée.
Programmation fonctionnelle et transports ferroviaires
L'analogie entre la programmation fonctionnelle et les transports ferroviaires a déjà été utilisée dans l’excellent Domain modeling made functional (Tackle Software Complexity with Domain-Driven Design and F#) de Scott Wlaschin. L'auteur y associe par exemple une fonction pure avec un segment de voie de chemin de fer qui passerait par un entrepôt (la fonction) dans lequel le chargement du wagon y arrivant serait modifié/transformé avant d'en sortir.
L'analogie avec le rail de chemin de fer vient du fait qu'il n'y a qu'une extrémité de chaque coté du rail et qu'un wagon qui arrive d'un coté doit sortir de l'autre (pure function), ce qui permet ensuite de les relier entre eux pour obtenir des "Higher-Order Functions” (HOF), c.a.d des fonctions qui ne prennent en entrée ou en sortie que d'autres fonctions pures. Le tout formant un circuit sur lequel les actions de l'utilisateur seraient les locomotives, et les données (états) les wagons.
Vous allez me dire que la programmation React/Redux ne peut pas se réduire à la construction d'un circuit de train électrique. Évidement, non. Je n'ai pas envie de me mettre les amateurs de modélisme ferroviaire à dos :) Mais, si ça peut aider à mieux appréhender ce changement de paradigme qu'est la programmation fonctionnelle et son application dans React/Redux, ne nous en privons pas.
Le Flow React/Redux
La façon courante de représenter le flow d'une application React/Redux est celle-ci :
Action=>Dispatcher/Reducer=>Store=>View
Ce qui peut se résumer ainsi; un train, propulsé par une action, va être aiguillé (dispatch) selon sa provenance vers un entrepôt dans lequel son chargement sera transformé (reducer) avant de finir sa course et de livrer son chargement au dépôt, ce qui sera immédiatement signalé sur le grand tableau des arrivées (view) ou tout autre dispositif à l'écoute des arrivées.
Attention au départ !
On commence par s'équiper des indispensables outils pour faire du React et Redux, plus quelques gadgets qui nous serons bien utiles chemin faisant (pan pan:).
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import { combineReducers, applyMiddleware, createStore } from 'redux';
import { createLogger } from 'redux-logger';
import 'bootstrap/dist/css/bootstrap.min.css';
import * as serviceWorker from './serviceWorker';
On décrit ensuite les actions que peut faire un utilisateur (les trains).
// action types
const SPEED_UP = 'SPEED_UP';
const SPEED_DOWN = 'SPEED_DOWN';
Le contenu des wagons :
// states
const params = {
charge: 100,
speed: 80,
temp: 20,
autonomie: 292,
};
Les aiguillages :
// dispatcher/reducers
function paramsReducer(state = params, action) {
switch(action.type) {
case SPEED_UP: {
return applySpeedUP(state, action);
}
case SPEED_DOWN: {
return applySpeedDOWN(state, action);
}
default: return state;
}
}
Les entrepôts de transformation :
// reducers
function applySpeedUP(state, action) {
const speed = action.params.speed + 10;
const autonomie = calculate(action.params.charge, speed, action.params.temp);
if (autonomie)
return {...state, speed, autonomie};
else
return state;
}
function applySpeedDOWN(state, action) {
const speed = action.params.speed - 10;
const autonomie = calculate(action.params.charge, speed, action.params.temp);
if (autonomie)
return {...state, speed, autonomie};
else
return state;
}
On accroche enfin les wagons à la locomotive :
// action creators
function doSpeedUP() {
return {
type: SPEED_UP,
params: store.getState().paramsState,
};
}
function doSpeedDOWN() {
return {
type: SPEED_DOWN,
params: store.getState().paramsState,
};
}
La partie calcul de l'autonomie :
Cette partie devrait être réécrite pour être plus "pure".
// Calculations
function calculate(charge, speed, temp) {
const consommations = { '50': 5.35, '60': 6.83, '70': 8.83, '80': 11.12, '90': 13.82, '100': 17.75, '110': 22.22, '120': 27.33, '130': 32.7 };
const temperatures = { '30': -2.5, '20': 0, '10': 2.5, '0': 5, '-10': 7.5, '-20': 10};
// Puissance restante
const puissance = 41; // Batterie ZOE 4.0 (41kW)
const battery = puissance - (puissance * (100 - charge) / 100);
// Consommation
const conso = consommations[speed];
let autonomie = battery * (parseInt(speed) / conso);
// Impact de la température extérieure
const impact = temperatures[temp];
autonomie = autonomie - (autonomie * impact / 100);
return autonomie;
}
On demande à voir dans la console de tous les changements d'états. Très utile pendant la phase de débogage, à retirer ensuite.
// Logger
const logger = createLogger();
On pourra combiner les voies de sortie de notre circuit principal avec d'autres circuit éventuels, puis on connecte le tout sur la voie qui mène à notre entrepôt général (le stock).
// Store
const rootReducer = combineReducers({
paramsState: paramsReducer,
});
const store = createStore(
rootReducer,
applyMiddleware(logger),
);
Ici on construit la partie visible de l'application; le pupitre de commandes et le tableau des arrivées, morceau par morceau.
// View layer
class CustomButton extends Component {
render() {
const { children, onClick } = this.props;
return (
);
}
}
class CustomButtonBox extends Component {
render() {
const { children, value, unit, onClickUP, onClickDOWN } = this.props;
return (
{ children }
)
}
}
On assemble le tout :
function TheApp({ params, onSpeedUP, onSpeedDOWN, onTempUP, onTempDOWN }) {
const { speed, temp, autonomie } = params;
return (
Autonomie
{ autonomie | 0 } km
On relie l'ensemble avec l'affichage et les commandes :
function render() {
ReactDOM.render(
On demande à écouter les mouvements dans le store afin de rafraîchir la vue quand une arrivée se produit. Et on affiche une première fois la vue.
store.subscribe(render);
render();