SRP, Open-Closed & Liskov Substitution
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Fred Pauchet 2022-04-14 20:56:45 +02:00
parent b45386ef48
commit 33e950334a
3 changed files with 322 additions and 420 deletions

View File

@ -1,407 +1,4 @@
\hypertarget{_single_responsibility_principle}{%
\subsubsection{Single Responsibility
Principle}\label{_single_responsibility_principle}}
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Document:}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, title, content, published\_at):}
\VariableTok{self}\NormalTok{.title }\OperatorTok{=}\NormalTok{ title}
\VariableTok{self}\NormalTok{.content }\OperatorTok{=}\NormalTok{ content}
\VariableTok{self}\NormalTok{.published\_at }\OperatorTok{=}\NormalTok{ published\_at}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, format\_type):}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"XML"}\NormalTok{:}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\VariableTok{self}\NormalTok{.title,}
\VariableTok{self}\NormalTok{.content,}
\VariableTok{self}\NormalTok{.published\_at.isoformat()}
\NormalTok{ )}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"Markdown"}\NormalTok{:}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(}\VariableTok{self}\NormalTok{.content)}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Format type \textquotesingle{}}\SpecialCharTok{\{\}}\StringTok{\textquotesingle{} is not known"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(format\_type))}
\end{Highlighting}
\end{Shaded}
Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML,
\ldots\hspace{0pt}), il sera nécessaire de modifier la classe
\texttt{Document}, ce qui n'est ni intuitif (\emph{ce n'est pas le
document qui doit savoir dans quels formats il peut être envoyés}), ni
conseillé (\emph{lorsque nous aurons quinze formats différents à gérer,
il sera nécessaire d'avoir autant de conditions dans cette méthode}).
Une bonne pratique consiste à créer une nouvelle classe de rendu pour
chaque type de format à gérer:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Document:}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, title, content, published\_at):}
\VariableTok{self}\NormalTok{.title }\OperatorTok{=}\NormalTok{ title}
\VariableTok{self}\NormalTok{.content }\OperatorTok{=}\NormalTok{ content}
\VariableTok{self}\NormalTok{.published\_at }\OperatorTok{=}\NormalTok{ published\_at}
\KeywordTok{class}\NormalTok{ DocumentRenderer:}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"XML"}\NormalTok{:}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\VariableTok{self}\NormalTok{.title,}
\VariableTok{self}\NormalTok{.content,}
\VariableTok{self}\NormalTok{.published\_at.isoformat()}
\NormalTok{ )}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"Markdown"}\NormalTok{:}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(}\VariableTok{self}\NormalTok{.content)}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Format type \textquotesingle{}}\SpecialCharTok{\{\}}\StringTok{\textquotesingle{} is not known"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(format\_type))}
\end{Highlighting}
\end{Shaded}
A présent, lorsque nous devrons ajouter un nouveau format de prise en
charge, nous irons modifier la classe \texttt{DocumentRenderer}, sans
que la classe \texttt{Document} ne soit impactée. En même temps, le jour
où une instance de type \texttt{Document} sera liée à un champ
\texttt{author}, rien ne dit que le rendu devra en tenir compte; nous
modifierons donc notre classe pour y ajouter le nouveau champ sans que
cela n'impacte nos différentes manières d'effectuer un rendu.
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 de
définir elle-même l'emplacement où l'évènement sera enregistré, que ce
soit dans une base de données, une instance Graylog ou un fichier.
Cette manière de structurer le code permet de centraliser la
configuration d'un type d'évènement à un seul endroit, ce qui augmente
ainsi la testabilité globale du projet.
Lorsque nous verrons les composants, le principe de responsabilité
unique deviendra le CCP - Common Closure Principle. Ensuite, lorsque
nous verrons l'architecture de l'application, ce sera la définition des
frontières (boundaries).
\hypertarget{_open_closed}{%
\subsubsection{Open Closed}\label{_open_closed}}
\begin{quote}
For software systems to be easy to change, they must be designed to
allow the behavior to change by adding new code instead of changing
existing code.
\end{quote}
L'objectif est de rendre le système facile à étendre, en évitant que
l'impact d'une modification ne soit trop grand.
Les exemples parlent d'eux-mêmes: des données doivent être présentées
dans une page web. Et demain, ce seras dans un document PDF. Et après
demain, ce sera dans un tableur Excel. La source de ces données restent
la même (au travers d'une couche de présentation), mais la mise en forme
diffère à chaque fois.
L'application n'a pas à connaître les détails d'implémentation: elle
doit juste permettre une forme d'extension, sans avoir à appliquer une
modification (ou une grosse modification) sur son cœur.
Un des principes essentiels en programmation orientée objets concerne
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:
\begin{itemize}
\item
Une classe \texttt{Customer}, pour laquelle la méthode
\texttt{GetDiscount} ne renvoit rien;
\item
Une classe \texttt{SilverCustomer}, pour laquelle la méthode revoit
une réduction de 10\%;
\item
Une classe \texttt{GoldCustomer}, pour laquelle la même méthode
renvoit une réduction de 20\%.
\end{itemize}
Si nous rencontrons un nouveau type de client, il suffit 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).
Nous passerions ainsi de:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Customer():}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, customer\_type: }\BuiltInTok{str}\NormalTok{):}
\VariableTok{self}\NormalTok{.customer\_type }\OperatorTok{=}\NormalTok{ customer\_type}
\KeywordTok{def}\NormalTok{ get\_discount(customer: Customer) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{int}\NormalTok{:}
\ControlFlowTok{if}\NormalTok{ customer.customer\_type }\OperatorTok{==} \StringTok{"Silver"}\NormalTok{:}
\ControlFlowTok{return} \DecValTok{10}
\ControlFlowTok{elif}\NormalTok{ customer.customer\_type }\OperatorTok{==} \StringTok{"Gold"}\NormalTok{:}
\ControlFlowTok{return} \DecValTok{20}
\ControlFlowTok{return} \DecValTok{0}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ jack }\OperatorTok{=}\NormalTok{ Customer(}\StringTok{"Silver"}\NormalTok{)}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ jack.get\_discount()}
\DecValTok{10}
\end{Highlighting}
\end{Shaded}
A ceci:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Customer():}
\KeywordTok{def}\NormalTok{ get\_discount(}\VariableTok{self}\NormalTok{) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{int}\NormalTok{:}
\ControlFlowTok{return} \DecValTok{0}
\KeywordTok{class}\NormalTok{ SilverCustomer(Customer):}
\KeywordTok{def}\NormalTok{ get\_discount(}\VariableTok{self}\NormalTok{) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{int}\NormalTok{:}
\ControlFlowTok{return} \DecValTok{10}
\KeywordTok{class}\NormalTok{ GoldCustomer(Customer):}
\KeywordTok{def}\NormalTok{ get\_discount(}\VariableTok{self}\NormalTok{) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{int}\NormalTok{:}
\ControlFlowTok{return} \DecValTok{20}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ jack }\OperatorTok{=}\NormalTok{ SilverCustomer()}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ jack.get\_discount()}
\DecValTok{10}
\end{Highlighting}
\end{Shaded}
En anglais, dans le texte : "\emph{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.}".
\textbf{En résumé}: nous fermons la classe \texttt{Customer} à toute
modification, mais nous ouvrons la possibilité de créer de nouvelles
extensions en ajoutant de nouveaux types {[}héritant de
\texttt{Customer}{]}.
De cette manière, nous simplifions également la maintenance de la
méthode \texttt{get\_discount}, dans la mesure où elle dépend
directement du type dans lequel elle est implémentée.
Nous pouvons également appliquer ceci à notre exemple sur les rendus de
document, où le code suivant:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ DocumentRenderer:}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"XML"}\NormalTok{:}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\NormalTok{ document.title,}
\NormalTok{ document.content,}
\NormalTok{ document.published\_at.isoformat()}
\NormalTok{ )}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"Markdown"}\NormalTok{:}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(document.content)}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Format type \textquotesingle{}}\SpecialCharTok{\{\}}\StringTok{\textquotesingle{} is not known"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(format\_type))}
\end{Highlighting}
\end{Shaded}
devient le suivant:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Renderer:}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ControlFlowTok{raise} \PreprocessorTok{NotImplementedError}
\KeywordTok{class}\NormalTok{ XmlRenderer(Renderer):}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document)}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\NormalTok{ document.title,}
\NormalTok{ document.content,}
\NormalTok{ document.published\_at.isoformat()}
\NormalTok{ )}
\KeywordTok{class}\NormalTok{ MarkdownRenderer(Renderer):}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(document.content)}
\end{Highlighting}
\end{Shaded}
Lorsque nous ajouterons notre nouveau type de rendu, nous ajouterons
simplement une nouvelle classe de rendu qui héritera de
\texttt{Renderer}.
Ce point sera très utile lorsque nous aborderons les
\href{https://docs.djangoproject.com/en/stable/topics/db/models/\#proxy-models}{modèles
proxy}.
\hypertarget{_liskov_substitution}{%
\subsubsection{Liskov Substitution}\label{_liskov_substitution}}
Dans Clean Architecture, ce chapitre ci (le 9) est sans doute celui qui
est le moins complet. Je suis d'accord avec les exemples donnés, dans la
mesure où la définition concrète d'une classe doit dépendre d'une
interface correctement définie (et que donc, faire hériter un carré d'un
rectangle, n'est pas adéquat dans le mesure où cela induit l'utilisateur
en erreur), mais il y est aussi question de la définition d'un style
architectural pour une interface REST, mais sans donner de
solution\ldots\hspace{0pt}
Le principe de substitution fait qu'une classe héritant d'une autre
classe doit se comporter de la même manière que cette dernière. Il n'est
pas question que la sous-classe n'implémente pas certaines méthodes,
alors que celles-ci sont disponibles sa classe parente.
\begin{quote}
{[}\ldots\hspace{0pt}{]} 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:
\href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia}).
\end{quote}
\begin{quote}
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:
\href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia
aussi})
\end{quote}
Ce n'est donc pas parce qu'une classe \textbf{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, dans la mesure où une instance
de cette classe pourra toujours être considérée comme étant du type de
son parent.
Petit exemple pratique: si nous définissons une méthode \texttt{walk} et
une méthode \texttt{eat} sur une classe \texttt{Duck}, et qu'une
réflexion avancée (et sans doute un peu alcoolisée) nous dit que
"\emph{Puisqu'un \texttt{Lion} marche aussi, faisons le hériter de notre
classe `Canard`"}, nous allons nous retrouver avec ceci:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Duck:}
\KeywordTok{def}\NormalTok{ walk(}\VariableTok{self}\NormalTok{):}
\BuiltInTok{print}\NormalTok{(}\StringTok{"Kwak"}\NormalTok{)}
\KeywordTok{def}\NormalTok{ eat(}\VariableTok{self}\NormalTok{, thing):}
\ControlFlowTok{if}\NormalTok{ thing }\KeywordTok{in}\NormalTok{ (}\StringTok{"plant"}\NormalTok{, }\StringTok{"insect"}\NormalTok{, }\StringTok{"seed"}\NormalTok{, }\StringTok{"seaweed"}\NormalTok{, }\StringTok{"fish"}\NormalTok{):}
\ControlFlowTok{return} \StringTok{"Yummy!"}
\ControlFlowTok{raise}\NormalTok{ IndigestionError(}\StringTok{"Arrrh"}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Lion(Duck):}
\KeywordTok{def}\NormalTok{ walk(}\VariableTok{self}\NormalTok{):}
\BuiltInTok{print}\NormalTok{(}\StringTok{"Roaaar!"}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
Le principe de substitution de Liskov suggère qu'une classe doit
toujours pouvoir être considérée comme une instance de sa classe parent,
et \textbf{doit pouvoir s'y substituer}. Dans notre exemple, cela
signifie que nous pourrons tout à fait accepter qu'un lion se comporte
comme un canard et adore manger des plantes, insectes, graines, algues
et du poisson. Miam ! Nous vous laissons tester la structure ci-dessus
en glissant une antilope dans la boite à goûter du lion, ce qui nous
donnera quelques trucs bizarres (et un lion atteint de botulisme).
Pour revenir à nos exemples de rendus de documents, nous aurions pu
faire hériter notre \texttt{MarkdownRenderer} de la classe
\texttt{XmlRenderer}:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ XmlRenderer:}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document)}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\NormalTok{ document.title,}
\NormalTok{ document.content,}
\NormalTok{ document.published\_at.isoformat()}
\NormalTok{ )}
\KeywordTok{class}\NormalTok{ MarkdownRenderer(XmlRenderer):}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(document.content)}
\end{Highlighting}
\end{Shaded}
Mais lorsque nous ajouterons une fonction d'entête, notre rendu en
Markdown héritera irrémédiablement de cette même méthode:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ XmlRenderer:}
\KeywordTok{def}\NormalTok{ header(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}"""}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document)}
\ControlFlowTok{return} \StringTok{"""}\SpecialCharTok{\{\}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\VariableTok{self}\NormalTok{.header(),}
\NormalTok{ document.title,}
\NormalTok{ document.content,}
\NormalTok{ document.published\_at.isoformat()}
\NormalTok{ )}
\KeywordTok{class}\NormalTok{ MarkdownRenderer(XmlRenderer):}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(document.content)}
\end{Highlighting}
\end{Shaded}
A nouveau, lorsque nous invoquerons la méthode \texttt{header()} sur une
instance de type \texttt{MarkdownRenderer}, nous obtiendrons un bloc de
déclaration XML
(\texttt{\textless{}?xml\ version\ =\ "1.0"?\textgreater{}}) pour un
fichier Markdown.
\hypertarget{_interface_segregation_principle}{%
\subsubsection{Interface Segregation

View File

@ -355,38 +355,36 @@ Des équivalents à ces directives existent au niveau des composants, puis au ni
\item
Reuse/release équivalence principle,
\item
Common Closure Principle,
\textbf{CCP} - Common Closure Principle,
\item
Common Reuse Principle.
\textbf{CRP} - Common Reuse Principle.
\end{enumerate}
\includegraphics{images/arch-comp-modules.png}
\subsection{Single Responsility Principle}
Le principe de responsabilité unique conseille de disposer de concepts ou domaines d'activité qui ne s'occupent chacun que d'une et une seule
chose.
Ceci rejoint (un peu) la \href{https://en.wikipedia.org/wiki/Unix_philosophy}{Philosophie Unix}, documentée par Doug McIlroy et qui demande de "\emph{faire une seule chose, mais de le faire bien}" \cite{unix_philosophy}.
Une classe ou un élément de programmation ne doit donc pas avoir plus d'une raison de changer.
Il est également possible d'étendre ce principe en fonction d'acteurs:
\begin{quote}
A module should be responsible to one and only one actor. \cite{clean_architecture}
--- Robert C. Martin
\end{quote}
Selon ce principe, une classe ou un élément de programmation ne doit donc pas avoir plus d'une seule raison de changer.
Plutôt que de centraliser le maximum de code à un seul endroit ou dans une seule classe par convenance ou commodité \footnote{Aussi appelé
\emph{God-Like object}}, le principe de responsabilité unique suggère que chaque classe soit responsable d'un et un seul concept.
Une manière de voir les choses consiste à différencier les acteurs ou les intervenants: imaginez disposer d'une classe représentant des données de membres du personnel.
Ces données pourraient être demandées par trois acteurs, le CFO, le CTO et le COO. Ceux-ci ont tous besoin de données et d'informations relatives à une même base de données centralisées, mais ont chacun besoin d'une représentation différente ou de traitements distincts. \cite{clean_architecture}
Une manière de voir les choses consiste à différencier les acteurs ou les intervenants: imaginez disposer d'une classe représentant des données de membres du personnel; ces données pourraient être demandées par trois acteurs:
Nous sommes d'accord qu'il s'agit à chaque fois de données liées aux employés, mais elles vont un cran plus loin et pourraient nécessiter des
ajustements spécifiques en fonction de l'acteur concerné et de la manière dont il souhaite disposer des données.
\begin{enumerate}
\item Le CFO (Chief Financial Officer)
\item Le CTO (Chief Technical Officer)
\item Le COO (Chief Operating Officer)
\end{enumerate}
Chacun d'entre eux aura besoin de données et d'informations relatives à ces membres du personnel, et provenant donc d'une même source de données centralisée.
Mais chacun d'entre eux également besoin d'une représentation différente ou de traitements distincts. \cite{clean_architecture}
Nous sommes d'accord qu'il s'agit à chaque fois de données liées aux employés; celles-ci vont cependant un cran plus loin et pourraient nécessiter des ajustements spécifiques en fonction de l'acteur concerné et de la manière dont il souhaite disposer des données.
Dès que possible, identifiez les différents acteurs et demandeurs, en vue de prévoir les modifications qui pourraient être demandées par l'un d'entre eux.
Dans le cas d'un élément de code centralisé, une modification induite par un des acteurs pourrait ainsi avoir un impact sur les données utilisées par les autres.
@ -426,6 +424,313 @@ Une méthode \texttt{render} permet également de proposer (très grossièrement
\caption{Un convertisseur de document un peu bateau}
\end{listing}
Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, ...), il sera nécessaire de modifier la classe \texttt{Document}.
Ceci n'est:
\begin{enumerate}
\item Ni intuitif: \emph{ce n'est pas le document qui doit savoir dans quels formats il peut être converti}
\item Ni conseillé: \emph{lorsque nous aurons quinze formats différents à gérer, il sera nécessaire d'avoir autant de conditions dans cette méthode}
\end{enumerate}
En suivant le principe de responsabilité unique, une bonne pratique consiste à créer une nouvelle classe de rendu pour chaque type de format à gérer:
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
class Document:
def __init__(self, title, content, published_at):
self.title = title
self.content = content
self.published_at = published_at
class DocumentRenderer:
def render(self, document):
if format_type == "XML":
return """<?xml version = "1.0"?>
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".format(
self.title,
self.content,
self.published_at.isoformat()
)
if format_type == "Markdown":
import markdown
return markdown.markdown(self.content)
raise ValueError("Format type '{}' is not known".format(format_type))
\end{minted}
\caption{Isolation du rendu d'un document par rapport à sa modélisation}
\end{listing}
A présent, lorsque nous devrons ajouter un nouveau format de prise en charge, il nous suffira de modifier la classe \texttt{DocumentRenderer}, sans que la classe \texttt{Document} ne soit impactée.
En même temps, le jour où une instance de type \texttt{Document} sera liée à un champ \texttt{author}, rien ne dit que le rendu devra en tenir compte; nous modifierons donc notre classe pour y ajouter le nouveau champ sans que cela n'impacte nos différentes manières d'effectuer un rendu.
Un autre exemple consiterait à faire communiquer une méthode avec une base de données: ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement spécifique (emplacement sur un disque, ...): cette action doit être prise en compte par une autre classe (ou un autre concept ou composant), qui s'occupera de définir elle-même l'emplacement où l'évènement sera enregistré, que ce soit dans une base de données, une instance Graylog ou un fichier.
Cette manière de structurer le code permet de centraliser la configuration d'un type d'évènement à un seul endroit, ce qui augmente ainsi la testabilité globale du projet.
L'équivalent du principe de responsabilité unique au niveau des composants sera le \texttt{Common Closure Principle} \index{CCP}.
Au niveau architectural, cet équivalent correspondra aux frontières.
\subsection{Open-Closed}
\begin{quote}
For software systems to be easy to change, they must be designed to allow the behavior to change by adding new code instead of changing existing code.
\end{quote}
L'objectif est de rendre le système facile à étendre, en limitant l'impact qu'une modification puisse avoir.
Reprendre notre exemple de modélisation de documents parle de lui-même:
\begin{enumerate}
\item Des données que nous avons converties dans un format spécifique pourraient à présent devoir être présentées dans une page web.
\item Et demain, ce sera dans un document PDF.
\item Et après demain, dans un tableur Excel.
\end{enumerate}
La source de ces données reste la même (au travers d'une couche de présentation): c'est leur mise en forme qui diffère à chaque fois.
L'application n'a pas à connaître les détails d'implémentation: elle doit juste permettre une forme d'extension, sans avoir à appliquer quelconque modification en son cœur.
Un des principes essentiels en programmation orientée objets concerne l'héritage de classes et la surcharge de méthodes: plutôt que de partir sur une série de comparaisons comme nous l'avons initisée plus tôt 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 prendre un nouvel exemple, nous pourrions ainsi définir trois classes:
\begin{itemize}
\item
Une classe \texttt{Customer}, pour laquelle la méthode \texttt{GetDiscount} ne renvoit rien;
\item
Une classe \texttt{SilverCustomer}, pour laquelle la méthode revoit une réduction de 10\%;
\item
Une classe \texttt{GoldCustomer}, pour laquelle la même méthode renvoit une réduction de 20\%.
\end{itemize}
Si nous devions rencontrer un nouveau type de client, il nous suffira de créer une nouvelle sous-classe, implémentant la réduction que nous souhaitons lui offrir.
Ceci évite d'avoir à gérer un ensemble conséquent de conditions dans la méthode initiale, en fonction d'une variable ou d'un paramètre - ici, le type de client.
Nous passerions ainsi de ceci:
\begin{listing}[H]
\begin{minted}{Python}
class Customer():
def __init__(self, customer_type: str):
self.customer_type = customer_type
def get_discount(customer: Customer) -> int:
if customer.customer_type == "Silver":
return 10
elif customer.customer_type == "Gold":
return 20
return 0
>>> jack = Customer("Silver")
>>> jack.get_discount()
10
\end{minted}
\end{listing}
A ceci:
\begin{listing}[H]
\begin{minted}{Python}
class Customer():
def get_discount(self) -> int:
return 0
class SilverCustomer(Customer):
def get_discount(self) -> int:
return 10
class GoldCustomer(Customer):
def get_discount(self) -> int:
return 20
>>> jack = SilverCustomer()
>>> jack.get_discount()
10
\end{minted}
\end{listing}
En anglais, dans le texte : "\emph{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.}".
\textbf{En résumé}: nous fermons la classe \texttt{Customer} à toute modification, mais nous ouvrons la possibilité de créer de nouvelles extensions en ajoutant de nouveaux types héritant de \texttt{Customer}.
De cette manière, nous simplifions également la maintenance de la méthode \texttt{get\_discount}, dans la mesure où elle dépend directement du type dans lequel elle est implémentée.
Nous pouvons également appliquer ceci à notre exemple sur les rendus de document, où le code suivant:
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
class Document:
def __init__(self, title, content, published_at):
self.title = title
self.content = content
self.published_at = published_at
def render(self, format_type):
if format_type == "XML":
return """<?xml version = "1.0"?>
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".format(
self.title,
self.content,
self.published_at.isoformat()
)
if format_type == "Markdown":
import markdown
return markdown.markdown(self.content)
raise ValueError(
"Format type '{}' is not known".format(format_type)
)
\end{minted}
\end{listing}
devient le suivant:
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
class Renderer:
def render(self, document):
raise NotImplementedError
class XmlRenderer(Renderer):
def render(self, document)
return """<?xml version = "1.0"?>
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".format(
document.title,
document.content,
document.published_at.isoformat()
)
class MarkdownRenderer(Renderer):
def render(self, document):
import markdown
return markdown.markdown(document.content)
\end{minted}
\caption{Notre convertisseur suit le principe Open-Closed}
\end{listing}
Lorsque nous ajouterons notre nouveau type de rendu, nous ajouterons simplement une nouvelle classe de rendu qui héritera de \texttt{Renderer}.
\subsection{Liskov Substitution}
Dans Clean Architecture, ce chapitre ci (le 9) est sans doute celui qui est le moins complet.
Je suis d'accord avec les exemples donnés, dans la mesure où la définition concrète d'une classe doit dépendre d'une interface correctement définie (et que donc, faire hériter un carré d'un rectangle, n'est pas adéquat dans le mesure où cela induit l'utilisateur en erreur), mais il y est aussi question de la définition d'un style architectural pour une interface REST, mais sans donner de solution...
Le principe de substitution fait qu'une classe héritant d'une autre classe doit se comporter de la même manière que cette dernière.
Il n'est pas question que la sous-classe n'implémente pas certaines méthodes, alors que celles-ci sont disponibles sa classe parente.
\begin{quote}
{[}\ldots\hspace{0pt}{]} 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.).
--- \href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia}.
\end{quote}
\begin{quote}
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.
--- \href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia aussi}
\end{quote}
Ce n'est donc pas parce qu'une classe \textbf{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, dans la mesure où une instance de cette classe pourra toujours être considérée comme étant du type de son parent.
Petit exemple pratique: si nous définissons une méthode \texttt{make\_some\_noise} et une méthode \texttt{eat} sur une classe \texttt{Duck}, et qu'une réflexion avancée (et sans doute un peu alcoolisée) nous dit que "\emph{Puisqu'un \texttt{Lion} fait aussi du bruit, faisons le hériter de notre classe `Canard`"}, nous allons nous retrouver avec ceci:
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
class Duck:
def make_some_noise(self):
print("Kwak")
def eat(self, thing):
if thing in ("plant", "insect", "seed", "seaweed", "fish"):
return "Yummy!"
raise IndigestionError("Arrrh")
class Lion(Duck):
def make_some_noise(self):
print("Roaaar!")
\end{minted}
\caption{Un lion et un canard sont sur un bateau...}
\end{listing}
Le principe de substitution de Liskov suggère qu'une classe doit toujours pouvoir être considérée comme une instance de sa classe parent, et \textbf{doit pouvoir s'y substituer}.
Dans notre exemple, cela signifie que nous pourrons tout à fait accepter qu'un lion se comporte comme un canard et adore manger des plantes, insectes, graines, algues et du poisson. Miam !
Nous vous laissons tester la structure ci-dessus en glissant une antilope dans la boite à goûter du lion, ce qui nous donnera quelques trucs bizarres (et un lion atteint de botulisme).
Pour revenir à nos exemples de rendus de documents, nous aurions pu faire hériter notre \texttt{MarkdownRenderer} de la classe \texttt{XmlRenderer}:
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
class XmlRenderer:
def render(self, document)
return """<?xml version = "1.0"?>
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".format(
document.title,
document.content,
document.published_at.isoformat()
)
class MarkdownRenderer(XmlRenderer):
def render(self, document):
import markdown
return markdown.markdown(document.content)
\end{minted}
\caption{Le convertisseur Markdown hérite d'un convertisseur XML...}
\end{listing}
Si nous décidons à un moment d'ajouter une méthode d'entête au niveau de notre classe de rendu XML, notre rendu en Markdown héritera irrémédiablement de cette même méthode:
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
class XmlRenderer:
def header(self):
return """<?xml version = "1.0"?>""
def render(self, document)
return """{}
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".format(
self.header(),
document.title,
document.content,
document.published_at.isoformat()
)
class MarkdownRenderer(XmlRenderer):
def render(self, document):
import markdown
return markdown.markdown(document.content)
\end{minted}
\caption{... et il a mal à l'entête}
\end{listing}
Le code ci-dessus ne porte pas à conséquence \footnote{Pas immédiatement, en tout cas...}, mais dès que nous invoquerons la méthode \texttt{header()} sur une instance de type \texttt{MarkdownRenderer}, nous obtiendrons un bloc de déclaration XML (\texttt{\textless{}?xml\ version\ =\ "1.0"?\textgreater{}}) pour un fichier Markdown, ce qui n'aura aucun sens.
\section{Tests unitaires et d'intégration}
\begin{quote}

View File

@ -144,7 +144,7 @@ protocoles du langage.
\section{The Zen of Python}
\begin{listing}[!h]
\begin{listing}[H]
\begin{verbatim}
>>> import this
The Zen of Python, by Tim Peters