Idées fausses courantes sur la POO

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.

Source: https://habr.com/ru/post/fr469135/


All Articles