From aff3366c0c397f51439ac487c30788d732f4c270 Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Tue, 8 Sep 2020 20:32:34 +0200 Subject: [PATCH] Integrates Gwift right into Django-concepts, for a better illustration. --- source/gwift/console.adoc | 6 - source/gwift/key-points.adoc | 100 ---------------- source/gwift/metamodel.adoc | 7 -- source/part-1-workspace/00-main.adoc | 4 +- source/part-1-workspace/solid.adoc | 66 +++++++++++ source/part-3-django-concepts/00-main.adoc | 2 + source/part-3-django-concepts/models.adoc | 110 +++++++++++++++++- source/part-3-django-concepts/shell.adoc | 1 + .../part-3-django-concepts/template-tag.adoc | 4 - source/part-3-django-concepts/templates.adoc | 12 +- 10 files changed, 187 insertions(+), 125 deletions(-) delete mode 100644 source/gwift/console.adoc delete mode 100644 source/gwift/key-points.adoc delete mode 100644 source/gwift/metamodel.adoc create mode 100644 source/part-1-workspace/solid.adoc create mode 100644 source/part-3-django-concepts/shell.adoc delete mode 100644 source/part-3-django-concepts/template-tag.adoc diff --git a/source/gwift/console.adoc b/source/gwift/console.adoc deleted file mode 100644 index 151fefd..0000000 --- a/source/gwift/console.adoc +++ /dev/null @@ -1,6 +0,0 @@ -***************************** -Jouons un peu avec la console -***************************** - -[TODO] - diff --git a/source/gwift/key-points.adoc b/source/gwift/key-points.adoc deleted file mode 100644 index 07efa90..0000000 --- a/source/gwift/key-points.adoc +++ /dev/null @@ -1,100 +0,0 @@ -== A retenir - -=== Constructeurs - -Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode `__init__`: créez plutôt une méthode static de type `create()`, en y associant les paramètres obligatoires ou souhaités: - -[source,python] ----- -class Wishlist(models.Model): - - @staticmethod - def create(name, description): - w = Wishlist() - w.name = name - w.description = description - w.save() - return w - -class Item(models.Model): - - @staticmethod - def create(name, description, wishlist): - i = Item() - i.name = name - i.description = description - i.wishlist = wishlist - i.save() - return i ----- - -Mieux encore: on pourrait passer par un `ModelManager` pour limiter le couplage; l''accès à une information stockée en base de données ne se ferait dès lors qu'au travers de cette instance et pas directement au travers du modèle. De cette manière, on limite le couplage des classes et on centralise l'accès. - -=== Relations - -==== Types de relations - -* ForeignKey -* ManyToManyField -* OneToOneField - -Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des **ForeignKey** d'une classe A vers une classe B. Il existe également les champs de type **ManyToManyField**, afin de représenter une relation N-N. Les champs de type **OneToOneField**, pour représenter une relation 1-1. -Dans notre modèle ci-dessus, nous n'avons jusqu'à présent eu besoin que des relations 1-N: la première entre les listes de souhaits et les souhaits; la seconde entre les souhaits et les parts. - -==== Mise en pratique - -Dans le cas de nos listes et de leurs souhaits, on a la relation suivante: - -[source,python] ----- -# wish/models.py - -class Wishlist(models.Model): - pass - - -class Item(models.Model): - wishlist = models.ForeignKey(Wishlist) ----- - -Depuis le code, à partir de l'instance de la classe `Item`, on peut donc accéder à la liste en appelant la propriété `wishlist` de notre instance. *A contrario*, depuis une instance de type `Wishlist`, on peut accéder à tous les éléments liés grâce à `_set`; ici `item_set`. - -Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l'attribut `related_name` afin de nommer la relation inverse. - -[source,python] ----- -# wish/models.py - -class Wishlist(models.Model): - pass - - -class Item(models.Model): - wishlist = models.ForeignKey(Wishlist, related_name='items') ----- - -A partir de maintenant, on peut accéder à nos propriétés de la manière suivante: - -[source,python] ----- -# python manage.py shell - ->>> from wish.models import Wishlist, Item ->>> w = Wishlist('Liste de test', 'description') ->>> w = Wishlist.create('Liste de test', 'description') ->>> i = Item.create('Element de test', 'description', w) ->>> ->>> i.wishlist - ->>> ->>> w.items.all() -[] ----- - -Remarque: si, dans une classe A, plusieurs relations sont liées à une classe B, Django ne saura pas à quoi correspondra la relation inverse. Pour palier à ce problème et pour gagner en cohérence, on fixe alors une valeur à l'attribut `related_name`. - -=== Querysets & managers - -* http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm -* https://docs.djangoproject.com/en/1.9/ref/models/querysets/#django.db.models.query.QuerySet.iterator -* http://blog.etianen.com/blog/2013/06/08/django-querysets/ diff --git a/source/gwift/metamodel.adoc b/source/gwift/metamodel.adoc deleted file mode 100644 index ce3acb1..0000000 --- a/source/gwift/metamodel.adoc +++ /dev/null @@ -1,7 +0,0 @@ -=== Métamodèle - -Sous ce titre franchement pompeux, on va un peu parler de la modélisation du modèle. Quand on prend une classe (par exemple, `Wishlist` que l'on a défini ci-dessus), on voit qu'elle hérite par défaut de `models.Model`. On peut regarder les propriétés définies dans cette classe en analysant le fichier `lib\site-packages\django\models\base.py`. On y voit notamment que `models.Model` hérite de `ModelBase` au travers de `six `_ pour la rétrocompatibilité vers Python 2.7. - -Cet héritage apporte notamment les fonctions `save()`, `clean()`, `delete()`, ... Bref, toutes les méthodes qui font qu'une instance est sait **comment** interagir avec la base de données. La base d'un `ORM `_, en fait. - -D'autre part, chaque classe héritant de `models.Model` possède une propriété `objects`. Comme on l'a vu dans la section **Jouons un peu avec la console**, cette propriété permet d'accéder aux objects persistants dans la base de données. diff --git a/source/part-1-workspace/00-main.adoc b/source/part-1-workspace/00-main.adoc index 9833f26..2cc7660 100644 --- a/source/part-1-workspace/00-main.adoc +++ b/source/part-1-workspace/00-main.adoc @@ -24,6 +24,8 @@ include::12-factors.adoc[] include::maintainable-applications.adoc[] +include::solid.adoc[] + include::environment.adoc[] include::venvs.adoc[] @@ -36,4 +38,4 @@ include::tools.adoc[] include::external_tools.adoc[] -include::summary.adoc[] \ No newline at end of file +include::summary.adoc[] diff --git a/source/part-1-workspace/solid.adoc b/source/part-1-workspace/solid.adoc new file mode 100644 index 0000000..705b814 --- /dev/null +++ b/source/part-1-workspace/solid.adoc @@ -0,0 +1,66 @@ +== SOLID + +. S : SRP (Single Responsibility +. O : Open closed +. L : LSP (Liskov Substitution) +. I : Interface Segregation +. D : Dependency Inversion + +=== Single Responsibility Principle + +Le principe de responsabilité unique définit que chaque concept ou domaine d'activité ne s'occupe que d'une et d'une seule chose. En prenant l'exemple d'une méthode qui communique avec une base de données, ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement quelconque. Cette action doit être prise en compte par une autre classe (ou un autre concept), qui s'occupera elle de définir l'emplacement où l'évènement sera enregistré (base de données, Graylog, fichier, ...). + +Cette manière d'organiser le code ajoute une couche d'abstraction (ie. "I don't care") sur les concepts, et centralise tout ce qui touche à type d'évènement à un et un seul endroit. Ceci permet également de centraliser la configuration pour ce type d'évènements, et augmenter la testabilité du code. + +=== Open Closed + +Un des principes essentiels en programmation orientée objets est l'héritage de classes et la surcharge de méthodes: plutôt que de partir sur une série de comparaisons pour définir le comportement d'une instance, il est parfois préférable de définir une nouvelle sous-classe, qui surcharge une méthode bien précise. Pour l'exemple, on pourrait ainsi définir trois classes: + +* Une classe `Customer`, pour laquelle la méthode `GetDiscount` ne renvoit rien; +* Une classe `SilverCustomer`, pour laquelle la méthode revoit une réduction de 10%; +* Une classe `GoldCustomer`, pour laquelle la même méthode renvoit une réduction de 20%. + +Si on rencontre un nouveau type de client, il suffit alors de créer une nouvelle sous-classe. Cela évite d'avoir à gérer un ensemble conséquent de conditions dans la méthode initiale, en fonction d'une autre variable (ici, le type de client). + +En anglais, dans le texte : "Putting in simple words the “Customer” class is now closed for any new modification but it’s open for extensions when new customer types are added to the project.". En résumé: on ferme la classe `Customer` à toute modification, mais on ouvre la possibilité de créer de nouvelles extensions en ajoutant de nouveaux types [héritant de `Customer`]. + +=== Liskov Substitution + +Le principe de substitution fait qu'une classe B qui hérite d'une classe A doit se comporter de la même manière que cette dernière. Il n'est pas question que la classe B n'implémente pas certaines méthodes, alors que celles-ci sont disponibles pour A. + +> [...] if S is a subtype of T, then objects of type T in a computer program may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T), without altering any of the desirable properties of that program (correctness, task performed, etc.). (Source: http://en.wikipedia.org/wiki/Liskov_substitution_principle[Wikipédia]). + +> Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S, where S is a subtype of T. (Source: http://en.wikipedia.org/wiki/Liskov_substitution_principle[Wikipédia aussi]) + +Ce principe s'applique à tout type de polymorphisme, et même aux langages de type *duck typing*: "when I see a bird that quacks like a duck, walks like a duck, has feathers and webbed feet and associates with ducks—I’m certainly going to assume that he is a duck" (Source: http://en.wikipedia.org/wiki/Duck_test[Wikipedia (as usual)]). Pour le cas émis ci-dessus, ce n'est donc pas parce qu'une classe a besoin **d'une méthode** définie dans une autre classe qu'elle doit forcément en hériter. Cela bousillerait le principe de substitution (et par la même occasion le *duck test*). + +=== Interface Segregation + +Ce principe stipule qu'un client ne peut en aucun cas dépendre d'une méthode dont il n'a pas besoin. Plus simplement, plutôt que de dépendre d'une seule et même (grosse) interface présentant un ensemble conséquent de méthodes, il est proposé d'exploser cette interface en plusieurs (plus petites) interfaces. Ceci permet aux différents clients de n'utiliser qu'un sous-ensemble précis d'interfaces, répondant chacune à un besoin particulier. + +L'exemple par défaut est d'avoir une interface permettant d'accéder à des éléments. Modifier cette interface pour permettre l'écriture impliquerait que toutes les applications ayant déjà accès à la première, obtiendraient (par défaut) un accès en écriture, ce qui n'est pas souhaité/souhaitable. + +Pour contrer ceci, on aurait alors une première interface permettant la lecture, tandis qu'une deuxième (héritant de la première) permettrait l'écriture. On aurait alors le schéma suivant : + +* A : lecture +* B (héritant de A) : lecture (par A) et écriture. + +=== Dependency inversion + +Dans une architecture conventionnelle, les composants de haut-niveau dépendant directement des composants de bas-niveau. Une manière très simple d'implémenter ceci est d'instancier un nouveau composant. L'inversion de dépendances stipule que c'est le composant de haut-niveau qui possède la définition de l'interface dont il a besoin, et le composant de bas-niveau qui l'implémente. + +Le composant de haut-niveau peut donc définir qu'il s'attend à avoir un `Publisher` pour publier du contenu vers un emplacement particulier. Plusieurs implémentation de cette interface peuvent alors être mise en place: + + * Une publication par SSH + * Une publication par FTP + * Une publication + * ... + +L'injection de dépendances est un patron de programmation qui suit le principe d'inversion de dépendances. + +=== Sources + +* http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp[Understanding SOLID principles on CodeProject] +* http://en.wikipedia.org/wiki/Software_craftsmanship[Software Craftmanship] +* http://lostechies.com/derickbailey/2011/09/22/dependency-injection-is-not-the-same-as-the-dependency-inversion-principle/[Dependency Injection is NOT the same as dependency inversion] +* http://en.wikipedia.org/wiki/Dependency_injection[Injection de dépendances] diff --git a/source/part-3-django-concepts/00-main.adoc b/source/part-3-django-concepts/00-main.adoc index b5d0ffb..70df8cb 100644 --- a/source/part-3-django-concepts/00-main.adoc +++ b/source/part-3-django-concepts/00-main.adoc @@ -17,6 +17,8 @@ En simplifiant, Django suit bien le modèle MVC, et toutes ces étapes sont lié include::models.adoc[] +include::shell.adoc[] + include::admin.adoc[] include::forms.adoc[] diff --git a/source/part-3-django-concepts/models.adoc b/source/part-3-django-concepts/models.adoc index 01d79cc..3d721ae 100644 --- a/source/part-3-django-concepts/models.adoc +++ b/source/part-3-django-concepts/models.adoc @@ -13,10 +13,68 @@ Assez de blabla, on démarre ! === Clés étrangères et relations +. ForeignKey +. ManyToManyField +. OneToOneField +Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des *ForeignKey* d'une classe A vers une classe B. Il existe également les champs de type *ManyToManyField*, afin de représenter une relation N-N. Les champs de type *OneToOneField*, pour représenter une relation 1-1. + +Dans notre modèle ci-dessus, nous n'avons jusqu'à présent eu besoin que des relations 1-N: la première entre les listes de souhaits et les souhaits; la seconde entre les souhaits et les parts. + +[source,python] +---- +# wish/models.py + +class Wishlist(models.Model): + pass + + +class Item(models.Model): + wishlist = models.ForeignKey(Wishlist) +---- + +Depuis le code, à partir de l'instance de la classe `Item`, on peut donc accéder à la liste en appelant la propriété `wishlist` de notre instance. *A contrario*, depuis une instance de type `Wishlist`, on peut accéder à tous les éléments liés grâce à `_set`; ici `item_set`. + +Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l'attribut `related_name` afin de nommer la relation inverse. + +[source,python] +---- +# wish/models.py + +class Wishlist(models.Model): + pass + + +class Item(models.Model): + wishlist = models.ForeignKey(Wishlist, related_name='items') +---- + +A partir de maintenant, on peut accéder à nos propriétés de la manière suivante: + +[source,python] +---- +# python manage.py shell + +>>> from wish.models import Wishlist, Item +>>> w = Wishlist('Liste de test', 'description') +>>> w = Wishlist.create('Liste de test', 'description') +>>> i = Item.create('Element de test', 'description', w) +>>> +>>> i.wishlist + +>>> +>>> w.items.all() +[] +---- + +Remarque: si, dans une classe A, plusieurs relations sont liées à une classe B, Django ne saura pas à quoi correspondra la relation inverse. Pour palier à ce problème et pour gagner en cohérence, on fixe alors une valeur à l'attribut `related_name`. === Querysets et managers +* http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm +* https://docs.djangoproject.com/en/1.9/ref/models/querysets/#django.db.models.query.QuerySet.iterator +* http://blog.etianen.com/blog/2013/06/08/django-querysets/ + L'ORM de Django (et donc, chacune des classes qui composent votre modèle) propose par défaut deux objets hyper importants: * Les managers, qui consistent en un point d'entrée pour accéder aux objets persistants @@ -37,7 +95,13 @@ Wish.objects.filter(name__icontains="test").filter(name__icontains="too") <2> -=== Propriétés Meta +=== Metamodèle + +Quand on prend une classe (par exemple, `Wishlist` que l'on a défini ci-dessus), on voit qu'elle hérite par défaut de `models.Model`. On peut regarder les propriétés définies dans cette classe en analysant le fichier `lib\site-packages\django\models\base.py`. On y voit notamment que `models.Model` hérite de `ModelBase` au travers de https://pypi.python.org/pypi/six[six] pour la rétrocompatibilité vers Python 2.7. + +Cet héritage apporte notamment les fonctions `save()`, `clean()`, `delete()`, ... Bref, toutes les méthodes qui font qu'une instance est sait **comment** interagir avec la base de données. La base d'un https://en.wikipedia.org/wiki/Object-relational_mapping[ORM], en fait. + +D'autre part, chaque classe héritant de `models.Model` possède une propriété `objects`. Comme on l'a vu dans la section **Jouons un peu avec la console**, cette propriété permet d'accéder aux objects persistants dans la base de données, au travers d'un `ModelManager`. En plus de cela, il faut bien tenir compte des propriétés `Meta` de la classe: si elle contient déjà un ordre par défaut, celui-ci sera pris en compte pour l'ensemble des requêtes effectuées sur cette classe. @@ -59,12 +123,52 @@ Les propriétés de la classe Meta les plus utiles sont les suivates: === Migrations -Les migrations (comprendre les "migrations du schéma de base de données") sont intimement liées à la représentation d'un contexte fonctionnel. L'ajout d'une nouvelle information, d'un nouveau champ ou d'une nouvelle fonction peut s'accompagner de tables de données à mettre à jour ou de champs à étendre. +Les migrations (comprendre les "_migrations du schéma de base de données_") sont intimement liées à la représentation d'un contexte fonctionnel. L'ajout d'une nouvelle information, d'un nouveau champ ou d'une nouvelle fonction peut s'accompagner de tables de données à mettre à jour ou de champs à étendre. -Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code. Le développeur s'occupe de créer les migrations en fonction des actions à entreprendre; ces migrations peuvent être retravaillées, _squashées_, ... et feront partie intégrante du processus de mise à jour de l'application. +Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code. Le développeur s'occupe de créer les migrations en fonction des actions à entreprendre; ces migrations peuvent être retravaillées, _squashées_, ... et feront partie intégrante du processus de mise à jour de l'application. + +A noter que les migrations n'appliqueront de modifications que si le schéma est impacté. Ajouter une propriété `related_name` sur une ForeignKey n'engendrera aucune nouvelle action de migration, puisque ce type d'action ne s'applique que sur l'ORM, et pas directement sur la base de données: au niveau des tables, rien ne change. Seul le code et le modèle sont impactés. === Shell === Les validateurs + +=== A retenir + +==== Constructeurs + +Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode `__init__`: créez plutôt une méthode static de type `create()`, en y associant les paramètres obligatoires ou souhaités: + +[source,python] +---- +class Wishlist(models.Model): + + @staticmethod + def create(name, description): + w = Wishlist() + w.name = name + w.description = description + w.save() + return w + +class Item(models.Model): + + @staticmethod + def create(name, description, wishlist): + i = Item() + i.name = name + i.description = description + i.wishlist = wishlist + i.save() + return i +---- + +Mieux encore: on pourrait passer par un `ModelManager` pour limiter le couplage; l'accès à une information stockée en base de données ne se ferait dès lors qu'au travers de cette instance et pas directement au travers du modèle. De cette manière, on limite le couplage des classes et on centralise l'accès. + +[source,python] +---- +class ItemManager(...): + (de mémoire, je ne sais plus exactement :-)) +---- diff --git a/source/part-3-django-concepts/shell.adoc b/source/part-3-django-concepts/shell.adoc new file mode 100644 index 0000000..cd86173 --- /dev/null +++ b/source/part-3-django-concepts/shell.adoc @@ -0,0 +1 @@ +== Shell diff --git a/source/part-3-django-concepts/template-tag.adoc b/source/part-3-django-concepts/template-tag.adoc deleted file mode 100644 index fe531b3..0000000 --- a/source/part-3-django-concepts/template-tag.adoc +++ /dev/null @@ -1,4 +0,0 @@ -Templates tags --------------- - -https://www.djangotemplatetagsandfilters.com/[Django Templates and Filters] \ No newline at end of file diff --git a/source/part-3-django-concepts/templates.adoc b/source/part-3-django-concepts/templates.adoc index 2c59946..87444c7 100644 --- a/source/part-3-django-concepts/templates.adoc +++ b/source/part-3-django-concepts/templates.adoc @@ -1,8 +1,12 @@ == Templates -Avant de commencer à interagir avec nos données au travers de listes, formulaires et IHM sophistiquées, quelques mots sur les templates: il s'agit en fait de *squelettes* de présentation, recevant en entrée un dictionnaire contenant des clés-valeurs et ayant pour but de les afficher dans le format que vous définirez. En intégrant un ensemble de *tags*, cela vous permettra de greffer les données reçues en entrée dans un patron prédéfini. +Avant de commencer à interagir avec nos données au travers de listes, formulaires et d'interfaces sophistiquées, quelques mots sur les templates: il s'agit en fait de *squelettes* de présentation, recevant en entrée un dictionnaire contenant des clés-valeurs et ayant pour but de les afficher selon le format que vous définirez. -Une page HTML basique ressemble à ceci: +En intégrant un ensemble de *tags*, cela vous permettra de greffer les données reçues en entrée dans un patron prédéfini. + +NOTE: (je ne sais plus ce que je voulais dire ici) + +Un squelette de page HTML basique ressemble à ceci: [source,html] ---- @@ -117,7 +121,7 @@ TEMPLATES = [ === Builtins -Django vient avec un ensemble de *tags*. On a vu la boucle `for` ci-dessus, mais il existe https://docs.djangoproject.com/fr/1.9/ref/templates/builtins/[beaucoup d'autres tags nativement présents]. Les principaux sont par exemple: +Django vient avec un ensemble de *tags* ou *template tags*. On a vu la boucle `for` ci-dessus, mais il existe https://docs.djangoproject.com/fr/1.9/ref/templates/builtins/[beaucoup d'autres tags nativement présents]. Les principaux sont par exemple: * `{% if ... %} ... {% elif ... %} ... {% else %} ... {% endif %}`: permet de vérifier une condition et de n'afficher le contenu du bloc que si la condition est vérifiée. * Opérateurs de comparaisons: `<`, `>`, `==`, `in`, `not in`. @@ -134,7 +138,7 @@ from django.db import models from django.template.defaultfilters import urlize -class Suggestion(moels.Model): +class Suggestion(models.Model): """Représentation des suggestions. """ subject = models.TextField(verbose_name="Sujet")