Les fonctions de première classe remplacent-elles le modèle de stratégie?

15

Le modèle de conception de la stratégie est souvent considéré comme un substitut aux fonctions de première classe dans les langues qui en manquent.

Par exemple, disons que vous vouliez transmettre des fonctionnalités à un objet. En Java, vous devez passer dans l'objet un autre objet qui encapsule le comportement souhaité. Dans un langage tel que Ruby, vous passeriez simplement la fonctionnalité elle-même sous la forme d'une fonction anonyme.

Cependant, j'y pensais et j'ai décidé que la stratégie offrait peut-être plus qu'une simple fonction anonyme.

En effet, un objet peut contenir un état qui existe indépendamment de la période d'exécution de sa méthode. Cependant, une fonction anonyme en elle-même ne peut conserver qu'un état qui cesse d'exister au moment où la fonction termine son exécution.

Dans un langage orienté objet qui prend en charge les fonctions de première classe, le modèle de stratégie a-t-il un avantage sur l'utilisation des fonctions?

Aviv Cohn
la source
10
"Cependant, une fonction anonyme en elle-même ne peut contenir qu'un état qui cesse d'exister au moment où la fonction termine son exécution.": Ce n'est pas vrai: une fermeture peut contenir un état qui survient à travers différentes invocations.
Giorgio
"Cependant, une fonction anonyme en elle-même ne peut conserver un état qui cesse d'exister au moment où la fonction termine son exécution." : sans oublier les variables globales, et les variables statiques à tout le moins.
gbjbaanb

Réponses:

13

Lorsque le langage prend en charge les références à la fonction ( Java le fait depuis la version 8 ), celles-ci sont souvent une bonne alternative pour les stratégies, car elles expriment généralement la même chose avec moins de syntaxe. Cependant, il existe certains cas où un objet réel peut être utile.

Une interface de stratégie peut avoir plusieurs méthodes. Prenons une interface RouteFindingStragegycomme exemple, qui encapsule différents algorithmes de recherche d'itinéraire. Il pourrait déclarer des méthodes comme

  • Route findShortestRoute(Node start, Node destination)
  • boolean doesRouteExist(Node start, Node destination)
  • Route[] findAllPossibleRoutes(Node start, Node destination)
  • Route findShortestRouteToClosestDestination(Node start, Node[] destinations)
  • Route findTravelingSalesmanRoute(Node[] stations)

qui seraient alors tous mis en œuvre par la stratégie. Certains algorithmes de recherche d'itinéraire peuvent permettre des optimisations internes pour certains de ces cas d'utilisation et d'autres non, de sorte que l'implémenteur peut décider comment implémenter chacune de ces méthodes.

Un autre cas est celui où la stratégie a un état intérieur. Bien sûr, dans certaines langues, les fermetures peuvent avoir un état intérieur, mais lorsque cet état intérieur devient très complexe, il devient souvent plus élégant de promouvoir la fermeture à une classe à part entière.

Philipp
la source
2
"lorsque cet état intérieur devient très complexe, il devient souvent utile de promouvoir la fermeture d'une classe à part entière": pourquoi? Si l'état intérieur devient complexe, vous pouvez également le placer dans un objet / enregistrement qui est stocké à l'intérieur de la fermeture.
Giorgio
1
@Giorgio Mais alors vous auriez deux entités de syntaxe à maintenir - la fermeture et la classe qui gère son état interne. Le code pourrait donc être simplifié en déplaçant la fermeture vers cette classe. Cela pourrait être mieux ou non, ce qui dépend du cas d'utilisation exact, du langage de programmation et des préférences personnelles.
Philipp
Cela me semble raisonnable.
Giorgio
5

Il n'est pas vrai qu'une fonction anonyme ne peut conserver l'état qui cesse d'exister lorsque la fonction termine son exécution.

Prenons l'exemple suivant en Common Lisp:

(defun number-strings (ss)
  (let ((counter 0))
    (mapcar #'(lambda (s) (format nil "~a: ~a" (incf counter) s)) ss)))

Cette fonction prend une liste de chaînes et ajoute un compteur à chaque élément de la liste. Ainsi, par exemple, en invoquant

(number-strings '("a" "b" "c"))

donne

("1: a" "2: b" "3: c")

La fonction number-stringsutilise en interne une fonction anonyme avec une variable counterqui contient l'état (la valeur actuelle du compteur) qui est réutilisée chaque fois que la fonction est invoquée.

En général, vous pouvez considérer une fermeture comme un objet avec une seule méthode. Alternativement, un objet est une collection de fermetures qui partagent les mêmes variables fermées. Je ne sais donc pas s'il existe des cas dans lesquels vous devez utiliser un objet au lieu d'une fermeture: je dirais que les deux sont des façons de regarder le même modèle sous des perspectives différentes.

En particulier, le modèle de stratégie nécessite un objet avec une seule méthode, donc une fermeture devrait faire le travail. Mais, comme Philipp l'a observé dans sa réponse, selon les circonstances (état complexe) et les langages de programmation, vous pouvez obtenir une solution plus élégante en utilisant des objets.

Giorgio
la source
Donc, dans un langage qui prend en charge les fonctions de première classe en tant que fermetures, utiliseriez-vous toujours la stratégie «classique»?
Aviv Cohn
1
J'ai tendance à être d'accord avec Philipp: cela dépend de la langue et des préférences personnelles. Je choisirais toujours l'approche qui rend la notation aussi simple que possible. Par exemple, en Lisp, je pouvais définir mon état comme une liste de variables via a letpuis définir ma fermeture à l'intérieur. En gros, j'aurais défini un objet avec une méthode à la volée. Dans un autre langage (par exemple Java), il pourrait être plus pratique (syntaxiquement plus simple) de définir un objet approprié pour conserver l'état. Donc, je déciderais au cas par cas.
Giorgio
1

Ce n'est pas parce que deux conceptions peuvent résoudre le même problème qu'elles sont des substitutions directes l'une de l'autre.

Si vous devez suivre l'état dans un programme fonctionnel, vous ne modifiez pas une variable fermée, même si le langage le permet. Vous organisez pour appeler une fonction qui prend un état comme argument et renvoie le nouvel état comme valeur de retour.

Votre architecture sera très différente, mais vous atteindrez le même objectif. N'essayez pas de forcer les modèles d'un paradigme directement sur l'autre.

Karl Bielefeldt
la source
"Si vous avez besoin de suivre l'état dans un programme fonctionnel, vous ne modifiez pas une variable fermée, même si le langage le permet.": Je suis un fan de style purement fonctionnel et je suis d'accord avec vos conseils. D'un autre côté, les fermetures ne sont pas seulement une construction fonctionnelle, et encore moins purement fonctionnelle. L'idée de refermer des variables du contexte lexical est orthogonale à la transparence / immuabilité référentielle.
Giorgio
Désolé, mais je ne peux pas suivre votre argumentation. Qu'est-ce que la gestion des états dans la programmation purement fonctionnelle a à voir avec la question posée?
Philipp
1
Le fait est que si vous utilisez une partie d'un paradigme fonctionnel, comme les fonctions de première classe, ne soyez pas surpris si vous avez besoin de tirer d'autres parties du paradigme pour le faire fonctionner en douceur.
Karl Bielefeldt
1

La stratégie est un concept , une recette utile pour résoudre un problème particulier et récurrent. Ce n'est pas une construction de langage, ni une quelconque forme d' implémentation . Une clôture peut être utilisée pour mettre en œuvre la stratégie un jour et Observer le lendemain.

Le terme stratégie est surtout utile dans les conversations avec d'autres programmeurs pour exprimer de manière concise votre intention. Il n'y a rien de magique là-dedans.

idobie
la source
2
La question mentionne spécifiquement le modèle de conception de stratégie qui a une structure de classe spécifique. L'autre sens de «stratégie» en tant que plan d'action destiné à atteindre un objectif spécifique est inexact dans ce contexte.
Je suis enclin à être d'accord avec @Snowman. Êtes-vous sûr de parler du modèle de stratégie ?
Rowan Freeman
1
@Snowman, même la page à laquelle vous avez lié n'indique pas exactement comment ce modèle doit être implémenté, mais donne plutôt des exemples dans des langages spécifiques, mais rien dans le diagramme UML ne dit que je dois utiliser l'héritage C ++, les interfaces Java ou les blocs Ruby . Je suis donc gentiment en désaccord avec votre analyse.
idobie