CollectionGeneriquesEtSOLID - Site personnel Hugo St

publicité
Révision et
principes SOLID
Architecture d’application
Hugo St-Louis
Introduction
aux principes
SOLID et
structures
génériques
Architecture d’application
Hugo St-Louis
Plan
 Structures
génériques
 Principes SOLID
 Conclusion
Lien avec l’objectif

Objectif Intégrateur :
Développer une application utilisant une
architecture qui permet d’atteindre les
critères de qualité logicielle définis dans
l’analyse fonctionnelle.

Aujourd’hui, nous allons voir les principes qui
guiderons la modélisation de notre modèle
objets.
Nous verrons aussi comment bien sélectionner
la structure de données qui convient pour
stocker nos données en mémoire.

Introduction



En informatique, une structure de données est une
structure logique destinée à contenir des
données, afin de leur donner une organisation
permettant leur traitement. La connaissance de
celles-ci permet de choisir celle qui répond le
mieux à une problématique donnée.
Je ne présenterai pas toutes les collections mais
les principales différences.
Dès le départ, on peut classer ces collections en
deux « familles ».


Avec clé
Sans clé
Notation

Pour mesurer l’efficacité d’un algorithme, on va mesurer le temps
qu’il utilise. Il suffit de compter les instructions et de les pondérer par
leur coût. On retrouve plusieurs types d’opérations :






Ajouter / Supprimer des enregistrement
Lecture / affectation d’enregistrement
Rechercher un enregistrement
Supprimer des enregistrement
Insérer des enregistrements
Pour noter la complexité des algorithmes la notion de O(n) est
utilisés. O(n) signifie « au pire n opérations ». Pour que tout le
monde comprenne voici quelques exemples. Imaginons que l’on
est une liste contenant 1 million d’éléments. Si un algorithme est en
O(1) : Quel que soit le nombre d’éléments, le temps est toujours
identique.
Présentation des structures
sans clé
Classe
List
Description et usage
• Il s’agit d’un tableau qui augmente sa taille lorsque nécessaire. Sa taille
initiale par défaut est 4.
• Lorsque l’on essaye d’insérer un élément alors que le tableau est plein,
un nouveau tableau est alloué. Celui-ci est 2 fois plus grand que le
précédent.
• Le contenu de l’ancien tableau est copié dans le nouveau et le nouvel
élément peut être ajouté à la suite.
• Pour insérer un élément à une position précise il faut commencer par
déplacer tous les éléments suivants du tableau d’une case et ensuite
placer le nouvel élément à sa place.
• La suppression consiste à déplacer tous les éléments suivants d’une case
en arrière. Pour rechercher un élément il faut parcourir toute la liste
élément par élément jusqu’à trouver le bon. Les éléments ne sont pas
nécessairement trié.
• Particulièrement bien adapté pour un petit nombre d’éléments.
• La recherche est lente et l’ajout/suppression peut être difficile s’il faut
redémiensionner souvent le tableau ou faire de l’insertion.
• Peut contenir plusieurs fois la même valeur à des index différent.
• Performant sur un ajout frequent de petite taille et consommé peu de
mémoire.
Présentation des structures
sans clé
Classe
Description et usage
SortedSet
• Organise les objets en mémoires avec une forme d’arbre bi-colore.
Chaque nœuds à un enfant plus grand et plus petit.
• La recherche se fait sur un arbre, par conséquent nous aurons une
recherche efficace O(Log n ).
• L’arbre utilisé est particulier et permet d’ajouter et d’insérer en O(Log n)
aussi.
Complexité
Type
Get ([i])
Rechercher
Ajouter
Insérer
Supprimer
List
O(1)
O(n)
O(1)*
O(n)
O(n)
SortedSet
N/A
O(logn)
O(logn)
O(logn)
O(logn)
Complexity
* List.Add est O(n) lorsqu’il faut redimensionner le tableau.
Présentation des structures
sans clé
Classe
Description et usage
HashSet
• Cette dernière fournit une collection qui ne contient aucun élément en
double et dont ces éléments ne sont pas placés dans un ordre particulier.
• Le HashSet est une table de hachage avec résolution des collisions par
chainage coalescent.
• Pour insérer un élément, la fonction commence par calculer la valeur de
hachage (méthode GetHashCode définie pour tous les objets) et en déduit
ainsi la position de l’élément dans le tableau.
• Si la case est vide il ajoute l’élément. Si la case contient déjà une valeur
(conflit), un principe de chainage est utilisé. C’est-à-dire que l’on cherche une
case vide à partir de la fin du tableau et on ajoute notre élément. A
l’emplacement on l’on aurait dû insérer l’élément on ajoute une information
indiquant la case qui a été utilisée pour stocker une valeur ayant la même
valeur de hachage. Pour rechercher un élément, on calcule son hash et on
cherche la case correspondante. Si la valeur contenue dans cette case
correspond à la valeur cherchée : Bingo. Autrement on regarde s’il y a eu une
collision (il y a un lien vers une autre case) et on suit le « lien ». Si ce n’est
toujours pas la bonne valeur on recommence : on vérifie s’il y a encore un lien
et on le suit le cas échéant. On continue ainsi jusqu’à ce qu’il n’y ait plus de
lien à suivre. On considère qu’une fonction de hachage est bonne s’il n’y a
pas plus de 5 éléments de la collection ayant la même valeur de hachage. En
moyenne la recherche est en O(1). Cependant si la fonction de hachage est
mauvaise et génère trop de collision, la recherche peut être en O(n).
Présentation des structures
sans clé
Classe
Description et usage
Stack
• Implémente une philosophie LIFO
Queue
• Implémente une philosophie FIFO
BitArray
•
•
Tableau de bit seulement.
Accessible avec un index.
LinkedList
•
ll s’agit d’une liste circulaire doublement chainée. Cela permet de faire des
insertions en tête et en queue en 0(1). En effet pour insérer un élément en
queue, il suffit de l’insérer avant l’élément de tête car la liste est circulaire.
Pour rechercher un élément il faut parcourir toute la liste élément par
élément jusqu’à trouver le bon, ce qui n’est pas des plus efficace.
Trié Versus non trié
 Recherche

VS
sans clé
Si la structure est trié, la recherche sera
nécessairement plus performante, par
contre elle sera plus difficile à maintenir.
1
2
3
4
5
6
7
8
9
10
3
2
2
1
8
5
6
4
7
8
Présentation des structures
Avec clé
Classe
Description et usage
SortedList
• Son fonctionnement est identique à List<T>.
Cependant les éléments dans le tableau sont
triés par ordre croissant et on utilise une clé.
• La recherche est assez rapide. Pour cela une
recherche dichotomique est effectuée
(O(log(n)).
• Lors de l’ajout, il faut donc commencer par
chercher sa place dans le tableau. Il est
possible que cette opération soit gourmande
si on doit déplacer tous les éléments.
• Mélange entre une List et un Dictionnary où
ont peut accéder aux éléments avec la
clé(recherche dichotomique) ou
l’index(accès direct).
Problème de l’insertion et de
la suppression
 L’ajout
de élément peut nécessiter la
modifier la structure entière.
 Recherche dichotomique(n(logn))
1
2
3
4
5
6
7
8
9
….
Présentation des structures
Avec clé
Classe
Description et usage
Dictionary
• Utilise une clé unique pour identifier éléments.
• Gestions des collisions nécessaire
• Le dictionnaire fonctionne comme le HashSet
sauf que la valeur de hachage est calculée à
partir de la clé (TKey) et non de la valeur
(TValue).
• Accès rapide aux éléments
Présentation des structures
Avec clé
Classe
Description et usage
SortedDictionary
• Est un compromis entre vitesse et
ordonnancement.
• Pareille comme le SortedSet<T> mais avec
une clé.
• Recherche avec un arbre.
• La recherche est moins rapide si on connait la
clé, mais plus rapide si on doit parcourir les
données.
Complexité
Type
Recherche
par la clé
Supression
Ajout
HashSet
O(1)*
O(1)*
O(1)**
Dictionary
1)O(1)*
O(1)*
O(1)**
SortedList
O(logn)
O(n)
O(n)
SortedDictionary
O(logn)
O(logn)
O(logn)
Complexité
* O(n) avec les collisions
** O(n) avec les collision ou lorsqu’il faut redimensionner le tableau.
Collections génériques
 Insertion
En tête
En
queue
Générale
LinkedList
O(n)
O(1)
O(1)
List
O(n)
O(n)
O(1)
O(log(n))
N/A
N/A
Dictionnary
O(1)
N/A
N/A
HashSet
O(1)
N/A
N/A
SortedSet
O(log(n))
N/A
N/A
SortedList
O(n)
N/A
N/A
SortedDictionnary
Collections génériques
 Suppression
En tête
En
queue
Général
e
LinkedList
O(n)
O(1)
O(1)
List
O(n)
O(n)
O(1)
O(log(n))
N/A
N/A
Dictionnary
O(1)
N/A
N/A
HashSet
O(1)
N/A
N/A
SortedSet
O(log(n))
N/A
N/A
SortedList
O(n)
N/A
N/A
SortedDictionnary
Collections génériques
 Accès
(i-ème élément)
LinkedList
O(n)
List
O(1)
SortedList
O(1)
Collections génériques
 Recherche
LinkedList
O(n)
List
O(n)
SortedDictionnary
O(log(n))
Dictionnary
O(n) mais en
moyenne O(1)
HashSet
O(n) mais en
moyenne O(1)
SortedSet
O(log(n))
SortedList
O(log(n))
Expérimentation

Expérimentons la recherche, l’ajout, la
suppression, et la vérification d’existence
avec les collections suivantes:

Sans clé
List<int>
 HashSet<int>
 SortedSet<int>


Avec clé
Dictionnary<int, int>
 SortedDictionnary<int, int>
 SortedList<int , int>

Références
 Choisir
la bonne structure de données
 C#/.NET Fundamentals: Choosing the
Right Collection Class
 Various Collection Classes and Their
Usage
Introduction
aux principes
SOLID
Architecture d’application
Hugo St-Louis
Introduction - SOLID
 Les
principes SOLID sont des
principes fondamentaux au quotidien pour un
bon développeur, surtout pour des projets
d’entreprises qui doivent vivre dans le temps.
 L’utilisation de ces différents principes
permettra d’obtenir des applications :



plus faciles à faire évoluer
plus faciles à maintenir
plus robustes
Introduction - SOLID

SOLID est l'acronyme de cinq principes de base
que l'on peut appliquer au développement objet:






Single Responsibility Principle,
Open/Closed Principle,
Liskov Substitution Principle,
Interface Segregation Principle
Dependency Inversion Principle.
Basé sur la méthodologie AGILE, tels que décrits
dans le livre de Robert Martin, Agile Software
Development, Principles, Patterns, and Practices
Introudction - SOLID
 L’objectif



de SOLID:
permettre d'améliorer la cohésion,
de diminuer le couplage,
favoriser l'encapsulation d'un programme
orienté objet.
Introduction

Métrique d’un bon programme objet par
rapport à sa conception


Cohésion: Une classe => une tâche
Couplage: Liens entre les objets pour former un
tout.
Deux modules sont dit couplés si une
modification d'un de ces modules demande
une modification dans l'autre.
 L’héritage est un couplage fort alors qu’une
association est un couplage faible


Encapsulation: Rendre invisible l’implémentation
Responsabilité unique (SRP:
Single Responsibility Principle)

Définition:"Si une classe a plus d'une
responsabilité, alors ces responsabilités
deviennent couplées. Des modifications
apportées à l'une des responsabilités peuvent
porter atteinte ou inhiber la capacité de la
classe de remplir les autres. Ce genre de
couplage amène à des architectures fragiles
qui dysfonctionnent de façon inattendues
lorsqu'elles sont modifiées."
-- Robert C. Martin
Responsabilité unique (SRP:
Single Responsibility Principle)
Responsabilité unique (SRP:
Single Responsibility Principle)
 Le
principe de responsabilité unique,
réduit à sa plus simple expression, est
qu'une classe donnée ne doit avoir
qu'une seule responsabilité, et, par
conséquent, qu'elle ne doit avoir qu'une
seule raison de changer.
Responsabilité unique (SRP:
Single Responsibility Principle)
 Les
avantages de cette approche sont
les suivants:



Diminution de la complexité du code
Augmentation de la lisibilité de la classe
Meilleure encapsulation, et meilleure
cohésion, les responsabilités étant
regroupées
Comment appliquer
 Pour
une classe de taille importante, il est
souvent bénéfique de lister toutes les
méthodes, et de regrouper celles dont le
nom ou les actions semblent être de la
même famille. Si plusieurs groupes
apparaissent dans une classe, c'est un
bon indicateur que la classe doit être
reprise.
Comment appliquer
 Une
autre méthode est de regarder les
dépendances externes de la classe.


La méthode appelle-t-elle directement la
base de données ? Utilise-t'elle une API
spécifique ? Certains membres sont-ils
appelés uniquement par une fonction, ou
par un sous-ensemble de fonctions ?
Si c'est le cas, ce sont peut-être des
responsabilités annexes, dont il faut se
débarrasser..
Exemple
 Pour
faire simple, on va prendre un
mauvais exemple, que l'on va refactoriser.
 Le pattern utilisé n’est pas mauvais en soit,
mais il ne respecte pas les règles SOLID.
Exemple

Voir le mauvais exemple

En termes de responsabilités, cette classe a
les responsabilités:




de créer les objets
de stocker les données de l'objet
et de gérer la persistance des objets.
Voir le bon exemple.
Exemple
 Suite
à cette factorisation, les
responsabilités de nos trois classes sont
beaucoup plus évidentes.



la classe d'accès aux données ne traite
plus que des données,
l'objet possède des méthodes pour
manipuler ses propres données,
la factory a la responsabilité de faire
travailler ensemble la classe d'accès aux
données et l'objet...
Exemple
 Une
notion à garder à l'esprit est qu'il ne
faut pas aller trop loin dans la séparation
des responsabilités, au risque de tomber
dans un excès inverse.
Ouvert/fermé (OCP:
Open/closed Principle)
Définition: "Les modules qui se conforment au principe
ouvert/ferme ont deux attributs principaux.
1.
Ils sont "ouverts pour l'extension". Cela signifie que le
comportement du module peut être étendu, que l'on
peut faire se comporter ce module de façons nouvelles
et différentes si les exigences de l'application sont
modifiées, ou pour remplir les besoins d'une autre
application.
2.
Ils sont "Fermés à la modification". Le code source d'un
tel module ne peut pas être modifié. Personne n'est
autorisé à y apporter des modifications.
Le but de ce principe est de mettre en place des classes
pour lesquelles il sera possible d’ajouter de nouveaux
comportements sans modifier le code de la classe ellemême. Si ce n’est pas le cas, il faut modifier le design.

Ouvert/fermé (OCP:
Open/closed Principle)
 Pour
implémenter ce principe il suffit de
conserver un design simple, et lorsqu’on
arrive aux limites de ce design, d'en
changer...
Ouvert/fermé (OCP:
Open/closed Principle)
 Les
avantages de cette approche sont
les suivants:


Plus de flexibilité par rapport aux évolutions
Diminution du couplage
Ouvert/fermé (OCP:
Open/closed Principle)
 Comme
règles de bonne conduite, on
peut essayer d'une part de ne pas
dépendre du type d'un objet pour choisir
un chemin de traitement.
 D'autre part, on peut limiter l'héritage, en
y préférant la composition.
Exemple
 Voir
le bon et le mauvais exemple
La substitution de Liskov
 Définition
pour ceux qui veulent aller à
l’Université :
Si pour chaque objet o1 de type S il
existe un objet o2 de type T tel que
pour tout programme P défini en termes
de T, le comportement de P est
inchangé quand on substitue o1 à o2,
alors S est un sous-type de T.
La substitution de Liskov

Définition: Les sous-types doivent être
remplaçables par leur type de base.

L’ajout d’un élément dans la hiérarchie de type
ne modifie pas le comportement attendues. Si
c’est le cas, la structure hiérarchique de l’héritage
devra être modifié.

Là, je vais en voir un ou deux (ou plus) dire: « Oui,
mais à partir du moment où ma classe S hérite de
ma classe T », je dois pouvoir caster S en T et là ça
va marcher...
La substitution de Liskov
 Le
but de ce principe est exactement de
pouvoir utiliser une méthode sans que
cette méthode ait à connaitre la
hiérarchie des classes utilisées dans
l'application, ce qui veut dire:



pas de cast
pas de as
pas de is
La substitution de Liskov
La substitution de Liskov
 Ce


principe apporte:
Augmentation de l'encapsulation
Diminution du couplage. En effet, LSP
permet de contrôler le couplage entre les
descendants d'une classe et les clients de
cette classe.
La substitution de Liskov

Comment l'appliquer:

Pour détecter le non respect de ce
principe, on va se poser la question de
savoir si on peut, sans dommage,
remplacer la classe en cours par une
interface d'un niveau supérieur.
Exemple
 Bien
que ce soit compliquer à
comprendre le résultat est simple.
 Utiliser des noms significatifs pour pouvoir
redéfinir leur comportement plutôt que
de créer plusieurs méthodes.
Séparation des Interfaces (ISP:
Interface Segregation
Principle)
 Définition:
Les clients d'une entité
logicielle ne doivent pas avoir à
dépendre d'une interface qu'ils n'utilisent
pas.
 Ce principe apporte principalement une
diminution du couplage entre les classes
(les classes ne dépendant plus les unes
des autres). L'autre avantage d'ISP est
que les clients augmentent en robustesse.
Séparation des Interfaces (ISP:
Interface Segregation
Principle)

Application:



Par exemple la classe List implémente les interfaces suivantes (vous
pouvez consulter sa définition sur le site de Microsoft) :






On va réunir les groupes "fonctionnels" des méthodes de la classe
dans des Interfaces séparées.
L'idée étant de favoriser le découpage de façon à ce que des
clients se conformant à SRP n'aient pas à dépendre de plusieurs
interfaces.
IList,
ICollection,
IReadOnlyList,
IReadOnlyCollection,
IEnumerable.
En analysant le contenu de chaque interface, vous apercevrez
que chacune se rapporte à un rôle précis. Chaque interface est
simple à comprendre de manière unitaire.
Exemple
 Dans
nos exemples de Work Items, on va
devoir gérer des Work Items pour lesquels
il existe un deadline. Nos Work Items
dépendant tous de IWorkItem, on va
directement ajouter les informations de
gestion de deadline au niveau de
IWorkItem et de WorkItem.
Exemple
Exemple
 Jusqu'ici,
tout va bien...Sauf que le
marketing ne veut pas entendre parler de
deadline pour ses items. On peut donc,
soit renvoyer une information erronée,
pour continuer à utiliser le IWorkItem
courant, soit se conformer au principe ISP,
et séparer notre interface en IWorkItem et
IDeadLineDependent.
Exemple
Exemple
 L'intérêt
est que, si demain on a besoin
d'une fonction ExtendDeadline dans
IDeadLineDependent, cela n'impactera
pas les WorkItems ne comportant pas de
Deadline. Et si on ne le modifie pas, on
n'introduit pas de bugs.
Inversion des dépendances
(DIP: Dependency Inversion
Principle)
 Définition:

Les modules de haut niveau ne doivent pas
dépendre des modules de bas niveau. Les
deux doivent dépendre d'abstractions. Les
abstractions ne doivent pas dépendre des
détails. Les détails doivent dépendre des
abstractions.
Inversion des dépendances
(DIP: Dependency Inversion
Principle)

Définition:



Si on change le mode de fonctionnement de la
base de données(passage de Oracle à SQL
Server), du réseau (changement de protocole),
de système d'exploitation, les classes métiers ne
doivent pas être impactées.
Inversement, le fait de changer les règles de
validation au niveau de la partie métier ne doit
pas demander une modification de la base de
données.
En gros, c’est d’isoler les systèmes, ou groupes
de fonctionnalités ensemble.
Inversion des dépendances
(DIP: Dependency Inversion
Principle)
Inversion des dépendances
(DIP: Dependency Inversion
Principle)
 Avantages:


Une nette diminution du couplage,
Une meilleure encapsulation.
Inversion des dépendances
(DIP: Dependency Inversion
Principle)
 Comment

l'appliquer:
L'idée est que chaque point de contact
entre deux modules soit matérialisé par une
abstraction(interface).
Exemple
Exemple
Conclusion
 Les
principes SOLID dictes la philosophie à
adopter lors de la conception ou la
maintenance d’un système.
 L’architecture doit être repensé en cours
de développement.
 Ces points sont des repères que vous
dicterez les limites selon votre expérience.
Téléchargement