Je travaille actuellement sur un projet logiciel qui effectue la compression et l'indexation de séquences de vidéosurveillance. La compression fonctionne en divisant les objets d'arrière-plan et de premier plan, puis en enregistrant l'arrière-plan en tant qu'image statique et le premier plan en tant que Sprite.
Récemment, j'ai entrepris de revoir certaines des classes que j'ai conçues pour le projet.
J'ai remarqué qu'il existe de nombreuses classes qui n'ont qu'une seule méthode publique. Certaines de ces classes sont:
compress
qui prend une vidéo d'entrée de type RawVideo
et renvoie une vidéo de sortie de type CompressedVideo
).split
qui prend une vidéo d'entrée de type RawVideo
et renvoie un vecteur de 2 vidéos de sortie, chacune de type RawVideo
).index
qui prend une vidéo d'entrée de type RawVideo
et renvoie un index vidéo de type VideoIndex
).Je me retrouve à instancier chaque classe juste pour faire des appels comme VideoCompressor.compress(...)
, VideoSplitter.split(...)
, VideoIndexer.index(...)
.
En surface, je pense que les noms de classe sont suffisamment descriptifs de leur fonction prévue, et ce sont en fait des noms. De même, leurs méthodes sont également des verbes.
Est-ce vraiment un problème?
Non, ce n'est pas un problème, bien au contraire. C'est un signe de modularité et de responsabilité claire de la classe. L'interface allégée est facile à saisir du point de vue d'un utilisateur de cette classe, et elle encouragera un couplage lâche. Cela présente de nombreux avantages mais presque aucun inconvénient. Je souhaite que plus de composants soient conçus de cette façon!
Il n'est plus orienté objet. Parce que ces classes ne représentent rien, ce ne sont que des vaisseaux pour les fonctions.
Cela ne veut pas dire que c'est faux. Si la fonctionnalité est suffisamment complexe ou lorsqu'elle est générique (c'est-à-dire que les arguments sont des interfaces, pas des types finaux concrets), il est logique de mettre cette fonctionnalité dans un module séparé .
De là, cela dépend de votre langue. Si le langage a des fonctions libres, il doit s'agir de modules exportant des fonctions. Pourquoi prétendre que c'est une classe quand ce n'est pas le cas. Si la langue n'a pas de fonctions gratuites comme par ex. Java, puis vous créez des classes avec une seule méthode publique. Eh bien, cela montre simplement les limites de la conception orientée objet. Parfois, la fonctionnalité est tout simplement meilleure.
Il y a un cas où vous pouvez avoir besoin d'une classe avec une seule méthode publique car elle doit implémenter une interface avec une seule méthode publique. Que ce soit pour le modèle d'observateur ou l'injection de dépendance ou autre. Ici encore, cela dépend de la langue. Dans les langages dotés de foncteurs de première classe (C++ (std::function
ou paramètre de modèle), C # (délégué), Python, Perl, Ruby (proc
), LISP, Haskell, ...) ces modèles utilisent des types de fonctions et ne pas besoin de cours. Java n'a pas (encore, dans la version 8) de types de fonction, vous utilisez donc des interfaces de méthode unique et des classes de méthode unique correspondantes.
Bien sûr, je ne préconise pas d'écrire une seule fonction énorme. Il doit avoir des sous-programmes privés, mais ils peuvent être privés du fichier d'implémentation (espace de noms statique ou anonyme au niveau du fichier en C++) ou d'une classe d'assistance privée qui n'est instanciée qu'à l'intérieur de la fonction publique ( pour stocker des données ou non ? ).
Il peut y avoir des raisons d'extraire une méthode donnée dans une classe dédiée. L'une de ces raisons est d'autoriser l'injection de dépendance.
Imaginez que vous ayez une classe appelée VideoExporter
qui, à terme, devrait pouvoir compresser une vidéo. Une façon propre serait d'avoir une interface:
interface IVideoCompressor
{
Stream compress(Video video);
}
qui serait implémenté comme ceci:
class MpegVideoCompressor : IVideoCompressor
{
// ...
}
class FlashVideoCompressor : IVideoCompressor
{
// ...
}
et utilisé comme ceci:
class VideoExporter
{
// ...
void export(Destination destination, IVideoCompressor compressor)
{
// ...
destination = compressor(this.source);
// ...
}
// ...
}
Une alternative mauvaise serait d'avoir un VideoExporter
qui a beaucoup de méthodes publiques et fait tout le travail, y compris la compression. Cela deviendrait rapidement un cauchemar de maintenance, ce qui compliquerait la prise en charge d'autres formats vidéo.
C'est un signe que vous voulez passer des fonctions comme arguments à d'autres fonctions. Je suppose que votre langage (Java?) Ne le prend pas en charge; si tel est le cas, ce n'est pas tant un défaut dans votre conception que c'est une lacune dans la langue de votre choix. C'est l'un des plus gros problèmes avec les langues qui insistent sur le fait que tout doit être une classe.
Si vous ne passez pas ces fausses fonctions, vous voulez juste une fonction libre/statique.
Je sais que je suis en retard à la fête, mais comme tout le monde semble avoir manqué de le souligner:
Le modèle de stratégie est utilisé lorsqu'il existe plusieurs stratégies possibles pour résoudre un sous-problème. En règle générale, vous définissez une interface qui applique un contrat sur toutes les implémentations, puis utilisez une certaine forme de Dependency Injection pour fournir la stratégie concrète pour vous.
Par exemple, dans ce cas, vous pouvez avoir interface VideoCompressor
, Puis plusieurs implémentations alternatives, par exemple class H264Compressor implements VideoCompressor
Et class XVidCompressor implements VideoCompressor
. Il n'est pas clair d'après OP qu'il y a une interface impliquée, même si ce n'est pas le cas, il se peut simplement que l'auteur d'origine ait laissé la porte ouverte pour implémenter un modèle de stratégie si nécessaire. Ce qui en soi est aussi un bon design.
Le problème que OP se retrouve constamment à instancier les classes pour appeler une méthode est un problème avec elle qui n'utilise pas correctement l'injection de dépendance et le modèle de stratégie. Au lieu de l'instancier là où vous en avez besoin, la classe contenant doit avoir un membre avec l'objet de stratégie. Et ce membre doit être injecté, par exemple dans le constructeur.
Dans de nombreux cas, le modèle de stratégie se traduit par des classes d'interface (comme vous le montrez) avec une seule méthode doStuff(...)
.
public interface IVideoProcessor
{
void Split();
void Compress();
void Index();
}
Ce que vous avez est modulaire et c'est bien, mais si vous deviez regrouper ces responsabilités dans IVideoProcessor, cela aurait probablement plus de sens du point de vue DDD.
D'un autre côté, si le fractionnement, la compression et l'indexation n'étaient liés en aucune façon, je les garderais en tant que composants séparés.
C'est un problème - vous travaillez à partir de l'aspect fonctionnel de la conception plutôt que des données. Ce que vous avez en fait, ce sont 3 fonctions autonomes qui ont été OO-ified.
Par exemple, vous avez une classe VideoCompressor. Pourquoi travaillez-vous avec une classe conçue pour compresser la vidéo - pourquoi n'avez-vous pas de classe Video avec des méthodes pour compresser les données (vidéo) que chaque objet de ce type contient?
Lors de la conception de systèmes OO, il est préférable de créer des classes qui représentent des objets, plutôt que des classes qui représentent des activités que vous pouvez appliquer. Auparavant, les classes étaient appelées types - OO était un moyen d'étendre un langage avec la prise en charge de nouveaux types de données. Si vous pensez à OO comme ça, vous obtenez une meilleure façon de concevoir vos classes.
ÉDITER:
laissez-moi essayer de m'expliquer un peu mieux, imaginez une classe de chaîne qui a une méthode de concat. Vous pouvez implémenter une telle chose où chaque objet instancié à partir de la classe contient les données de chaîne, vous pouvez donc dire
string mystring("Hello");
mystring.concat("World");
mais l'OP veut qu'il fonctionne comme ceci:
string mystring();
string result = mystring.concat("Hello", "World");
il y a maintenant des endroits où une classe peut être utilisée pour contenir une collection de fonctions connexes, mais ce n'est pas OO, c'est une façon pratique d'utiliser les fonctionnalités OO d'un langage pour vous aider à gérer votre base de code mieux, mais ce n'est en aucun cas une sorte de "OO Design". L'objet dans de tels cas est totalement artificiel, simplement utilisé comme ça parce que le langage n'offre rien de mieux pour gérer ce genre de problème. Par exemple. Dans des langages comme C # vous utiliseriez une classe statique pour fournir cette fonctionnalité - elle réutilise le mécanisme de classe, mais vous n'avez plus besoin d'instancier un objet juste pour appeler les méthodes dessus. Vous vous retrouvez avec des méthodes comme string.IsNullOrEmpty(mystring)
que je pense est pauvre par rapport à mystring.isNullOrEmpty()
.
Donc, si quelqu'un demande "comment puis-je concevoir mes classes", je recommande de penser aux données que la classe contiendra plutôt qu'aux fonctions qu'elle contient. Si vous optez pour "une classe est un tas de méthodes", vous finissez par écrire un code de style "meilleur C". (ce qui n'est pas nécessairement une mauvaise chose si vous améliorez le code C) mais cela ne vous donnera pas le meilleur programme conçu OO.
ISP (principe de ségrégation d'interface) dit qu'aucun client ne devrait être forcé de dépendre de méthodes qu'il n'utilise pas. Les avantages sont multiples et clairs. Votre approche respecte totalement le FAI, et c'est bien.
Une approche différente, respectant également le FAI, consiste par exemple à créer une interface pour chaque méthode (ou ensemble de méthodes à forte cohésion) puis à avoir une seule classe implémentant toutes ces interfaces. Que ce soit ou non une meilleure solution dépend du scénario. L'avantage de ceci est que, lorsque vous utilisez l'injection de dépendances, vous pouvez avoir un client avec différents collaborateurs (un par interface) mais à la fin tous les collaborateurs pointeront vers la même instance d'objet.
Au fait, vous avez dit
Je me retrouve à instancier chaque classe
, ces classes semblent être des services (et donc apatrides). Avez-vous pensé à en faire des singletons?
Oui, il y a un problème. Mais pas grave. Je veux dire que vous pouvez structurer votre code comme ça et rien de mauvais ne se produira, il est maintenable. Mais il y a quelques faiblesses dans ce type de structuration. Par exemple, considérez que si la représentation de votre vidéo (dans votre cas, elle est regroupée dans la classe RawVideo) change, vous devrez mettre à jour toutes vos classes d'opérations. Ou considérez que vous pouvez avoir plusieurs représentations de vidéo qui varient dans l'exécution. Ensuite, vous devrez faire correspondre la représentation à une classe "d'opération" particulière. Il est également ennuyeux de faire glisser une dépendance pour chaque opération que vous souhaitez effectuer sur smth. et pour mettre à jour la liste des dépendances transmises chaque fois que vous décidez que l'opération n'est plus nécessaire ou que vous avez besoin d'une nouvelle opération.
C'est également une violation de SRP. Certaines personnes traitent simplement la SRP comme un guide pour répartir les responsabilités (et vont jusqu'à loin en traitant chaque opération comme une responsabilité distincte), mais elles oublient que la SRP est également un guide pour regrouper les responsabilités. Et selon les responsabilités du SRP qui changent pour la même raison doivent être regroupées afin que si un changement se produit, il est localisé dans le moins de classes/modules possible. Quant aux grandes classes, ce n'est pas un problème d'avoir plusieurs algorithmes dans la même classe tant que ces algorithmes sont liés (c'est-à-dire qu'ils partagent des connaissances qui ne devraient pas être connues en dehors de cette classe). Le problème, ce sont les grandes classes qui ont des algorithmes qui ne sont liés d'aucune façon et qui changent/varient pour différentes raisons.