philnoug@home:~$

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 (
            <button
                type="button"
                className="btn btn-outline-info"
                onClick={ onClick }
            >
                { children }
            </button>
        ); 
    }
}

class CustomButtonBox extends Component {
    render() {
        const { children, value, unit, onClickUP, onClickDOWN } = this.props;
        return (
            <h3>
                <small className="text-info">{ children }</small>
                <br />
                <div className="btn-group">
                    <CustomButton onClick={ onClickDOWN }>-</CustomButton>
                    <span>
                        { value }
                        <small>{ unit }</small>
                    </span>
                    <CustomButton onClick={ onClickUP }>+</CustomButton>
                </div>
            </h3>
        )
    }
}

On assemble le tout :

function TheApp({ params, onSpeedUP, onSpeedDOWN, onTempUP, onTempDOWN }) {
    const { speed, temp, autonomie } = params;
    return (
        <div className="card">
            <div className="card-header">
                <h1>
                    <small className="text-info">Autonomie</small>
                    <br />
                    { autonomie | 0 } km
                </h1>
            </div>

            <div className="card-body">
                <CustomButtonBox
                    value={ speed }
                    unit=" km/h"
                    onClickUP={ onSpeedUP }
                    onClickDOWN={ onSpeedDOWN }>
                    Vitesse
                </CustomButtonBox>
              </div>
            <div className="card-footer"> </div>
        </div>
    );
}

On relie l’ensemble avec l’affichage et les commandes :

function render() {
    ReactDOM.render(
        <TheApp
            params = { store.getState().paramsState }
            onSpeedUP   = { () => store.dispatch(doSpeedUP()) } 
            onSpeedDOWN = { () => store.dispatch(doSpeedDOWN()) }     
        />,
        document.getElementById('root')
    );
}

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();

Sources de l’application

Tester l’application