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.