Strategy Pattern utilisant des paramètres différents

a marqué ce sujet comme résolu.

Bonjour à tous,

Je suis en train de développer une application permettant de savoir ce qui est en train d’être joué quelque part pour ensuite pouvoir par exemple l’ajouter à une playlist Spotify. Ce quelque part est variable et peut par exemple être une station radio spécifique ou un speaker Sonos.

Donc pour modéliser tout ça, je pensais utiliser un Strategy pattern qui ferait deux choses:

  1. Identifier la chanson qui est en train d’être jouée
  2. La sauvegarder dans une playlist

Pour ma première étape, je voulais avoir une interface du type

public interface SpeakerService {
  Song whatsPlaying();
}

Et ensuite avoir une implémentation pour RadioSpeakerService et une autre pour SonosSpeakerService. Mais c’est ici que je bloque: ces deux implémentations ont besoin de paramètres différents pour savoir ce qui est joué. Dans le cas du RadioSpeakerService, on a besoin d’une URL vers le stream tandis que pour SonosSpeakerService, on a besoin de l’ID du speaker (et potentiellement d’autres paramètres).

Je vois deux solutions pour résoudre ceci:

  • Ajouter un paramètre PlayingRequest à ma méthode whatsPlaying qui serait une interface. Ensuite avoir deux implémentations de cette interface, une pour radio, une pour Sonos. Je pense que ça devrait fonctionner mais ça m’a l’air potentiellement usine à gaz et une mauvaise utilisation du pattern
  • Plutôt que d’ajouter un paramètre à la méthode, je pourrais ajouter des attributs à mes services concrets et ma méthode pourrait rester sans paramètre. Je n’aime pas cette solution non plus car idéallement, je voudrais que mes services soient des composants Spring que je peux instancier une seule fois plutôt qu’une fois par requête.

Je serais curieux d’avoir votre avis si il y a d’autres solutions que je n’aurais pas envisagées qui vous semblent adéquates pour résoudre mon problème. Ou si une des deux solutions présentées vous semblent pas si mauvaise que ça.

Merci d’avance !

Salut,

Où se trouve ton service ? Est-ce que c’est une application qui tourne en local chez toi, ou bien c’est un service en ligne ? Est-ce qu’il y a un seul utilisateur humain par instance ou plusieurs ?

Je te pose la question parce que si tu es tout seul à utiliser ton service, ou si c’est un truc qui tourne en local, passer les identifiants de tes sonos dans SonosSpeakerService est certainement suffisant, car tu ne vas pas modifier ton système souvent (et au pire tu redémarres après un changement de configuration)). Pas besoin de se casser la tête.

Sinon, si c’est une application plus ou moins ouverte au monde disponible H24 avec plein d’utilisateurs, par contre…

L’approche naïve serait une interface abstraite PlayingParameters avec deux classes concrètes RadioPlayingParameters et SonosPlayingParameters, mais ça implique que, au moment où tu appelles whatsPlaying(), tu dois connaitre le type de service pour lui passer le bon type de paramètre. Ca va complètement à l’encontre de plein de principes OO, et donc ce n’est pas une bonne solution.

Pour faire mieux, voici mon idée: on pourrait imaginer que tu passes un genre d’URL générique, ou autrement dit une URI, qui, dans le cas sonos, encapsule les identifiants nécessaire. Par exemple sonos://user=123@xyz.com&id=abc&room=def. C’est par exemple le genre de principe utilisé dans les URL de connexion aux bases de donnée (ça ne te rappelle pas jdbc:mysql:…, h2:…, etc. ?)

Dans le cas des DataSource, en très très gros résumé, au sommet, il y a une factory qui va regarder le protocole de l’URI, et instancier le driver correspondant. Puis le driver reçoit l’URI, et va récupérer les paramètres pour se configurer et se connecter.

Dans ton cas il te faudrait donc un SpeakerServiceFactory du genre: SpeakerService getSpeakerService(URI)

Tu peux mettre pas mal de choses dans une URL, un token complet si tu veux, donc il y a de quoi faire. Sinon, il te faudra imaginer un autre objet plus riche mais qui reste toujours générique.

+0 -0

Salut Quentin,

Merci pout ta réponse !

Où se trouve ton service ? Est-ce que c’est une application qui tourne en local chez toi, ou bien c’est un service en ligne ? Est-ce qu’il y a un seul utilisateur humain par instance ou plusieurs ? […] Sinon, si c’est une application plus ou moins ouverte au monde disponible H24 avec plein d’utilisateurs, par contre…

Dans un premier temps, je serai le seul utilisateur mais je compte améliorer ça pour ouvrir ça à plus d’utilisateurs. Mon idée est d’abord d’implémenter ça de façon simple avec un utilisateur et sans me soucier trop de l’authentification et hardcoder 2–3 choses (pour interagir avec Sonos ou Spotify, je compte faire du OAuth 2.0) avant de rendre ça plus flexible.

Mais je ne pense pas que ça devrait changer grand chose à mon design de base concernant mes services.

Tu peux mettre pas mal de choses dans une URL, un token complet si tu veux, donc il y a de quoi faire. Sinon, il te faudra imaginer un autre objet plus riche mais qui reste toujours générique.

Utiliser un paramètre commun à tous les services (que ça soit un URI ou autre chose) est effectivement une autre possibilité mais je la trouve aussi pas super car mes implémentations ont potentiellement besoin de paramètres complètement différent. Par exemple pour Sonos, il y a des notions de household et group. Ca peut peut-être rentrer dans un format URI mais ça m’a l’air assez hacky.

Et même si on utilise une autre classe que URI, ça veut dire que si j’ajoute un nouveau service, je devrai modifier cette classe pour lui ajouter les attributs nécessaires, qui ne seront utilisés que par une seule implémentation.

L’approche naïve [] dois connaitre le type de service pour lui passer le bon type de paramètre. Ca va complètement à l’encontre de plein de principes OO, et donc ce n’est pas une bonne solution.

Dans tous les cas, la requête que je recevrai de l’utilisateur sera différente. Donc je pense que je pourrais m’en sortir en ayant des Factory (ou quelque chose du genre) qui pourraient créer des PlayingRequest en fonction de la requête reçue et qui potentiellement retourneraient aussi le service à utiliser.

Hello,

Utiliser un paramètre commun à tous les services (que ça soit un URI ou autre chose) est effectivement une autre possibilité mais je la trouve aussi pas super car mes implémentations ont potentiellement besoin de paramètres complètement différent. Par exemple pour Sonos, il y a des notions de household et group. Ca peut peut-être rentrer dans un format URI mais ça m’a l’air assez hacky.

Ca reste des identifiants, des ID, des clés non ? Donc ça passe dans une chaîne de caractères.

Et même si on utilise une autre classe que URI, ça veut dire que si j’ajoute un nouveau service, je devrai modifier cette classe pour lui ajouter les attributs nécessaires, qui ne seront utilisés que par une seule implémentation.

Un moment donné quand tu dois passer quelque chose de générique, ou avec des paramètres tellement différents d’un service à l’autre, tu n’as pas trop le choix en fait, tu es obligé d’utiliser un truc pas trop rigide. Tu dois forcément avoir une factory qui regarde un bout de message comun à tous pour savoir quoi faire ensuite.

Je pensais à une URI parce que tu as parlé de streams audio, mais en fait tu peux passer du JSON ou du XML si tu es plus à l’aise. Ca ne change pas grand chose au principe.

En fait c’est quoi l’objectif ? Tu écoutes une musique sur une radio, ou sur un service de musique quelconque autre que spotify, et le but c’est de retrouver le morceau sur spotify et de l’ajouter dans une playlist ?

Si c’est ça, j’avoue que ça peut potentiellement m’intéresser… c’est un peu embêtant quand tu as des amis qui ne sont pas abonnés au même service que toi.

+0 -0

Je compte faire un billet (ou un post de présentation de projets) lorsque j’aurai terminé mais en quelques mots, l’objectif, c’est d’avoir un service qui exécute deux actions:

  1. Identifier une chanson qui est en train d’être jouée quelque part. Initialement, je compte supporter la radio (j’ai déjà joué avec des flux audio) et les Sonos (car j’en possède) mais ça pourrait s’étendre à d’autres services.
  2. Faire une action avec cette chanson. Celle qui me serait la plus utile serait d’ajouter cette chanson à une playlist sur Spotify mais je peux aussi imaginer simplement l’enregistrer dans une base de données ou contacter un autre service que Spotify.

Mon cas d’utilisation, c’est que quand j’écoute la radio et qu’une chanson que j’aime bien passe, je voudrais qu’elle soit ajoutée presque automatiquement à ma playlist Spotify. Ce que mon service permettrait de faire en un appel API.

Je pensais à une URI parce que tu as parlé de streams audio, mais en fait tu peux passer du JSON ou du XML si tu es plus à l’aise. Ca ne change pas grand chose au principe.

Pour clarifier et être sûr qu’on est sur la même longueur d’ondes, voici comment j’imagine mon design. J’ai un contrôleur qui acceptera des requêtes JSON des utilisateurs qui spécifieront d’où le service doit lire la chanson. Idéalement, je compte autoriser les requêtes à être fortement différentes selon les sources (radio serait juste un paramètre URI, Sonos serait plusieurs paramètres pour identifier le Sonos qu’on cible, etc.).

Une fois que mon contrôleur reçoit cette requête, je voulais appliquer le Strategy pattern car il me semble adapté: je veux toujours exécuter les deux étapes que je décris plus haut, peu importe la source (ou l’action que l’on voudra prendre avec la chanson). Mais c’est ici que je tombe sur un os: généralement, les méthodes utilisées par ce pattern n’ont soit pas de paramètre, soit des paramètres communs à toutes les implémentations. Or dans mon cas, mes implémentations étaient tellement différentes, c’est quelque chose que je voudrais éviter.

J’ai trouvé un papier qui propose une solution à mon problème et qui décrit exactement ce que je suis en train d’affronter:

  1. Shortcomings of Strategy pattern

The strategy pattern can be used to host different algorithms which either have no parameters or the set of parameters for each algorithm is the same. The problem arises if various algorithms with different sets of parameters are to be used. Obviously, these parameters cannot be declared in the execute() method in the abstract class Algorithm, as they vary from algorithm to algorithm. Existence of various sets of parameters results in different interfaces which would not be acceptable for the strategy pattern. Hence, the strategy pattern needs to be adapted in order to handle the problem.

The traditional solution is to develop a context interface that would have the union of all parameters needed by the different algorithms. However, this solution breaks the independence between client and the algorithm. It means that adding a new algorithm, whose parameters are not in the union of parameters, requires some changes to be made on the client.

In the following section we present a solution which does not break the independence between client and algorithms; is able to host algorithms with different parameters; and provide the user with parameters specific to a chosen algorithm.

Je dois encore prendre le temps de le lire pour voir ce qu’ils proposent.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte