Bonjour, Habr!
Aujourd'hui, une publication traduite vous attend, reflétant dans une certaine mesure nos recherches concernant les nouveaux livres sur la POO et la FI. Veuillez participer au vote.

Le paradigme OOP est-il mort? Peut-on dire que la programmation fonctionnelle est l'avenir? Il semble que de nombreux articles écrivent à ce sujet. Je suis enclin à ne pas être d'accord avec ce point de vue. Parlons!
Tous les quelques mois, je tombe sur un article sur un blog où l'auteur fait des affirmations apparemment bien fondées à la programmation orientée objet, après quoi elle déclare OOP une relique du passé, et nous devons tous passer à la programmation fonctionnelle.
Plus tôt, j'ai
écrit que OOP et FI ne se contredisent pas. De plus, j'ai pu les combiner avec beaucoup de succès.
Pourquoi les auteurs de ces articles ont-ils autant de problèmes avec la POO, et pourquoi la FA leur semble-t-elle une alternative si évidente?
Comment enseigner la POO
Lorsque l'on nous enseigne la POO, ils soulignent généralement qu'elle est basée sur quatre principes: l'
encapsulation , l'
hérédité , l'
abstraction , le
polymorphisme . Ce sont ces quatre principes qui sont généralement critiqués dans les articles où les auteurs raisonnent sur le déclin de l'OLP.
Cependant, la POO, comme FI, est un outil. Pour résoudre des problèmes. Il peut être consommé, il peut également être abusé. Par exemple, en créant la mauvaise abstraction, vous abusez de la POO.
Ainsi, la classe
Square
ne doit jamais hériter de la classe
Rectangle
. Dans un sens mathématique, ils sont bien sûr connectés. Cependant, d'un point de vue de la programmation, ils ne sont pas dans une relation d'héritage. Le fait est que les exigences pour un carré sont plus strictes que pour un rectangle. Alors que dans un rectangle il y a deux paires de côtés égaux, un carré doit avoir tous les côtés égaux.
Héritage
Discutons plus en détail de l'héritage. Vous vous souvenez probablement d'exemples de manuels avec de belles hiérarchies de classes héritées, et toutes ces structures fonctionnent pour résoudre le problème. Cependant, dans la pratique, l'héritage n'est pas utilisé aussi souvent que la composition.
Prenons un exemple. Disons que nous avons une classe très simple, un contrôleur dans une application Web. La plupart des frameworks modernes supposent que vous travaillerez avec comme ceci:
class BlogController extends FrameworkAbstractController { }
Il est supposé que de cette façon, il vous sera plus facile de faire des appels comme celui-
this.renderTemplate(...)
, car ces méthodes sont héritées de la classe
FrameworkAbstractController
.
Comme indiqué dans de nombreux articles sur ce sujet, un certain nombre de problèmes tangibles se posent ici. Toute fonction interne de la classe de base se transforme en fait en API. Elle ne peut plus changer. Toutes les variables protégées du contrôleur de base seront désormais plus ou moins liées à l'API.
Il n'y a rien à confondre. Et si nous avions choisi l'approche avec composition et injection de dépendances, cela se serait déroulé comme suit:
class BlogController { public BlogController ( TemplateRenderer templateRenderer ) { } }
Vous voyez, vous ne dépendez plus d'un
FrameworkAbstractController
brumeux, mais dépendez d'une chose très bien définie et étroite,
TemplateRenderer
. En fait,
BlogController
n'hérite d'aucun autre contrôleur, car il n'hérite d'aucun comportement.
Encapsulation
La deuxième caractéristique souvent critiquée de la POO est l'encapsulation. Dans le langage littéraire, le sens de l'encapsulation est formulé comme suit: les données et les fonctionnalités sont livrées ensemble, et l'état interne de la classe est caché au monde extérieur.
Cette opportunité, encore une fois, permet l'utilisation et les abus. Le principal exemple d'abus dans ce cas est un état qui fuit.
Relativement parlant, supposons que la classe
List<>
contienne une liste d'éléments, et cette liste peut être modifiée. Créons une classe pour traiter un panier de commandes comme suit:
class ShoppingCart { private List<ShoppingCartItem> items; public List<ShoppingCartItem> getItems() { return this.items; } }
Ici, dans la plupart des langages orientés OOP, les événements suivants se produisent: la variable items sera retournée par référence. Par conséquent, nous pouvons encore faire ceci:
shoppingCart.getItems().clear();
Ainsi, nous effacerons réellement la liste des articles dans le panier, et ShoppingCart n'en sera même pas informé. Cependant, si vous regardez attentivement cet exemple, il devient clair que le problème ne réside pas dans le principe de l'encapsulation. Ce principe est simplement violé ici, car l'état interne fuit de la classe
ShoppingCart
.
Dans cet exemple particulier, l'auteur de la classe
ShoppingCart
pourrait utiliser l'
immuabilité pour contourner le problème et s'assurer que le principe d'encapsulation n'est pas violé.
Les programmeurs inexpérimentés violent souvent le principe de l'encapsulation d'une autre manière: ils introduisent un état où il n'est pas nécessaire. Ces programmeurs inexpérimentés utilisent souvent des variables de classe privées pour transférer des données d'une fonction à une autre au sein de la même classe, alors qu'il serait plus approprié d'utiliser des objets de transfert de données pour transférer une structure complexe vers une autre fonction. À la suite de ces erreurs, le code est inutilement compliqué, ce qui peut entraîner des bogues.
En général, il serait bien de se passer de l'état tout à fait - stocker les données mutables dans les classes chaque fois que possible. Ce faisant, vous devez
assurer une encapsulation fiable et vous assurer qu'il n'y a aucune fuite nulle part.
Abstraction
L'abstraction, encore une fois, est mal comprise à bien des égards. En aucun cas, vous ne devez bourrer le code de classes abstraites et y créer des hiérarchies profondes.
Si vous faites cela sans bonne raison, alors vous cherchez juste des ennuis par vous-même. Peu importe comment l'abstraction est effectuée - en tant que classe abstraite ou en tant qu'interface; dans tous les cas, une complexité supplémentaire apparaîtra dans le code. Cette complexité doit être justifiée.
Autrement dit, une interface ne peut être créée que si vous êtes prêt à passer du temps et à documenter le comportement attendu de la classe qui l'implémente. Oui, tu m'as bien lu. Il ne suffit pas de dresser une liste des fonctions que vous devez implémenter - décrivez également comment (idéalement) elles devraient fonctionner.
Polymorphisme
Enfin, parlons du polymorphisme. Il suggère qu'une classe peut implémenter de nombreux comportements. Un mauvais exemple de manuel est d'écrire que le
Square
avec polymorphisme peut être soit un
Rectangle
soit un
Parallelogram
. Comme je l'ai déjà souligné ci-dessus, cela dans la POO est décidément impossible, car les comportements de ces entités sont différents.
En parlant de polymorphisme, il faut garder à l'esprit les
comportements , pas le
code . Un bon exemple est la classe
Soldier
dans un jeu vidéo. Il peut mettre en œuvre à la fois un comportement
Movable
(situation: il peut se déplacer) et un comportement
Enemy
(situation: vous tire). En revanche, la classe
GunEmplacement
ne peut implémenter que le comportement
Enemy
.
Donc, si vous écrivez
Square implements Rectangle, Parallelogram
, cette déclaration ne devient pas vraie. Vos abstractions doivent fonctionner selon la logique métier. Vous devriez penser plus au comportement qu'au code.
Pourquoi la PF n'est pas une solution miracle
Donc, lorsque nous avons répété les quatre principes de base de la POO, réfléchissons à ce qui est la caractéristique de la programmation fonctionnelle, et pourquoi ne pas l'utiliser pour résoudre tous les problèmes de votre code?
Du point de vue de nombreux adeptes de la PF, les classes sont
sacrilèges et le code doit être présenté sous forme de
fonctions . Selon la langue, les données peuvent être transférées de fonction en fonction à l'aide de types primitifs, ou sous la forme de l'un ou l'autre ensemble de données structurées (tableaux, dictionnaires, etc.).
De plus, la plupart des fonctions ne devraient pas avoir d'effets secondaires. En d'autres termes, ils ne doivent pas modifier les données à un endroit inattendu en arrière-plan, mais uniquement travailler avec des paramètres d'entrée et produire une sortie.
Cette approche sépare les
données de la
fonctionnalité - à première vue, ce FP diffère radicalement de la POO. FP souligne que de cette manière le code reste simple. Vous voulez faire quelque chose, écrire une fonction à cet effet - c'est tout.
Les problèmes commencent lorsque certaines fonctions doivent s'appuyer sur d'autres. Lorsque la fonction A appelle la fonction B, et la fonction B appelle encore cinq à six fonctions, et à la toute fin se trouve
une fonction de remplissage à zéro qui peut casser - c'est là que vous ne serez pas envié.
La plupart des programmeurs qui se considèrent comme des partisans du FP aiment FP pour sa simplicité et ne considèrent pas ces problèmes comme graves. C'est assez honnête si votre tâche est de simplement passer le code et de ne plus jamais y penser. Si vous souhaitez créer une base de code qui soit pratique à prendre en charge, il est préférable de respecter les principes du
code pur , en particulier, d'appliquer l'
inversion de dépendance , dans laquelle l'IF en pratique devient également beaucoup plus compliqué.
POO ou FP?
OOP et FI sont des
outils . En fin de compte, peu importe le paradigme de programmation que vous utilisez. Les problèmes décrits dans la plupart des articles sur ce sujet concernent l'organisation du code.
À mon avis, la macrostructure de l'application est beaucoup plus importante. Quels sont les modules qu'il contient? Comment échangent-ils des informations entre eux? Quelles structures de données sont les plus courantes avec vous? Comment sont-ils documentés? Quels objets sont les plus importants en termes de logique métier?
Tous ces problèmes ne sont aucunement liés au paradigme de programmation utilisé; au niveau d'un tel paradigme, ils ne peuvent même pas être résolus. Un bon programmeur étudie le paradigme afin de maîtriser les outils qu'il propose, puis choisit ceux qui conviennent le mieux pour résoudre la tâche.