M@XCode

Personal blog dedicated to computer science

Dependency injection - What is it ? How to do it in Java ? Why to use it ?

The term “Dependency Injection” has been created by Martin Fowler in 2004 [1]. The term has gained some hype with time and several colleagues presented me the concept.

I wanted to gain a better knowledge of it so I wrote this article that has 3 main objectives :

  • Give a definition of the concept
  • Illustrate it’s usage
  • Make the reader understand why dependency injection may improve software

Vocabulary

Dependency Injection is a technique related to inversion of control. The term “inversion of control” was born in 1988 in an article written by Johnson and Foote [5].

When we write an application that depends on a specific component we will need to call the component we want to use.

In this situation the developer is fully in control of what is happening in the program.

Now, if we use a framework, we can delegate to the framework numerous tasks like instantiating components (but not only). In this configuration control is inverted.

Many frameworks provide inversion of control. In this article we will focus on the Spring Framework (Java) that is built around this concept.

Dependency injection invert a very specific “aspect of control” [1].

To understand the concept, we need first to define precisely the terms involved here :

  • Dependency : A piece of code that needs to be used in another piece of code. In object oriented programming world those pieces of codes are usually classes. We say that a class has a dependency when it needs to use another class. The first class is dependent from the second class. For instance the class responsible to render a web page has a dependency to the class responsible to query the database.
  • Injection : in the medical world, an injection refers to the introduction of a liquid into the body of a patient. An injection is usually done by a doctor (or a nurse). We need three actors for performing injection : (1) the injector (the doctor), (2) the liquid, and the receiver (3).

The idea of “dependency injection” is to let the framework we are using inject our dependencies automatically when we need to use them. Frameworks usually provide in their core code and “injector” to perform this task.

To understand it better we will go through an example :

The example project

Our project is pretty simple. It’s a REST API server with a single endpoint :

1
GET https://maximilien-andile.com/velo

The server (maximilien-andile.com) exposes a GET endpoint (/velo).
The idea of this endpoint is to get the number of available bikes (from a self-service bike rental provider) at a particular station.

In order to send the response to the user our server will need to call the API of the self-service bike rental provider.

UML Sequence Diagram - Example Rest API

Two components, one dependency

UML Component view

In our project we have two components :

  • The web server : it will handle all incoming requests from the web. It has an HTTP interface
  • The external API client : client that will call the self-service bike API.

Here the web server will use the external API client.
We have a dependency. The web server needs to use the external API client to handle incoming HTTP requests.
The web server is dependant of the external API client.

A first version of the project

We will draft a first version of the project to better understand the concrete manifestation of a dependency into the code.

The source code will be written in Java with Spring Boot.

The entry point

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demovelo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoveloApplication {

public static void main(String[] args) {
SpringApplication.run(DemoveloApplication.class, args);
}

}

Here we have the entry point of our program. It consists of a class named DemoveloApplication which contains a single
static function called main.

The REST Controller :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.demovelo.controller;

import com.example.demovelo.service.BikeService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BikeController {

@RequestMapping(path = "/velo",method = RequestMethod.GET)
public int getBikesAtStationX(){
// Instantiate External API Client
BikeService bikeService = new BikeService();
// Call the appropriate method to get the number
// of bikes at station X
return bikeService.bikesAvailableAtStation("X");
}
}

You see here that we have a class (with the annotation @RestController needed by Spring) and then a public method getBikesAtStationX.

This method has also an annotation @RequestMapping(path = "/velo",method = RequestMethod.GET). It allows the Spring framework
to map the request GET /velo received by our server to be mapped to this particular method.

In this method we are instantiating the second component BikeService which is responsible to call the JCDecaux API.
We then call the method bikesAvailableAtStation on the instance created bikeService.

The External API Client : BikeService

And here is the code of BikeService :

1
2
3
4
5
6
7
8
9
10
package com.example.demovelo.service;

public class BikeService {

public BikeService() {
}
public int bikesAvailableAtStation(String stationId) {
return 42;
}
}

Note here that we have not implemented the actual call to the JCDecaux API. (We juste return 42…)

What is wrong with this code ?

Memory usage will increase with server load

Each request to the REST Server will create an instance of BikeService. The memory consumption will increase with the server load.

It is not testable

What if we want to unit test the getBikesAtStationX method ? Here is an unit test example :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.demovelo.controller;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class BikeControllerTest {

@Test
void getBikesAtStationX() {
BikeController bikeController = new BikeController();
assertEquals(12, bikeController.getBikesAtStationX());
}
}

We have a BikeControllerTest class with a single method getBikesAtStationX, which is the method resposible to test getBikesAtStationX.

In this method we create an instance of the BikeController named bikeController and then we call assertEquals (from the junit Assertion Library).

This method takes two parameters : the expected result and the actual result. Here we check that 42 is equal to bikeController.getBikesAtStationX().

If we launch the unit tests it will pass (because bikeController.getBikesAtStationX() always return `42, hence the assertion is valid).

When we will develop getBikesAtStationX and call the external API the result will vary. Our test will fail.
We do not control the external API therefore we cannot base an unit test on it. Unit tests have to be isolated from external services.

We need to find a way to control the output of the method bikesAvailableAtStation.

Dependent components should only use a small interface

In our example the REST server is responsible to create a new instance of BikeService :

1
2
3
4
public int getBikesAtStationX(){
BikeService bikeService = new BikeService();
return bikeService.bikesAvailableAtStation("X");
}

It means that the dependent component is responsible to construct an instance of it’s dependency.

The BikeController should not be responsible of this task.

A modification to the BikeService constructor will break the BikeController.
In other words when we modify the implementation details of our components, every other components that depends on it need to be modified.

Implementation details should be hidden to the clients. Clients should only use on a small interface.

Note here that I have used the term client. When a component A uses a component B we say that the component A is a client of the component B.

UML Component view - The Web Server should use an interface

A new version of the project with dependency injection

The application entry point do not change. Only the BikeService and BikeController will change.

The new BikeService

The first step is to create an interface. This interface define a contract for any implementation of a BikeService :

1
2
3
4
5
package com.example.demovelodi.service;

public interface BikeService {
int bikesAvailableAtStation(String stationId);
}

Then we create a concrete implementation of that contract :

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.demovelodi.service;

import org.springframework.stereotype.Service;

@Service
public class BikeServiceImpl implements BikeService {

@Override
public int bikesAvailableAtStation(String stationId) {
return 42;
}
}

Here our class BikeServiceImpl implements the BikeService interface. To fulfill this contract we define the method bikesAvailableAtStation.

Note that we added 1 Spring Annotation and 1 java annotation :

  • @Service : allow the Spring framework to register the class as a bean (a component).
  • @Override : Here we have an implementation of an interface, so at first sight this annotation seems useless, this class do not extend any class. The annotation was automatically provided by my IDE. After a quick look it seems that adding the annotation will force the compiler to check if we implemented correctly the interface…

The new BikeController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.demovelodi.controller;

import com.example.demovelodi.service.BikeService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BikeController {

private final BikeService bikeService;

public BikeController(BikeService bikeService) {
this.bikeService = bikeService;
}

@RequestMapping(path = "/velo",method = RequestMethod.GET)
public int getBikesAtStationX(){
return this.bikeService.bikesAvailableAtStation("X");
}
}

We added the private property bikeService in our class (of type BikeService).

The we added a constructor to our class that takes as parameter a BikeService.

The method getBikesAtStationX did not change a lot, except that we do not construct an instance of BikeService in it.
Instead we are using the class property.

We never instantiate the BikeService in our controller, it will get instantiated by the Framework.

An injector, that is hidden by the framework will instantiate it.

Here is an UML Class diagram that show the relation between classes :

UML Class view of dependency injection

  • The BikeServiceImpl class implements the BikeService interface
  • The BikeController uses the BikeService interface
  • The FrameworkInjector will instantiate the BikeServiceImpl class and inject it into the BikeController.

Unit test

The first version of the BikeController was impossible to test.

The second version of the controller requires an interface in it’s constructor. As a consequence we can use a mocked BikeService instead of the actual implementation of the BikeService.

Here is the mocked implementation :

1
2
3
4
5
6
7
8
9
10
package com.example.demovelodi.controller;

import com.example.demovelodi.service.BikeService;

public class BikeServiceMock implements BikeService {
@Override
public int bikesAvailableAtStation(String stationId) {
return 65;
}
}

Here instead of calling the real API we are directly returning a fixed integer, allowing us to assert that the controller returns what is expected :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.demovelodi.controller;

import com.example.demovelodi.service.BikeService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class BikeControllerTest {

private BikeController bikeController;

@BeforeEach
void setUp() {
BikeService mockedBikeService = new BikeServiceMock();
this.bikeController = new BikeController(mockedBikeService);
}

@Test
void getBikesAtStationX() {
assertEquals(65, this.bikeController.getBikesAtStationX());
}
}

In the setUp function (that will be run before each tests) we create an instance of our mocked implementation. Then we set the property bikeController with a new instance of the BikeController constructed with the instance of the mocked Service.

The test (in the method getBikesAtStationX) will assert (verify) that 65 is actually returned by this.bikeController.getBikesAtStationX().

Comparison of the two controllers

To better see the differences between the two version here are the two controllers

  • Without Dependency Injection :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.demovelo.controller;

import com.example.demovelo.service.BikeService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BikeController {

@RequestMapping(path = "/velo",method = RequestMethod.GET)
public int getBikesAtStationX(){
// Instantiate External API Client
BikeService bikeService = new BikeService();
// Call the appropriate method to get the number
// of bikes at station X
return bikeService.bikesAvailableAtStation("X");
}
}
  • With DI :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.demovelodi.controller;

import com.example.demovelodi.service.BikeService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BikeController {

private final BikeService bikeService;

public BikeController(BikeService bikeService) {
this.bikeService = bikeService;
}

@RequestMapping(path = "/velo",method = RequestMethod.GET)
public int getBikesAtStationX(){
return this.bikeService.bikesAvailableAtStation("X");
}
}

Dependency injection and maintainability

In this section I will focus on the effects of dependency injection on software maintainability.

What is software maintainability ?

There are two definitions of “software maintainability” :

The ISO/IEC defines it as :

the capability of the software to be modified

For Matinlassi Mari and Niemelä Eila [2], the maintainability is :

The ease with which a software system or component can be modified or adapt to a changed environment.”

The definition emphasis that a maintainable software can be changed without pain for the future developers that will work on the project.

Why do we care about maintainability ?

When you care about maintainability you care about your peers but also about the company that employs you.

A component that is hard to make evolve will increase the budget of a project.

Coupling can be used to measure maintainability

The previous definition is clear. A software is maintainable if we can change it easily.

The characteristic of “changing easily” is very subjective and therefore hard to measure.

We can use a proxy to detect a maintainable software : coupling.

A system is said to be highly coupled if it’s different parts are highly interdependent. In opposite a system is loosely coupled if it’s parts it are less interdependent.

Coupling can be measured by code parsers. In 1999 a pretty famous paper written by Briand, Daly, & Wust defined different metrics around the concept of coupling [4].

Two measures of coupling

From the Briand, Daly, & Wust [4] we can remember two metrics :

  • CBO (Coupling Between Objects) can be calculated for any class. It counts the number of couples there is between the given class and other classes.
    A couple is created when a class use methods or attributes of the other class. If the class is used by two other class the CBO is 2.

  • RFC (Response For A Class) is the number of methods that can be called when a message is received on an object of a class [4].
    This metric gives an idea of the level of testability of the software created.
    If only one method can be called for each message received on the object, building an unit test is easy. When for a single message 10 methods can be called and those method can also call 10 more methods, writing unit tests is not an easy task.

Those two metrics allow us to give a precise definition of coupling.

The more dependency injection the less coupling

Razina & Janzen in a paper from 2007 wanted to demonstrate that “significantly reduces dependencies of modules in a piece of software, therefore making the software more maintainable.”.

They calculated “cohesion and coupling metrics” on 40 projects (20 projects that were using Dependency Injection, 20 without dependency injection).

The study did not succeed into proving the hypothesis. But they demonstrate that :

a trend of lower coupling in projects with higher dependency injection percentage (more than 10 %) was evident

Using dependency injection does not automatically reduce coupling. But when it is used software coupling metrics seems to be lower.

Further reading

  • [1]Inversion of Control Containers and the Dependency Injection pattern, Martin Fowler
  • [2] Mari, M. (2003, September). The impact of maintainability on component-based software systems. In null (p. 25). IEEE.
  • [3] Razina, E., & Janzen, D. S. (2007). Effects of dependency injection on maintainability. In Proceedings of the 11th IASTED International Conference on Software Engineering and Applications: Cambridge, MA (p. 7).
  • [4] Briand, L. C., Daly, J. W., & Wust, J. K. (1999). A unified framework for coupling measurement in object-oriented systems. IEEE Transactions on software Engineering, 25(1), 91-121.
  • [5] Johnson, R. E., & Foote, B. (1988). Designing reusable classes. Journal of object-oriented programming, 1(2), 22-35. (article is here)

Javascript : que sont les closures et comment les utiliser

Une closure (qui peut se traduire par fermeture en français) est une fonction qui garde accès au scope englobant dans laquelle elle est définie. Et ce même lorsque ce scope est “fermé”. Cette définition ma rendu très perplexe au début. Mais essayons de la comprendre. Pour cela faisons un petit rappel sur la portée des variables.

Portée des variables.

La portée d’une variable fait référence aux règles qui régissent l’accès et l’utilisation aux variables de nos script. S’interroger sur la portée d’une variable permet de savoir si la variable est bien définie et si donc on a le droit de s’en servir.

Lorsqu’on commence à programmer on se pose rarement la question.

La portée globale

1
2
var le_yo="yo";
alert(yo);

Ici notre variable le_yo est défnit globalement, elle est donc disponible dans tout notre script c’est pour cela que l’on peut ecrire alert(le_yo); sans problèmes. Rien de bien sorcier ici.

La portée locale

C’est ici que cela se corse :

1
2
3
4
function dire_yo(){
var loc = "yo":
alert(yo);
}

Ici la variable loc a une portée locale car elle est défnit dans une fonction. En fait chaque fonction javascript dispose de son scope. Le scope, pour prendre une image, c’est un peu le bac à sable de notre fonction : l’ensemble de son terrain de jeu. On y a notre pelle, notre rateau.

Ce qu’il faut comprendre c’est qu’on ne peut pas accéder à notre pelle et notre rateau en dehors du bac à sable.

Ce code ne fonctionnera pas :

1
2
3
4
5
function dire_yo(){
var loc = "yo";
alert(yo);
}
alert(loc);

On se mangera l’erreur Uncaught ReferenceError: loc is not defined. Et c’est normal, parce que loc n’existe que dans la fonction dire_yo !

En revanche ce code marche :

1
2
3
4
5
6
7
var glob="yaaaaaaaa"
function dire_yo(){
var loc = "yo";
alert(yo);
}
alert(glob);
// alertera : 'yaaaaaaaa'

Ici la variable glob est définie dans le scope global donc elle est accessible dans tout notre script. On peut même modifier cette variable dans notre fonction :

1
2
3
4
5
6
7
8
var glob="yaaaaaaaa"
function dire_yo(){
var loc = "yo";
glob= glob+"ouuuuuu";
}
dire_yo();
alert(glob);
// alertera : 'yaaaaaaaaouuuuuu'

Ici on execute d’abord notre fonction, qui va ajouter ouuuuuu à la variable glob et du coup on aura une alerte yaaaaaaaaouuuuuu.

Comment utiliser map et reduce en Javascript

Map et Reduce sont deux méthodes qui sont apparues dans le cadre de ES5. Ces méthodes sont utilisables sur les tableaux (Array). Ces deux méthodes proviennent de la programmation fonctionnelle. Cette dernière m’étant encore assez obscure j’ai pour projet d’écrire un article plus détaillé.

Map transformer des tableaux en d’autres tableaux

L’objectif de map est d’itérer sur tous les éléments d’un tableau et de produire un autre tableaux avec un contenu transformé

On peut par exemple imaginer un tableau de prénoms :

1
2
// tableau (A)
var prenoms = ["Joe","Patrick","Thierry"];

On peut imaginer une transformation simple sur ce tableau : générer un tableau contenant les premières lettres de chaque élément.

1
var initials = prenoms.map(function(prenom){return prenom.charAt(0);})

Ici :

  • On utilise la méthode map sur notre array prenoms
  • La méthode map prends en entrée une fonction avec un seul paramètre qui contiendra un prénom de notre tableau prenoms(A)
  • Cette fonction doit retourner ce que l’on souhaite insérer dans notre nouveau tableau (B)

Reduce : itérer sur un array pour produire un seul et unique résultat

Reduce permet aussi d’itérer sur un array en revanche au lieu de produire systématiquement un tableau transformé, elle va renvoyer un unique résultat (qui peut être d’un autre type que l’Array…)

Nous allons prendre le même exemple. Nous partons d’un array de prénoms :

1
2
// tableau (A)
var prenoms = ["Joe","Pat","Max"];

Notre objectif est de récupérer le nombre total de lettres de nos prénoms.

Vous me pardonnerez la stupidité de l’exemple.

1
2
3
var total_number_of_letters = prenoms.reduce(function(somme,prenom){
return somme + length;
},0);

Ici :

  • On applique la méthode reduce à l’array prenoms.
  • Cette méthode prends deux paramètres :
  1. Une fonction (un itérateur)
  2. La valeur d’initialisation du résultat renvoyé. Ici on initialise à 0, car on veut la somme

La fonction, premier paramètre de la méthode reduce prends elle même deux paramètres :

  • La “valeur de retour de la réduction”. => ici la somme
  • L’élément qui sera traité à chaque itération prenom

Nous partons donc ici d’un tableau et nous générons un entier représentant le nombre total de caractère de nos prénoms.

1
2
3
4
5
6
var total_number_of_letters = prenoms.reduce(function(somme,prenom){
return somme + length;
},0);

console.log(total_number_of_letters);
// Affiche : 9 !

ES6 : les principales features à connaître

ES6 ou ECMA Script 6 est un ensemble de normes ou standards de programmation. La version 6 de ce standard est sortie en 2015.

Les constantes

Il n’était possible de définir une constante avec ES5, qu’en passant par la création d’objets. Il fallait donc créer une propriété d’un objet et spécifier que cette propriété n’était ni writable ni configurable

Avec ES6 il suffit d’écrire :

1
2
3
4
const OMEGA = 0.5671432904
if(OMEGA > 12){
// do stuff
}

Il peut être intéressant d’utiliser les constantes au lieu de var afin de signaler qu’il n’y aura pas de réassignement.

Les variables block-scoped

La notion de scope est très importante en JS.

Une variable peut avoir une portée :

  • Globale
  • Locale

Une variable globale est modifiable dans l’ensemble du programme, elle est définie en dehors de toute fonction. Une variable locale quant à elle n’existe que dans le cadre de l’exécution de la fonction. Elle est créée lors de l’exécution de la fonction. Elle est ensuite détruite à la fin de l’exécution.

ES6 apporte une nouveauté intéressante en créant des variables dont la portée est limitée à un bloc :

1
2
3
4
5
var i
if (PI>5) {
let gamma = 20*3;
i = gamma * gamma;
}

Ici :

  • i est une variable ayant un scope globale
  • gamma est une variable ayant un scope de bloc

Le Hoisting

Je fais un micro-passage ici par la notion de Hoisting en Javascript. Car cette notion est intéressante.

La règle est qu’on peut se servir d’une variable et la déclarer a posteriori dans le code..

1
2
3
4
5
6
b= 32;

c= b+1;

var b;
var c;

Ce code est fonctionnel. Car le moteur va passer en revue le code une première fois pour procéder aux déclarations. En revanche si on utilise le mode strict on ne peut utiliser ce genre de chose !

Les Arrow Functions ou fonctions fléchées

On connait trop bien la manière de déclarer des fonctions anonymes. Cette méthode de déclaration est assez longue :

Voici l’ancienne méthode :

1
odds  = evens.map(function (v) { return v + 1; });

La nouvelle version ES6

1
2
3
4
5
6
7
8
9
10
odds  = evens.map(v => v + 1);
// OU encore
(a,b) => a+b
// équivalent à
(a,b) => {
return a+b;
}
// on peut aussi des fonctions sans paramètres de cette manière
() => { instructions }
_ => { instructions }

Les templates et les chaînes de caractères

1
2
3
4
let amount = "150 $"
let due_date = "15th April"

let sentence = "You owe me ${amount} , the due date is ${due_date}"

Les paramètres par défaut

Alors qu’il fallait utiliser des techniques un peu moches pour déterminer si un paramètre était renseigné :

1
2
3
// Réassignation de x
// il vaut 0 si x n'est pas un nombre...
var x = typeof(x) === "number" ? x : 0;

On peut maintenant définir les paramètres ainsi :

1
2
3
let addition = function (a=0,b=0){
return a+b;
}

Boucles for of (foreach)

Houra voilà le foreach en JS :

1
2
3
4
let students = ["Joe","Donald"];
for (let student of students){
console.log(student);
}

Les strings multi-lignes

On peut maintenant déclarer une chaîne sur plusieurs lignes grâce au symbole backtish :

1
2
3
4
var vh = `Mon père, ce héros au sourire si doux,
Suivi d'un seul housard qu'il aimait entre tous
Pour sa grande bravoure et pour sa haute taille,
Parcourait à cheval, le soir d'une bataille,`

Les assignements déstructurés

Avec ES5 on doit faire :

1
2
3
var body = req.body,
username = body.username,
password = body.password

Avec ES6 on fera :

1
var {username, password} = req.body

Les promesses

L’utilisation est désormais native.

Les classes

Le grand changement est l’introduction des classes :

Déclaration

1
2
3
4
5
6
7
8
9
10
11
12
13
class Human {

constructor(sex,age){

this.sex=sex;
this.age=age;

}

speak(string){

}
}

Héritage

On peut faire hériter une classe d’une autre classe via le mot clé extends.

1
2
3
4
5
6
7
8
9
10
11
12
class Teacher extends Human {
constructor (sex, age, speciality) {
super(sex,age)
this.speciality = speciality
}
}
class Student extends Human {
constructor (sex, age, major) {
super(sex,age)
this.major = major
}
}

Accès aux méthodes de la classe de base

Pour accéder aux méthodes et propriétés définies dans la classe de base on utilise super.

1
2
3
4
5
6
7
8
9
10
class Rectangle extends Shape {
constructor (id, x, y, width, height) {
super(id, x, y)

}
toString () {
return "Rectangle > " + super.toString()
}
}
// source : http://es6-features.org/#BaseClassAccess

super.toString() appelle la méthode toString() définie dans la classe Shape.

Getters et Setters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student extends Human {
constructor (sex, age, major) {
super(sex,age)
this.major = major
}

get major(){
return this.major
}
}

var paul = new Student("Male",20,"CS");
if(paul.major=="CS"){

}

Modularisation: la syntaxe export et import

Dans un module on peut utiliser la syntaxe export:

1
2
3
4
5
export var standard_price = 10

export function get_other_prices (){
//...
}

Puis dans le fichier principal on peut importer variables et méthodes :

1
import {standard_price,get_other_prices} from 'module'

On peut aussi tout importer :

1
2
import * as my_module from 'module'
console.log(my_module.standard_price);

Comment utiliser ES6.

  • N’étant pas supporté encore par tous les navigateurs il faudra utiliser un compilateur comme Babel qui se chargera de traduire votre code avec une syntaxe supportée.
  • Avec NodeJS les principales features sont supportées par les nouvelles versions.

ADN : une nouvelle source de stockage de données durable

Les sources de stockage actuelles sont paradoxalement moins durables plus le temps avance. Par exemple une écriture gravée sur la pierre se conserve en moyenne 10.000 ans, une écriture sur parchemin se conserve 1.000 ans. Un DVD conserve les données au maximum une dizaine d’années. Un disque dur est rarement garanti par le fabricant plus de 5 années, tant il est fragile.

La solution pour un stockage durable de l’information serait la réplication multiple. Dès qu’un support de stockage atteint la fin de sa vie, on copie la donnée sur un support similaire. Cette solution est écologiquement problématique. C’est sans compté sur les erreurs de copies qui peuvent compromettre les informations stockées.

Je suis passé par hasard sur un papier de recherche sur le stockage de l’information sur le support ADN ! Incroyable me suis-je dit. Essayons de comprendre le mécanisme…

Pour les curieux voici le papier : (https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3672958/). Publié dans Nature en 2013.

Avantages du stockage de l’information sur les brins d’ADN

  • La capacité de stockage est très importante, on peut stocker sur un volume très faible un volume important de données
  • Les brins d’ADN peuvent se conserver longtemps.

Méthodes de transcription de l’information.

Les résultats de l’équipe de recherche sont très intéressant puisqu’ils ont réussi à encoder 739kB de données dans un brin d’ADN synthétique puis ont réussi à re-décrypter les fragments de data sans aucune erreur ni compromission.

Etape 1 : Encodage de la données en Base 3 Huffman

Le codage de Huffman part du principe que plus un caractère est utilisé moins son encodage sera large. Les caractères les plus utilisés de la source de données tiendront donc moins de place ! C’est un avantage très intéressant pour stocker de large volume de textes.

Dans le cadre de l’expérimentation chaque byte est encodé avec 5 ou 6 caractères base 3 (appelés trits).

Les trits peuvent être égaux à 0,1 ou 2.

1
2
3
4
// Chaine originale
"Thou"
// Base-3 Huffman
"20112202000211010002"

Ainsi T est encodé sous la forme 20112 , h sous la forme 20200

Etape 2 : conversion des trits en nucléotides

Chaque lettre est donc convertie en trits. On va ensuite prendre chaque trit est le traduire par un nucléotide.

Les nucléotides sont l’unité de base de la construction de l’ADN. Il existe 4 nucléotides différents : l’adénine (A), la guanine (G), la thymine (T) et la cytosine (C).

On va donc remplacer chaque trit par un nucléotide différent de celui qui le précède si il y a répétition.

1
2
3
4
5
6
// Chaine originale
"T"
// Base-3 Huffman
"20112"
// ADN
"TAGAT"

On souhaite en effet éviter les répétitions de nucléotides. pour éviter de former ce que l’on nomme en chimie des homopolymères.

Etape 3 : Génération des fragments d’ADN avec overlap.

L’équipe a ensuite procédé à la génération de multiples fragments d’ADN contenant 100 bases chacun, avec un overlap de 75 bases à chaque fois.

Cette méthode permettant d’avoir une donnée redondante pour éviter les pertes de données en cas de problèmes de conservation.

Dans chaque fragment on ajoute des données d’indexation du fragment afin de savoir où ce dernier se situe dans la chaîne complète :

Stockage ADN et indexing

Perspectives :

  • Le stockage reste encore très cher car il nécessite une équipe importante et du matériel haut de gamme…
  • 12,400 dollars/MB pour le stockage et 220 dollars/MB pour le décodage !
  • Les coûts toutefois sont amenés à se réduire drastiquement (l’équipe envisage un rapport de 100 par rapport aux coûts actuels)
  • Le volume de stockage est très important comparé à la taille du réceptacle !

Comment créer un script shell permettant de récupérer et d'analyser des articles wikipedia avec lynx

Qu’est ce qu’un script shell

Un script “shell” est un programme écrit pour être exécuté via l’interpréteur de commande UNIX. Ces scripts sont facile à générer et suivent les patterns des langages de programmation communs. Ainsi on a la possibilité de créer des boucles et des structures conditionnelles par exemple.

Un script shell est dans les fait un fichier avec l’extension sh. On l’exécute en ouvrant cmd sous windows, ou le terminal sur macOS. La commande permettant d’executer un script shell est la suivante:

1
$ sh name_of_the_script.sh

NB : Par habitude on rajoute un sigle dollar devant la commande. Il s’agit juste d’une convention permettant d’indiquer au lecteur que cette commande doit être exécutée dans le terminal.

Itérer sur les lignes d’un fichier via shell

Afin de lire les lignes d’un fichier les unes des autres on peut utiliser la structure for. For indique à l’algorithme qu’il faut itérer pour chaque élément d’un input (ici l’ensemble des lignes de notre fichier) :

1
2
3
4
5
location_of_the_url_file = "./urls.txt"
for url in `cat $location_of_the_url_file `
{
echo $url
}

Dans un premier lieu on déclare la variable location_of_the_url_file qui stocke l’emplacement de notre fichier relativement à notre dossier de travail.

Dans un second temps on va récupérer l’ensemble des lignes de notre fichier via la commande :

1
`cat $location_of_the_url_file`

Récupérer le contenu d’une page web via wget

wget est un outil de téléchargement très puissant. On peut donc l’utiliser pour télécharger le contenu d’une page web directement depuis le terminal. Il faut d’abord bien s’assurer de l’avoir installer sur sa machine.

Si ce n’est pas le cas on peut utiliser la commande brew install wget. Homebrew est un gestionnaire de dépendance très pratique qui permet des installations rapides. Pour plus d’information voici la page : http://brew.sh/index_fr.html

1
wget -O ./pages/wiki.html $url

La commande suivante permet de télécharger la page dont l’url est $url et de stocker cette dernière dans le dossier pages sous le nom wiki.html. Le téléchargement dans un répertoire cible est géré par la commande -O.

Créer un dump en format plain text de notre page html grâce à lynx

Lynx est un outil permettant la manipulation de pages HTML. Il s’agit en fait d’un navigateur en mode ligne de commande. Une sorte de chrome version simplifiée.

Pour accéder à google depuis votre terminal :

1
lynx http://www.google.fr

Google en version Lynx

On peut donc produire une version .txt de notre page

1
lynx -dump $url >./plain_text/page_version_txt.txt

Cette version txt de nos pages web nous permettra de nous livrer à des analyses de type “NLP” (Natural Language Processing). Nous traiterons plus en détail cet aspect dans un prochain billet.

Voici le code complet du programme : https://github.com/maximilienandile/shell_web_page_to_plain_text

Machine Learning : classification à l'aide des arbres de décisions : fonctionnement et application en NodeJS

Qu’est ce qu’un arbre de décisions

Un arbre de décision est une représentation visuelle d’un algorithme de classification de données suivant différents critères qu’on appellera décisions (ou noeuds). Voici un exemple :

Exemple d'un arbre de décision pour l'accord d'un prêt

Cet arbre de décision permet en fonction de quelques questions de déterminer si une banque doit prêter ou pas au client qui demande un prêt. Ce dernier est très simplifié mais la plupart des banques utilisent des systèmes similaires permettant aux agents une prise de décision experte.

Mais comment arrive t’on à de telles règles de décisions. Dans les faits il s’agit de synthétiser la connaissance et l’historique de l’ensemble des prêts accordés par la banque et de classer chacun de ces prêts selon qu’ils aient été remboursés sans incident ou pas. Il s’agit donc de trouver dans une énorme quantité de données les questions qu’il est judicieux de poser afin de prédire la qualité d’un emprunteur.

Vocabulaire

Nous allons détailler la construction d’un tel arbre de décision. Posons d’abord quelques mots de vocabulaire. On va classer des objets.

  • Chaque objet de la données historique (set d’entraînement) dispose d’une classe bien définie
  • Chaque objet (par exemple un prêt accordé à monsieur X) dispose de features, de propriétés bien définies elles aussi. Par exemple : la personne a qui on a accordé le prêt possédait-elle une maison, possédait elle un CDI ?
  • Chaque propriété ou feature a un ensemble de valeurs possibles. Par exemple (‘oui’ ou ‘non’)

Dans notre graphique représentant l’arbre de décision on a les éléments suivants :

  • Un noeud de décision (représenté par un rectangle) (on pose une question)
  • Un bloc de fin qui est représenté par un oval. (on a trouvé la classe)

Vocabulaire de l'arbre de décision

Fonctionnement de l’algorithme : un exemple

Dans les faits on aura en entrée de notre algorithme une série de données qui représentera de nombreux objets (ici des prêts) ayant chacun un set de propriétés (ici : CDI oui ou Non, Locataire oui ou non …). Ces objets du set d’entrainement seront déjà classés (ex: Bon Emprunteur).Par exemple :

1
2
3
4
5
6
7

Prêt 1 : CDI : 1 - Locataire : 1 - Classe : Bon emprunteur
Prêt 2 : CDI : 0 - Locataire : 0 - Classe : Bon emprunteur
Prêt 3 : CDI : 1 - Locataire : 1 - Classe : Bon emprunteur
Prêt 4 : CDI : 0 - Locataire : 0 - Classe : Mauvais emprunteur
Prêt 5 : CDI : 1 - Locataire : 1 - Classe : Bon emprunteur
...

Notre algorithme va donc devoir choisir une première question à poser à notre candidat. Pour cela il doit choisir la feature (ou propriété) qui permet de découper nos prêts en deux sets les plus homogènes possibles, c’est à dire deux sets regroupant des prêts dont les emprunteurs sont en grande partie d’une même catégorie.

Par exemple l’algorithme va tester la feature CDI et va ensuite répartir les prêts dans deux set : les prêts fait par des personnes en CDI et les autres. Si dans un des set on trouve que des prêts ayant la même classe alors l’algorithme s’arrêtera pour cette branche. Par exemple si on constate que tous les gens qui n’ont pas de CDI sont des mauvais emprunteurs alors on peut s’arrêter là et dire à nos agents de ne plus prêter aux gens sans CDI.

Si on a pas obtenu un set composé de prêts qui ont la même classe après ce premier split, on va ensuite refaire le même travail pour les features suivantes. Nous allons sélectionner la feature permettant un classement le plus homogène possible… Ce processus est mené jusqu’à ce qu’o obtiennent des classes homogènes.

Overfitting et extraction de sens

Cet algorithme est peu gourmand en mémoire. Mais il y a un risque très important d’overfitting, c’est à dire que l’arbre n’est pas utilisable car il fournit des résultats très bon pour le set d’entraînement, mais utilisé en conditions réelles il classe très mal les nouveaux exemples. Pour cela on peut envisager d’avoir couper notre set d’entrainement en deux afin d’avoir un set de données permettant de valider si il y a overfitting ou pas.

Cet algorithme est très intéressant pour extraire de l’information d’une source de données obscure. Il permet d’isoler les propriétés ou features qui apportent le plus d’information pour déterminer la classe de chaque objet.

Application en NodeJS

Calcul de l’entropie de Shannon

Dans le paragraphe précédent nous disions qu’il fallait trouver la feature générant des classes qui sont les plus homogènes possibles. Mais comment définir cette homogénéité ?

Nous allons utiliser un résultat important de la théorie mathématique de l’information : l’entropie. Et plus particulièrement l’entropie de Shannon :


$H=-\sum_{i=1}^{n} P(x_{i})log_{2}(P(x_{i}) )$

L’entropie correspond au désordre. Plus on a de classes différentes dans un set de données plus l’entropie sera grande. De manière opposée si dans un set de données tous les objets ont la même classe, il n’y aura pas de désordre l’entropie sera nulle.

Dans les faits on va calculer l’entropie de la manière suivante en NodeJS :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
var _ = require('lodash');
/**
* This function is used to compute the Shannon Entropy
* @method calculate_shannon_entropy
* @param {Array} data an array of data
* @return {Number} The Shannon Entropy
*/
var calculate_shannon_entropy=function(data){

// init the shannon Entropy
var shannon_entropy = 0;
// we get the number of entries in the array
var length_of_data = data.length;
// create an empty array to keep track of different labels
var labels_counter = {}
// we iterate through the array
_.forEach(data, function(value, key) {
// extract the label from the one data element
var label_extracted = value.label;
// check if this label is inside our label counter array
if (label_extracted in labels_counter){
// the element is inside our object increment its value
labels_counter[label_extracted] +=1
} else {
labels_counter[label_extracted] = 1
}
});

// We then iterate through our label counter object
_.forEach(labels_counter, function(value, key) {
// we get the frequency of the number of time a label occurs
var p = parseFloat(value/length_of_data)
// Caculate the entropy of a class and add it to the shannon_entropy variable
shannon_entropy -= p * getBaseLog(2,p)

})

return shannon_entropy

}

/**
* This function return the log of Y in base X
* @method getBaseLog
* @param {Number} x base
* @param {Number} y number
* @return {Number} the log of Y in base X
*/
var getBaseLog=function(x, y) {
return Math.log(y) / Math.log(x);
}

var a = [
{"has_a_car":1, "has_a_house":1, "has_children":1,"label":"bon_emprunteur"},
{"has_a_car":1, "has_a_house":1, "has_children":0,"label":"bon_emprunteur"},
{"has_a_car":1, "has_a_house":0, "has_children":1,"label":"bon_emprunteur"},
{"has_a_car":0, "has_a_house":0, "has_children":0,"label":"mauvais_emprunteur"},
{"has_a_car":0, "has_a_house":0, "has_children":1,"label":"mauvais_emprunteur"}
]
calculate_shannon_entropy(a)
// returns 0.9709505944546686

On prend donc la fréquence d’apparition de la classe bon_emprunteur et son log en base 2 auquel on va ajouter moins la fréquence d’apparition de la classe mauvais_emprunteur et son log en base 2 !

Il va falloir ensuite que l’on puisse éliminer des features au fur et à mesure que l’on split notre données au fil des blocs de décision. C’est l’objet de la partie suivante :

Création de la fonction permettant de partitionner un set de données en fonction d’une feature et de sa valeur

Notre mission ensuite est de préparer la fonction qui permet à partir d’un set de données d’entrainement de récupérer un set plus petit ne contenant plus que les objets disposant de cette feature dont la valeur est égale à une des valeurs possibles de la feature.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

/**
* This function split our dataset and extract object that have the feature set to a specific value
* @method split_the_dataset_by_feature_and_value
* @param {Array of Object} data our dataset
* @param {String} feature the name of the feature
* @param {String or Number} value the value of the feature to extract from the dataset
* @return {Array of Object} value the dataset without the objects that have the feature "feature" set to the value "value" & without the feature "feature"
*/
var split_the_dataset_by_feature_and_value=function(data_set,feature,value){
// we partition our array in order to isolate in the data
// Objects that have the `feature` set to the right value
var data_set


var array_partitioned = _.filter(data_set, function(o) { return o[feature]==value; });
// we then have to remove the feature from our objects
var array_to_return = []


array_partitioned.forEach(function(v){
var new_object = {}
_.forEach(v, function(value, key) {

if(key!==feature){
new_object[key]=value
}
});
array_to_return.push(new_object)

});

return array_to_return
}

split_the_dataset_by_feature_and_value(a,"has_a_house",0)
// returns
// [ { has_a_car: 1, has_children: 1, label: 'bon_emprunteur' },
// { has_a_car: 0, has_children: 0, label: 'mauvais_emprunteur' },
// { has_a_car: 0, has_children: 1, label: 'mauvais_emprunteur' } ]

Par exemple ici nous nous débarassons des objets dans notre set de données dont la feature has_a_house est différent de 0.

Maintenant attaquons nous au coeur du réacteur : nous devons trouver la meilleur feature, autrement dit celle qui génère le moins d’entropie (on parle aussi de gain informationnel maximal)…

Déterminer la feature dans un set de données générant un gain informationnel maximal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

var _ = require('lodash');
var data_set_splitter = require('./data_set_splitter')
var shannon_entropy = require('./shannon_entropy')

/**
* This function will select the best feature for splittingwith the help
* of the shannon entropy
* @method get_the_best_feature_for_splitting
* @param {Array} data A dataset of training, an array conposed of object with a property "label" that represents the class
* @return {String} The name of the best feature for splitting (the property name)
*/
var get_the_best_feature_for_splitting= function(data){
// we compute the Entropy
var base_entropy_value = shannon_entropy.calculate_shannon_entropy(data)
// init a variable that will contain the best informational gain value
var best_informational_gain_value = 0
// init a variable that will contain the best feature choice for splitting
var best_feature_name = ""
// we iterate through our features (we take the first data element)
_.forEach(data[0], function(value_of_feature, feature_name) {
if(feature_name != "label"){


// get the unique values of each feature
var unique_values_of_this_feature = _.uniq(_.map(data, feature_name));
// init a variable that will contain the new_entropy_value
var new_entropy_value = 0;
// iterate through each possible value of this feature
_.forEach(unique_values_of_this_feature, function(value_of_this_feature, key) {

var data_set_without_this_feature_of_value = data_set_splitter.split_the_dataset_by_feature_and_value(data,feature_name,value_of_this_feature)

var p = parseFloat(data_set_without_this_feature_of_value.length/data.length)
new_entropy_value += p*shannon_entropy.calculate_shannon_entropy(data_set_without_this_feature_of_value)

})

// we can now have the informational gain for this specific feature
var informational_gain_value = base_entropy_value - new_entropy_value
// if the informational gain value is higher than the best one
if(informational_gain_value > best_informational_gain_value){
// it now become the best informational gain
best_informational_gain_value = informational_gain_value
// and the best feature has to be keep in memory
best_feature_name = feature_name
}
}

});
return best_feature_name;
}

get_the_best_feature_for_splitting(a)
// returns "has_a_car"

module.exports = {
get_the_best_feature_for_splitting:get_the_best_feature_for_splitting
}

Cette fonction doit détecter quelle est la feature qui apporte un gain informationnel maximal. Nous cherchons en effet à trouver la feature permettant de diviser nos exemples dans des “compartiments”, “compartiments” qui doivent avoir la particularité de regrouper des exemples de classes similaires, et ce dans une proportion la plus importante possible. Ainsi nous réduisons le désordre.

Dans les faits nous allons faire usage du calcul de l’entropie de Shannon pour calculer cette notion de différentiel de désordre.
Si on prend l’exemple de la feature has_a_car, on a donc deux valeurs possibles : vrai ou faux. Première étape : l’algorithme va donc générer un set de données ne contenant pas les exemples dont la has_a_car vaut vrai. On calcule ensuite le rapport entre le nombre d’éléments de notre nouveau set de données et le nombre d’élément que contient le set de données d’origine. On calcule ensuite l’entropie avec ce nouveau jeu de données. Ce processus est répété pour chaque valeur de la feature (donc 2 fois ici). On calcule ensuite pour chacune des feature le gain informationnel.

Ce dernier étant défini comme la différence entre l’entropie de base et la somme des entropies (pour chaque valeur de la feature). L’algorithme sélectionnera la feature qui permet d’obtenir le meilleur gain informationnel !

Construction de l’arbre de décisions de manière récursive

Il est temps de construire notre arbre. Nous allons le faire grâce à l’usage d’une fonction récursive, c’est à dire une fonction qui va elle même s’appeler au cours de son exécution. Pour plus d’informations sur la récursivité rendez-vous sur la page wikipédia : https://fr.wikipedia.org/wiki/Fonction_r%C3%A9cursive.

Lorsqu’on bâtit une fonction récursive il faut en premier lieu visualiser nos conditions de sorties. Nous allons donc itérer sur notre jeu de données d’entrainement afin à chaque fois de détecter la meilleur feature permettant de diviser notre jeu de données. Ainsi à chaque itération on isole la meilleure feature, on débarasse notre jeu de données de cette dernière. Cette meilleure feature sera donc une feuille de notre arbre. De cette feuille on aura autant de ramifications que de valeurs possibles à cette feature. Chaque feuille sera ensuite générée de suivant le même algorithme. Les conditions de sorties seront donc au nombre de deux :

  • Il ne reste plus de features dans le jeu de données
  • Il ne reste plus que des objets ayant tous la même classe.

Voici l’algorithme :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
var _ = require('lodash');
var data_set_splitter = require('./data_set_splitter')
var best_feature_selector = require('./best_feature_selector')
var shannon_entropy = require('./shannon_entropy')

/**
* This method will generate a decision tree
* @method generate_decision_tree
* @param {Array of object} data The dataset
* @return {Object} The decision tree/ ex : {"has_a_car":{"yes":{"has_a_house":{"yes":"good_borrower","no":"bad_borrower"}},"no":"bad_borrower"}}
*/
var generate_decision_tree = function(data){

// Because we build a recursive function
// we have to set our stopping conditions at the
// top.

// group the dataset by label
var grouped_data_per_label = _.groupBy(data, function(o){
return o.label
});

// We stop when there are only elements of the same class
// inside the dataset
//
if(_.size(grouped_data_per_label)===1){
return Object.keys(grouped_data_per_label)[0]
}



// If there is no more features to split
// The size of the first training is only 1 because
// there is just one property the class
// ex : data[0] = {"label":"good_borrower"}
if(_.size(data[0])===1){
return mostly_present_class(data)
}




// Get the best feature to split
var best_feature_for_splitting = best_feature_selector.get_the_best_feature_for_splitting(data)

// create the object decision tree
var decision_tree ={}
decision_tree[best_feature_for_splitting]={}

// we have then to extract the unique values of this feature in the dataset
// get unique values of this feature
var unique_values_of_best_feature = _.uniq(_.map(data, best_feature_for_splitting));
// forEach unique values of this feature
_.map(unique_values_of_best_feature, function(value_of_this_feature) {


// We get the dataset cleaned of examples that have a feature "best_feature_for_splitting"
// set to the value : "value_of_this_feature"
var new_data_set = data_set_splitter.split_the_dataset_by_feature_and_value(data,best_feature_for_splitting,value_of_this_feature)
decision_tree[best_feature_for_splitting][value_of_this_feature] = generate_decision_tree(new_data_set);

});


return decision_tree

}


module.exports={
generate_decision_tree : generate_decision_tree
}

On obtient donc le résultat suivant pour notre set de données d’exemple :

1
{"has_a_car":{"yes":{"has_a_house":{"yes":"good_borrower","no":"bad_borrower"}},"no":"bad_borrower"}}

L’ensemble du code se trouve sur mon repo github : https://github.com/maximilienandile/decision_tree_example

Base de données : comprendre les différences entre le modèle relationnel et le modèle multidimensionnel

Le modèle relationnel est dominant

Lorsqu’on est amené à construire une application web on utilise en général le modèle relationnel (cette question ne se pose pas toutefois si on travaille avec une base de données de type NoSQL, comme MongoDB, Couchbase …). Nous utilisons tous finalement le modèle relationnel sans forcément en avoir conscience. Sa manière de fonctionner étant relativement naturelle. D’ailleurs si nous revenons une étape avant, on peut légitimement s’interroger sur ce qu’est un modèle de base de données… Il s’agit simplement de règles de construction et d’utilisation d’une base de données. Un modèle est en général très normé afin que l’architecture d’une base reste compréhensible même en cas de changement d’équipe.

Les bases de données avec des modèles relationnels sont encore largement utilisées :

Répartition de l'utilisation des SGBD(Système de gestion de bases de données) en Octobre 2016, http://db-engines.com/en/ranking

Caractéristiques du modèle relationnel

Modéliser une donnée de façon relationnel revient à penser les données de son application sous forme d’une multitude d’objets qui disposent de relations permettant de les lier ensemble. La modélisation de la base de données amène à la génération d’un schéma de la base de données. Cette définition semble abstraite, prenons un exemple pour mieux la comprendre : le commerce électronique.

Pour un site e-commerce nous avons plusieurs objets : des clients, des commandes, des adresses, des produits, des fabricants. Ces objets disposent d’une multitude de liens entre eux. Par exemple un client dispose de plusieurs adresses, une commande de produits, un client de commandes … On voit aussi que si certains objets ont des relations multiples avec d’autres, on a aussi des relations uniques entre certains objets. Un produit à par exemple un seul et unique fabricant. Il ne peut en avoir deux…

Les différents types de relations

Ainsi les liens entre les objets sont de différents types :

  • 1-n : ou encore relations un à plusieurs par exemple entre l’objet client et l’objet commande : un client peut avoir plusieurs commandes, mais une commande ne peut avoir qu’un seul client.
  • n-n : ou encore relations plusieurs à plusieurs : un produit peut avoir plusieurs couleurs et une couleur n’est pas spécifique à un produit. Il peut y avoir plus d’un produit de couleur X.
  • 1-1 : ou encore les relations un à un par exemple si nous stockions les numéros de carte d’identité de nos clients. Un client ne peux avoir qu’un seul numéro de CNI et un numéro de CNI correspond à un unique client. Ce type de relation est toutefois extrêmement rare.

Tables et objets

Chaque famille d’objet est stocké dans une table. On a ainsi une table client, une table commande

Chaque objet client dispose d’un clé primaire. Cette clé est un identifiant unique d’un objet particulier stocké dans la table. Cette clé unique permet ensuite de lier l’objet en question à un autre objet cible (par exemple une commande). Dans la table des commandes on trouvera une clé primaire (qui permet d’identifier formellement la commande), mais aussi une clé étrangère client qui permet de lier cette commande à un client.

Limites du modèle relationnel

Le type de modélisation relationnel est particulièrement efficace pour traiter les données provenant d’applications transactionnelles. Par exemple pour un site e-commerce l’utilisation d’une modélisation relationnelle est bien adapté. On parle de systèmes de type OLTP (OnLine Transaction Processing). Un système de facturation client est ainsi assez aisé à créer dans le cadre du paradigme relationnel : on aura la table client, la table facture, la table des acomptes…etc chacune étant reliée grâce aux clés primaires (d’un côté) et étrangères (de l’autre).

Le modèle multidimensionnel : des requêtes analytiques performantes et plus facile à écrire

Mais pourquoi donc aurait-on besoin d’ajouter un autre schéma ? La réponse est simple lorsque les bases relationnelles commencent à accumuler un volume de données important le temps nécessaires aux requêtes analytiques augmente significativement. Précisons bien d’emblé que seules les requêtes analytiques sont concernées. Par exemple voiloir calculer un taux de rétention sur une table regroupant l’ensemble des activités des utilisateurs peut s’avérer très couteux en temps de calcul.

Le modèle multidimensionnel permet aussi de réduire la difficulté d’écriture des requêtes qui dans le cadre des bases classiques peuvent vite devenir illisible. Et qui dit illisible, dit temps perdu par le développeur ! Ainsi on peut interroger plus facilement ces modèles tout en ayant des temps de réponses bien meilleurs.

Comparaison des requêtes SQL & MDX

Par exemple dans le cas d’un modèle relationnel (en supposant un schéma permettant cette requête); pour avoir le nombre distinct de commandes d’un produit de la catégorie X par client on pourrait avoir une requête comme suis :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT c.first_name , c.last_name, COUNT(DISTINCT o.id)
FROM
order_detail od
-- Obtention de l'information produit à partir de la table order detail
INNER JOIN product p ON od.product_id = p.id
-- Obtention de la catégorie
INNER JOIN category c ON p.category_id = c.id
-- Obtention de la commande pour avoir le client
INNER JOIN order o ON od.order_id = o.id
-- Obtention de la données client (pour pouvoir afficher la data par client)
INNER JOIN client c ON o.client_id = c.id
-- Nous ne souhaitons que les produits d'id X
WHERE c.name = 'X'
-- On groupe les résultats par client
GROUP BY c.first_name+' '+c.last_name

On est obligé de faire une jointure sur plusieurs tables et la requête devient assez longue et peu élégante, de suite elle est plus complexe à débugger. Tandis qu’on aura une version plus courte de la requête avec l’aide du MDX (un simili SQL qui permet d’interroger des bases multidimensionnelles de manière plus simple) :

1
2
3
4
SELECT [Measures].[Orders Count] ON COLUMNS,
[Customer].[Customer].Members ON ROWS
FROM [Data Warehouse project]
WHERE [Product].[Product Categories].[Category].[X]

Beaucoup plus simple et lisible !

Dimensions, faits et mesures : le modèle en étoile

Voici un modèle d’organisation multidimensionnel (aussi appelé OLAP) :

Le modèle multidimensionnel en étoile (star schema)

On voit donc qu’on organise la données en pensant d’abord aux analyses qui vont y être faite. Ici on veut pouvoir comprendre la répartition du profit et du revenu net en fonction du produit, du client, de la catégorie, du code postal de livraison, de la méthode de livraison ! On va donc créer nos dimensions d’analyse : on aura donc 5 dimensions. Ce qui se manifestera dans le schéma par 5 tables de dimensions.

A ces 5 tables de dimensions on ajoute une table centrale, qui est la table de fait (fact table). Cette table de fait va regrouper les clés étrangères des tables de dimensions (ID_client, ID_product, …) mais aussi nos mesures(ou metrics) : ici le profit, le revenu net et la TVA.

On voit donc bien maintenant pourquoi il s’agit d’un modèle en étoile. Les dimensions ne sont pas liées entre elles et c’est la table de fait qui fait le lien entre les mesures et l’ensemble des dimensions !

Les métiers peuvent interroger directement les bases !

Par la suite on peut donner la possibilité au métier d’interroger directement la base afin de faire des analyses à volonté à partir de ces dimensions. De nombreuses solutions d’interrogations existent. L’une de mes préférée est Saiku (http://www.meteorite.bi/) qui dispose d’une version open source ! Les métiers peuvent générer des tableaux à la volée en couplant les dimensions, sans avoir à maitriser le MDX !

Le modèle dispose encore de bien d’autres avantages et caractéristiques, j’essayerai ‘écrire à nouveau sur ce sujet passionnant.

Lectures :

Comment installer PHPUnit sur un projet nécessitant composer et utilisant des variables d'environnement

Installation de PHPUnit (macOS)

Afin d’installer PHPUnit nous allons devoir ouvrir le terminal et taper les commandes suivantes :

1
2
3
$ curl https://phar.phpunit.de/phpunit.phar -L -o phpunit.phar
$ chmod +x phpunit.phar
$ mv phpunit.phar /usr/local/bin/phpunit

Ces commandes permettent de télécharger le fichier .phar. (Un fichier .PHAR est une PHP Archive, une application PHP compactée dans un seul et même fichier). Elles permettent ensuite d’étendre le niveau des droits sur ce dernier fichier (lecture, écriture, exécution). Nous déplaçons ensuite phpunit.phar dans le dossier /usr/local/bin/phpunit afin que nous puissions l’exécuter directement depuis notre terminal via la commande phpunit.

Création du fichier phpunit.xml contenant l’ensemble des variables d’environnement

Si vos scripts PHP font usage de variables d’environnement. Vous vous devez de fournir une valeur à ces variables dans vos tests unitaires. En effet si vous exécutez vos tests unitaires sans ces variables définies vos test échoueront, faute de variables définies.

PHPUnit permet la configuration de ces variables de manière très simple. Il vous suffit de créer un fichier nommé phpunit.xml à la racine du dossier de votre application :

1
2
3
4
5
6
7
8
9

<phpunit bootstrap="bootstrap_file.php">
<php>
<env name="DB_HOST" value="127.0.0.1:3306"/>
<env name="DB_USER" value="root"/>
<env name="DB_PASS" value="root"/>
...
</php>
</phpunit>

Dans notre cas nous définissons ici 3 variables d’environnement nommées DB_HOST, DB_USER et DB_PASS ayant respectivement les valeurs 127.0.0.1:3306, root, root.

D’autre part dans ce fichier de configuration est aussi définit un fichier de bootstrap qui va nous permettre de créer une procédure de démarrage spécifique au test de notre application.

Création du script de bootstrap.

Notre script de bootstrap devra effectuer une tâche :

  • Charger l’ensemble des classes que nous avons développées afin que nous n’ayons pas à faire des require dans chaque fichier décrivant un test unitaire. Cette astuce permet de gagner un temps précieux !
1
2
3
4
5
6
7
8
<?php
function my_autoloader($class) {
include 'classes/' . $class . '.php';
}

spl_autoload_register('my_autoloader');

?>

Si toutes vos classes sont regroupées dans le dossier classes (sans sous-dossiers) situé à la racine de votre application vous pouvez adopter cet autoloader simple. Une autre contrainte est que le nom de vos classes doit correspondre trait pour trait au nom du fichier dans lesquels elles sont définies.

Jess Telford fournit une implémentation plus complexe permettant d’inclure les sous dossiers : http://jes.st/2011/phpunit-bootstrap-and-autoloading-classes/.

Un test unitaire

1
2
3
4
5
6
7
8
9
10
11
<?php
date_default_timezone_set('America/Los_Angeles');
// require composer classes
require_once __DIR__ .'/../vendor/autoload.php';
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{


}

Et voilà. Vous pouvez tester des fonctions disposant de variables d'environnement et tester toutes vos classes sans avoir à penser à appeler require à chaque utilisation d’une nouvelle classe.

Comprendre les headers d'une requête HTTP

HTTP

HTTP signifie (Hypertext Transfer Protocol), il s’agit d’un protocole de communication entre ordinateurs connectés au réseau internet*. Lorsque nous échangeons avec nos semblables nous utilisons nous même un protocole de communication : il s’agit de notre langue et de notre grammaire. L’Internet dispose aussi d’un langage et d’une grammaire bien déterminée et il s’agit d’HTTP.

Contrairement au français ou comprendre et apprendre la grammaire nécessite un apprentissage long et douloureux, la grammaire du web est définit sous forme de RFC (“request for comments”), il s’agit d’une pléthore de documents décrivant les règles de fonctionnement de notre web. Plus précisément, c’est la RFC 2616 qui définit le fonctionnement du protocole HTTP version 1.1.

Client et Serveur

Le trajet d'une requête HTTP

Sur le schéma précédent nous voyons que les requêtes HTTP sont émises depuis le client. Le client est dans ce cas le navigateur web utilisé (ici Chrome). Ce dernier va envoyer au serveur la requête HTTP (la question). Ce dernier va ensuite traité la requête et renvoyer la ressource correspondante au client.

Les principales caractéristiques du protocole

  1. Les échanges du protocoles HTTP repose sur TCP/IP qui est une suite de protocoles destinés au transfert de données sur le réseau internet.
  2. On peut transférer via HTTP n’importe quel type de média ! Que cela soit du texte, du HTML, du JSON, des images, des films (pensez aux sites de streaming qui reposent grandement sur HTTP).
  3. Le protocole HTTP est sans état, c’est à dire que le serveur ne se souviendra pas de vous entre vos requêtes, d’une certaine façon HTTP n’a pas de mémoire. Et cela lui va très bien (cf REST)

Visualisation d’une requête HTTP

Comment peut on visualiser une requête HTTP dans Chrome ? Rien de plus simple il suffit de :

  1. Cliquer sur Afficher
  2. Puis sur Options pour les développeurs
  3. Puis cliquez sur outil de développement

Vous verrez s’afficher la fenêtre vous permettant de surveiller l’activité de networking de votre navigateur, autrement dit toutes les requêtes qui sortent de votre machine :

Les requêtes HTTP dans Chrome

Nous voyons donc ici toutes les requêtes lancées par chrome à la visite du site Le Monde. Et cela en fait un sacré paquet ! En effet les éléments du site sont chargés via HTTP. Souvenez vous, ce protocole est multimédia ! On va y recourir (enfin notre navigateur) pour charger les images, les feuilles de style CSS, les pages HTML, le favicon…

Voici un exemple de requête HTTP.

1
2
Request URL:http://www.lemonde.fr/
Request Method:GET

L’URL permet de router notre requête vers le serveur correspondant au site web sur le réseau. Ensuite on précise la méthode, ici il s’agit de GET. La méthode GET est surtout celle utilisée pour charger les images et les pages HTML. Il existe d’autres méthodes, appelées aussi verbes comme POST qui est utilisée notamment pour envoyer les données d’un formulaire sur un serveur. (par exemple pour envoyer votre formulaire d’inscription à Airbnb).

Mais une requête HTTP est aussi composée d’un header dont voici un exemple :

1
2
3
4
5
6
7
8
9
10
11
GET / HTTP/1.1
Host: www.lemonde.fr
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.34 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: _ga=GA1.2.374864853.1475228586; xtvrn=$43260$; _cb_ls=1; lmdAbDay=1; __vrf=14752522666255dKTLyAPMbCEHo57kMWgskL26Q4Wie1W; __vrrefresh=http%253A%252F%252Fwww.lemonde.fr%252F; __utmt=1; __utma=194882738.374864853.1475228586.1475238480.1475252268.2; __utmb=194882738.1.10.1475252268; __utmc=194882738; __utmz=194882738.1475238480.1.1.utmcsr=google|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided); xtan43260=-; xtant43260=1; _cb=DQQzWqCueIlMC6ZCTd; _chartbeat2=.1475238481310.1475252269278.1; cdx_ses_1_10013=3342761584&1577be3312a&1577be3312a&; kameleoonVisit=ml6c91nkhia4vvot/1/1475252301068/20160909/12726701/true
If-Modified-Since: Fri, 30 Sep 2016 16:10:47 GMT

Chaque ligne est générée par notre client (notre navigateur web). D’un point de vue développeur il ne faut d’ailleurs jamais se fier au header car ces données peuvent être manipulées avant d’être envoyées et changées à volonté par n’importe quel développeur.

Voici le détail du header avec des explications pour chaque donnée

Propriété Description Exemple
Host Il s’agit du nom de domaine du serveur auquel on envoie la requête, on pécise aussi le port si celui-ci n’est pas 80 www.lemonde.fr
Connection Il s’agit du comportement de la connexion. Ici on va garder la connection TCP ouverte entre le moment entre les requêtes réponses, au lien d’en ouvrir une autre à chaque reprise keep-alive
Cache-Control Cette propriété indique au cache du serveur s’il doit recharger la page à chaque reprise ou suivant un délai précis. Ici c’est le cas, on doit recharger à chaque fois la page max-age=0
Upgrade-Insecure-Requests Indique au client que l’on préfèrera toujours une reconnection HTTPS si jamais celui-ci est en train de migrer entre HTTP et HTTPS 1
User-Agent Il s’agit de la chaine de caractère permettant d’identifier formellement le navigateur qui a généré la requête; on y trouve sa version son développeur … Renseignement très intéressant pour le web analytics Mozilla/5.0 (Macintosh; ….
Accept Indique le type de contenu (de média) que le client est en mesure d’accepter. text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Referer Indique la page d’où vient le visiteur. (très utile pour l’analyse de la provenance des visiteurs). Vous noterez la coquille (il manque un r) qui provient directement d’une erreur sur la RFC :) http://www.lemonde.fr/
Accept-Encoding Spécifie le type de compression du contenu que le client (le navigateur) peut traiter. Le contenu est en effet souvent compresser pour optimiser le temps de transfert ! gzip, deflate, sdch
Accept-Language Spécifie quel langage le client accepte fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4
Cookie On retrouve ici le cookie (une série de données) qui avait été précédemment enregistré sur le navigateur de l’utilisateur et qui est renvoyé au serveur lors de chaque requêtes. On retrouve d’ailleurs ici des traces de Google Analytics _ga=GA1.2.374864853.1475228586; xtvrn=$43260$; _cb_ls=1
If-Modified-Since Permet au serveur de renvoyer la réponse “304 Not modified” si jamais la page n’a pas été modifiée depuis la date indiquée Fri, 30 Sep 2016 16:16:34 GMT

Précisons d’ailleurs que l’on peut mettre en place un système d’authentification utilisateur via les headers HTTP ! Le client rajoute simplement dans sa requête le nom d’utilisateur et le mot de passe demandé à l’utilisateur via une fenêtre native. Ce nom d’utilisateur est de mot de passe est encodé en base 64

1
Authorization: Basic XXXXXXXXXXXX

XXXXXXXXXXXX représentant l’encodage de la chaine nom_de_l_utilisateur : mot_de_passe ! Remarquez aussi que lorsque vous transmettez des données sensibles sur vos sites favoris et que ces derniers n’utilisent pas HTTPS (la version sécurisée du protocole HTTP) cela représente un réel danger, car toute requête peut être interceptée ! et dans une requête POST de transmission de formulaire vos données sont lisibles! Faites l’essai en essayant de remplir un formulaire et en surveillant la requête HTTP qui sera émise !

J’espère que vous aurez maintenant envie d’ouvrir votre inspecteur chrome afin d’inspecter les requêtes que fait votre navigateur ! Cela peut être un moyen intéressant pour découvrir tous les services qu’un site utilise (par exemple les réseaux de pubilicités, les services de web analytics…). Fouillez !