Classe qui ne représente rien - est-ce correct?

42

Je suis en train de concevoir mon application et je ne suis pas sûr de bien comprendre SOLID et OOP. Les classes doivent faire une chose et le faire bien, mais de l’autre côté, elles doivent représenter de vrais objets avec lesquels nous travaillons.

Dans mon cas, je procède à une extraction de caractéristiques sur un jeu de données, puis à une analyse par apprentissage automatique. Je suppose que je pourrais créer trois classes

  1. FeatureExtractor
  2. DataSet
  3. Analyseur

Mais la classe FeatureExtractor ne représente rien, elle fait quelque chose qui la rend plus une routine qu'une classe. Une seule fonction sera utilisée: extract_features ()

Est-il correct de créer des classes qui ne représentent pas une chose mais font une chose?

EDIT: je ne sais pas si c'est important, mais j'utilise Python

Et si extract_features () ressemblerait à cela: vaut-il la peine de créer une classe spéciale pour cette méthode?

def extract_features(df):
    extr = PhrasesExtractor()
    extr.build_vocabulary(df["Text"].tolist())

    sent = SentimentAnalyser()
    sent.load()

    df = add_features(df, extr.features)
    df = mark_features(df, extr.extract_features)
    df = drop_infrequent_features(df)
    df = another_processing1(df)
    df = another_processing2(df)
    df = another_processing3(df)
    df = set_sentiment(df, sent.get_sentiment)
    return df
Alicja Głowacka
la source
13
Cela semble parfaitement bien en fonction. Considérer les trois choses que vous avez énumérées en tant que modules est correct, et vous voudrez peut-être les placer dans différents fichiers, mais cela ne signifie pas qu'elles doivent être des classes.
Bergi
31
Sachez qu'il est assez courant et acceptable d'utiliser des approches non-OO en Python.
JPMc26
13
Vous pouvez être intéressé par Domain Driven Design. Les "classes devraient représenter des objets du monde réel" est en réalité faux ... elles devraient représenter des objets du domaine . Le domaine est souvent fortement lié au monde réel, mais selon l'application, certaines choses peuvent être considérées ou non comme des objets, ou certaines choses qui, "en réalité", sont distinctes peuvent se retrouver soit liées, soit identiques dans le domaine de l'application.
Bakuriu
1
À mesure que vous vous familiariserez mieux avec la programmation orientée objet, je pense que vous constaterez que les classes correspondent très rarement à des entités individuelles. Par exemple, voici un essai qui affirme qu'essayer de créer toutes les fonctionnalités associées à une entité du monde réel dans une classe unique est très souvent un anti-motif: programmer.97things.oreilly.com/wiki/index.php/…
Kevin - Rétablir Monica
1
"Ils devraient représenter des objets réels avec lesquels nous travaillons." pas nécessairement. Beaucoup de langages ont une classe de flux représentant un flux d'octets, qui est un concept abstrait plutôt qu'un "objet réel". Techniquement, un système de fichiers n'est pas non plus un "objet réel", c'est juste un concept, mais il existe parfois des classes représentant un système de fichiers ou une partie de celui-ci.
Pharap

Réponses:

96

Les classes doivent faire 1 chose et bien le faire

Oui, c'est généralement une bonne approche.

mais d'autre part, ils devraient représenter un objet réel avec lequel nous travaillons.

Non, c'est un malentendu commun à mon humble avis. L'accès à la programmation orientée objet pour un bon débutant est souvent "commencer par des objets représentant des choses du monde réel" , c'est vrai.

Cependant, vous ne devriez pas vous arrêter avec ça !

Les classes peuvent (et devraient) être utilisées pour structurer votre programme de différentes manières. Modéliser des objets du monde réel en est un aspect, mais pas le seul. La création de modules ou de composants pour une tâche spécifique est un autre cas d'utilisation judicieux pour les classes. Un "extracteur de fonctionnalités" est probablement un tel module, et même s'il ne contient qu'une méthode publiqueextract_features() , je serais étonné si ne contenait pas aussi beaucoup de méthodes privées et peut-être un état partagé. Donc, avoir une classe FeatureExtractorintroduira un emplacement naturel pour ces méthodes privées.

Note secondaire: dans des langages tels que Python qui prennent en charge un concept de module distinct, on peut également utiliser un module FeatureExtractorpour cela, mais dans le contexte de cette question, il s'agit d'une différence négligeable à mon humble avis.

De plus, un "extracteur de fonctionnalités" peut être imaginé comme "une personne ou un robot qui extrait des fonctionnalités". C'est une "chose" abstraite, peut-être pas quelque chose que vous trouverez dans le monde réel, mais le nom lui-même est une abstraction utile, qui donne à chacun une idée de ce que la responsabilité de cette classe est. Je ne suis donc pas d'accord pour dire que cette classe ne "représente rien".

Doc Brown
la source
33
J'aime particulièrement le fait que vous avez mentionné la question de l'état interne. Je trouve souvent que c'est le facteur déterminant pour savoir si je fais de quelque chose une classe ou une fonction en Python.
David Z
17
Vous semblez recommander la «classite». Ne faites pas de classe pour cela, c'est un anti-modèle de style Java. Si c'est une fonction, faites-en une fonction. L'état interne n'est pas pertinent: les fonctions peuvent avoir cela (via des fermetures).
Konrad Rudolph
25
@ KonradRudolph: vous semblez avoir manqué le fait qu'il ne s'agit pas de créer une fonction. Il s'agit d'un morceau de code qui nécessite plusieurs fonctions, un nom commun et peut-être un état partagé. L'utilisation d'un module à cette fin peut être judicieuse dans les langages ayant un concept de module distinct de celui des classes.
Doc Brown
8
@DocBrown Je vais devoir être en désaccord. C'est une classe avec une méthode publique. API sage, c'est impossible à distinguer d'une fonction. Voir ma réponse pour plus de détails. L'utilisation de classes à méthode unique peut être justifiée si vous devez créer un type spécifique pour représenter un état. Mais ce n'est pas le cas ici (et même alors, des fonctions peuvent parfois être utilisées).
Konrad Rudolph
6
@DocBrown Je commence à comprendre ce que vous voulez dire: vous parlez des fonctions qui sont appelées de l'intérieur extract_features? J'ai en quelque sorte simplement supposé qu'il s'agissait de fonctions publiques d'un autre endroit. Très bien, je conviens que si elles sont privées, elles devraient probablement entrer dans un module (mais quand même: pas dans une classe, à moins de partager l’état), avec extract_features. (Cela dit, vous pouvez bien sûr les déclarer localement dans cette fonction.)
Konrad Rudolph
44

Doc Brown est sur place: les classes n'ont pas besoin de représenter des objets du monde réel. Ils ont juste besoin d'être utiles . Les classes ne sont fondamentalement que des types additionnels, et à quoi correspond intou stringcorrespond le monde réel? Ce sont des descriptions abstraites et non des choses concrètes et tangibles.

Cela dit, votre cas est spécial. Selon votre description:

Et si extract_features () ressemblerait à cela: vaut-il la peine de créer une classe spéciale pour cette méthode?

Vous avez absolument raison: si votre code est tel qu’il est montré, il ne sert à rien de le transformer en classe. Il y a un discours célèbre qui fait valoir que ces utilisations des classes en Python sont odeur de code, et que les fonctions simples sont souvent suffisantes. Votre cas en est un exemple parfait.

La surutilisation des classes est due au fait que la programmation orientée objet est devenue un courant dominant avec Java dans les années 1990. Malheureusement, Java ne disposait pas à l’époque de nombreuses fonctionnalités du langage moderne (telles que les fermetures), ce qui signifie que de nombreux concepts étaient difficiles, voire impossibles à exprimer, sans l’utilisation de classes. Par exemple, il était impossible jusqu'à récemment en Java de disposer de méthodes comportant un état (c'est-à-dire des fermetures). Au lieu de cela, vous deviez écrire une classe pour porter l’état et exposer une seule méthode (appelée quelque chose comme invoke).

Malheureusement, ce style de programmation est devenu populaire bien au-delà de Java (en partie grâce à un ouvrage de génie logiciel influent par ailleurs très utile), même dans des langages ne requérant pas de telles solutions de contournement.

En Python, les classes sont évidemment un outil très important et doivent être utilisées généreusement. Mais ils ne sont pas le seul outil et il n’ya aucune raison de les utiliser là où ils n’ont pas de sens. C'est une idée fausse commune que les fonctions libres n'ont pas de place dans la POO.

Konrad Rudolph
la source
Il serait utile d'ajouter que si les fonctions appelées dans l'exemple sont en fait des fonctions privées, leur encapsulation dans un module ou une classe serait tout à fait appropriée. Sinon, je suis entièrement d'accord.
Leliel
11
Il est également utile de rappeler que "POO" ne signifie pas "écrire un tas de classes". Les fonctions sont des objets en Python, il n'est donc pas nécessaire de considérer cela comme "pas de la POO". Plutôt, il s’agit simplement de réutiliser les types / classes intégrés, et la "réutilisation" est l’un des problèmes sacrés de la programmation. Envelopper cela dans une classe empêcherait la réutilisation, car rien ne serait compatible avec cette nouvelle API (sauf __call__définition, dans ce cas, il suffit d'utiliser une fonction!)
Warbo
3
Également sur le thème "classes contre fonctions libres": eev.ee/blog/2013/03/03/…
Joker_vD
1
N'est-ce pas le cas, bien que dans Python, les "fonctions libres" soient aussi des objets avec un type exposant une méthode __call__()? Est-ce vraiment si différent d'une instance de classe interne anonyme? Syntaxiquement, bien sûr, mais du point de vue du langage, cela semble être une distinction moins importante que celle que vous présentez ici.
JimmyJames
1
@ JimmyJames Droite. Le point essentiel est qu’elles offrent les mêmes fonctionnalités dans le but spécifique mais sont plus simples à utiliser.
Konrad Rudolph
36

Je suis en train de concevoir mon application et je ne suis pas sûr de bien comprendre SOLID et OOP.

Ça fait 20 ans que je suis là et je ne suis pas sûr non plus.

Les classes doivent faire 1 chose et bien le faire

Difficile de se tromper ici.

ils devraient représenter des objets réels avec lesquels nous travaillons.

Oh vraiment? Permettez - moi de vous présenter à la classe la plus populaire et de tous les temps: String. Nous l'utilisons pour le texte. Et l'objet du monde réel qu'il représente est le suivant:

Pêcheur détient 10 poissons suspendus à un cordon

Pourquoi non, tous les programmeurs ne sont pas obsédés par la pêche. Ici, nous utilisons quelque chose appelé une métaphore. C'est bien de faire des modèles de choses qui n'existent pas vraiment. C'est l'idée qui doit être claire. Vous créez des images dans l'esprit de vos lecteurs. Ces images ne doivent pas nécessairement être réelles. Je viens de comprendre facilement.

Une bonne conception POO regroupe les messages (méthodes) autour des données (état) de sorte que les réactions à ces messages puissent varier en fonction de ces données. Si cela vous donne une idée du monde réel, spiffy. Si non, eh bien. Tant que cela a du sens pour le lecteur, ça va.

Maintenant, bien sûr, vous pourriez penser à ça comme ça:

lettres de fête suspendues à une chaîne de caractères lire "LETS DO THINGS!"

mais si vous pensez que cela doit exister dans le monde réel avant de pouvoir utiliser la métaphore, votre carrière en programmation impliquera de nombreuses activités artistiques et artisanales.

confits_orange
la source
1
Une jolie série de photos ...
Zev Spitz
À ce stade, je ne suis pas sûr que "chaîne" soit même métaphorique. Il a juste une signification spécifique dans le domaine de la programmation, de même que des mots comme classet tableet column...
Kyralessa
@ Kyralessa, vous apprenez la métaphore au débutant ou vous lui permettez que ce soit magique. Sauvez-moi des codeurs qui croient en la magie
candied_orange
6

Il faut se méfier! SOLID ne dit nulle part qu'une classe ne devrait «faire qu'une chose». Si c'était le cas, les classes n'auraient jamais qu'une seule méthode et il n'y aurait pas vraiment de différence entre les classes et les fonctions.

SOLID dit qu'une classe devrait représenter une responsabilité unique . Celles-ci ressemblent un peu aux responsabilités des membres d’une équipe: le conducteur, l’avocat, le pickpocket, le graphiste, etc. Chacune de ces personnes peut effectuer plusieurs tâches (liées), mais toutes relevant d’une seule responsabilité.

Le but de ceci est - si les exigences changent, vous ne devez idéalement que modifier une seule classe. Cela rend le code plus facile à comprendre, à modifier et à réduire les risques.

Il n’existe aucune règle selon laquelle un objet doit représenter "une chose réelle". Il s’agit simplement d’un savoir-faire culte du fret depuis que OO a été initialement inventé pour être utilisé dans des simulations. Mais votre programme n'est pas une simulation (peu d'applications OO modernes le sont), cette règle ne s'applique donc pas. Tant que chaque classe a une responsabilité bien définie, ça devrait aller.

Si une classe n'a en réalité qu'une seule méthode et qu'elle n'a aucun état, vous pouvez envisager de la transformer en une fonction autonome. C’est certes correct et respecte les principes KISS et YAGNI - inutile de faire un cours si vous pouvez le résoudre avec une fonction. D'un autre côté, si vous avez des raisons de croire que vous pourriez avoir besoin d'un état interne ou de plusieurs implémentations, vous pouvez également en faire une classe à l'avance. Vous devrez utiliser votre meilleur jugement ici.

JacquesB
la source
+1 pour "pas besoin de faire un cours si vous pouvez le résoudre avec une fonction". Parfois, quelqu'un a besoin de dire la vérité.
tchrist
5

Est-il correct de créer des classes qui ne représentent pas une chose mais font une chose?

En général c'est OK.

Sans une description un peu plus précise, FeatureExtractoril est difficile de dire ce que la classe est censée faire.

Quoi qu’il en soit, même si cela FeatureExtractorn’expose qu’une extract_features()fonction publique , je pourrais penser à la configurer avec une Strategyclasse, qui détermine exactement comment l’extraction doit être effectuée.

Un autre exemple est une classe avec une fonction Template .

Et il y a plus de modèles de conception comportementaux , basés sur des modèles de classe.


Comme vous avez ajouté du code pour des éclaircissements.

Et si extract_features () ressemblerait à cela: vaut-il la peine de créer une classe spéciale pour cette méthode?

La ligne

 sent = SentimentAnalyser()

comprend exactement ce que je voulais dire que vous pouvez configurer une classe avec une stratégie .

Si vous avez une interface pour cette SentimentAnalyserclasse, vous pouvez la transmettre à la FeatureExtractorclasse au moment de sa construction, au lieu de la coupler directement à cette implémentation spécifique de votre fonction.

πάντα ῥεῖ
la source
2
Je ne vois pas de raison d'ajouter de la complexité (une FeatureExtractorclasse), mais d'introduire encore plus de complexité (une interface pour la SentimentAnalyserclasse). Si le découplage est souhaitable, alors la fonction extract_featurespeut être considérée get_sentimentcomme un argument (l' loadappel semble indépendant de la fonction et n'est appelé que pour ses effets). Notez également que Python n'a pas / encourage les interfaces.
Warbo
1
@warbo - même si vous fournissez la fonction sous forme d'argument, vous limitez les implémentations potentielles à celles qui s'intègrent dans le format d'une fonction, mais s'il est nécessaire de gérer un état persistant entre une invocation et la Ensuite (par exemple, a CacheingFeatureExtractorou a TimeSeriesDependentFeatureExtractor), un objet conviendrait mieux. Tout simplement parce qu'il n'y a pas besoin d'un objet actuellement ne signifie pas sera jamais là.
Jules
3
@Jules Premièrement, vous n'en aurez pas besoin (YAGNI), deuxièmement, les fonctions Python peuvent faire référence à un état persistant (fermetures) si vous en avez besoin (vous n'allez pas), troisièmement, utiliser une fonction ne restreint rien, aucun objet avec __call__La méthode sera compatible si vous en avez besoin (vous ne le ferez pas), quatrièmement en ajoutant un wrapper comme FeatureExtractorsi vous rendiez le code incompatible avec tout autre code jamais écrit (à moins que vous ne fournissiez une __call__méthode, dans ce cas, une fonction serait clairement plus simple )
Warbo
0

Motifs et tous les langages / concepts sophistiqués mis à part: ce que vous avez découvert est un travail ou un traitement par lots .

En fin de compte, même un programme purement POO doit en quelque sorte être guidé par quelque chose, pour réellement effectuer un travail; il doit y avoir un point d'entrée en quelque sorte. Dans le modèle MVC, par exemple, le contrôleur "C" reçoit les événements de clic, etc. de l'interface graphique, puis orchestre les autres composants. Dans les outils de ligne de commande classiques, une fonction "principale" ferait de même.

Est-il correct de créer des classes qui ne représentent pas une chose mais font une chose?

Votre classe représente une entité qui fait quelque chose et orchestre tout le reste. Vous pouvez le nommer Controller , Job , Main ou tout ce qui vous passe par la tête.

Et si extract_features () ressemblerait à cela: vaut-il la peine de créer une classe spéciale pour cette méthode?

Cela dépend des circonstances (et je ne connais pas la manière habituelle de procéder en Python). S'il ne s'agit que d'un petit outil en ligne de commande unique, une méthode plutôt qu'une classe devrait convenir. La première version de votre programme peut s’en tirer avec une méthode, bien sûr. Si, par la suite, vous vous retrouvez avec des dizaines de méthodes de ce type, peut-être même avec des variables globales mélangées, il est temps de refactoriser les classes.

AnoE
la source
3
Notez que dans de tels scénarios, l’appel de "méthodes" de procédures autonomes peut prêter à confusion. La plupart des langages, y compris Python, les appellent "fonctions". Les "méthodes" sont des fonctions / procédures liées à une instance particulière d'une classe, ce qui est le contraire de votre utilisation du terme :)
Warbo
C'est assez vrai, @Warbo. Ou nous pourrions les appeler procédure ou defun ou sub ou ...; et il peut s'agir de méthodes de classe (sic) non liées à une instance. J'espère que le gentil lecteur saura faire abstraction du sens voulu. :)
AnoE
@Warbo c'est bon à savoir! La plupart des documents pédagogiques que j'ai rencontrés indiquent que les termes fonction et méthode sont interchangeables et qu'il ne s'agit que d'une préférence liée à la langue.
Dom
@Dom En général, une "" pure "" fonction "est un mappage de valeurs d'entrée à des valeurs de sortie; une "procédure" est une fonction qui peut également avoir des effets (par exemple, supprimer un fichier); les deux sont distribués statiquement (c'est-à-dire levés dans le périmètre lexical). Une "méthode" est une fonction ou (généralement) une procédure qui est dynamiquement distribuée (recherchée) à partir d'une valeur (appelée "objet"), qui est automatiquement liée à un argument (implicite thisou explicite self) de la méthode. Les méthodes d'un objet étant mutuellement "ouvertement" récursives, le remplacement fooentraîne donc que tous les self.fooappels utilisent ce remplacement.
Warbo
0

Nous pouvons penser à la programmation orientée objet en tant que modélisation du comportement d'un système. Notez que le système ne doit pas nécessairement exister dans le "monde réel", bien que des métaphores du monde réel puissent parfois être utiles (par exemple, "pipelines", "usines", etc.).

Si notre système souhaité est trop compliqué à modéliser en une fois, nous pouvons le décomposer en éléments plus petits et les modéliser (le "domaine du problème"), ce qui peut impliquer une décomposition plus poussée, et ainsi de suite jusqu'à ce que nous obtenions des comportements dont le comportement correspond (plus ou moins) celui d'un objet langage intégré tel qu'un nombre, une chaîne de caractères, une liste, etc.

Une fois que nous avons ces pièces simples, nous pouvons les combiner pour décrire le comportement de pièces plus grandes, que nous pouvons combiner en pièces encore plus grandes, et ainsi de suite jusqu'à ce que nous puissions décrire toutes les composantes du domaine nécessaires à l'ensemble. système.

C'est cette phase de "combinaison" dans laquelle nous pourrions écrire certaines classes. Nous écrivons des classes quand il n'y a pas d'objet existant qui se comporte comme nous le voulons. Par exemple, notre domaine peut contenir des "foos", des collections de foos appelées "bars" et des collections de barres appelées "bazs". Nous pouvons remarquer que les foos sont assez simples pour modéliser avec des chaînes, alors nous le faisons. Nous constatons que les barres exigent que leur contenu obéisse à une contrainte particulière qui ne correspond à rien de ce que Python fournit, auquel cas nous pourrions écrire une nouvelle classe pour appliquer cette contrainte. Peut-être que les bazs n'ont pas de telles particularités, alors nous pouvons simplement les représenter avec une liste.

Notez que nous pourrions écrire une nouvelle classe pour chacun de ces composants (foos, bars et bazs), mais nous n’avons pas besoin de le faire s’il existe déjà un comportement correct. En particulier, pour une classe soit utile , il doit «fournir quelque chose (données, les méthodes, les constantes, les sous - classes, etc.), même si nous avons beaucoup de couches de classes personnalisées , nous devons éventuellement utiliser une fonctionnalité intégrée; Par exemple, si nous écrivions une nouvelle classe pour les foos, elle ne contiendrait probablement qu'une chaîne, alors pourquoi ne pas oublier la classe foo et laisser la classe bar contenir ces chaînes à la place? Gardez à l'esprit que les classes sont aussi un objet intégré, elles sont simplement particulièrement flexibles.

Une fois que nous avons notre modèle de domaine, nous pouvons prendre certaines instances particulières de ces éléments et les organiser dans une "simulation" du système particulier que nous souhaitons modéliser (par exemple, "un système d'apprentissage automatique pour ...").

Une fois que nous avons cette simulation, nous pouvons la lancer et hop, nous avons un système de travail (simulation d’un) apprentissage automatique pour ... (ou tout ce que nous modélisons).


Dans votre cas particulier, vous essayez de modéliser le comportement d'un composant "extracteur de fonctionnalités". La question est la suivante: existe-t-il des objets intégrés se comportant comme un "extracteur de fonctionnalités", ou devrez-vous les séparer en éléments plus simples? Il semble que les extracteurs de caractéristiques se comportent beaucoup comme des objets fonction. Je pense donc que vous seriez prêt à les utiliser comme modèle.


Une chose à garder à l'esprit lorsque vous étudiez ces types de concepts est que différentes langues peuvent fournir différentes fonctionnalités et objets intégrés (et, bien sûr, certains n'utilisent même pas une terminologie telle que "objets"!). Par conséquent, des solutions qui ont du sens dans une langue pourraient être moins utiles dans une autre (cela peut même s'appliquer à différentes versions de la même langue!).

Historiquement, une grande partie de la littérature de la programmation orientée objet (en particulier des "modèles de conception") s'est concentrée sur Java, qui est assez différent de Python. Par exemple, les classes Java ne sont pas des objets, Java n’avait pas d’objets fonction jusqu’à très récemment, Java applique une vérification de type stricte (ce qui encourage les interfaces et les sous-classes) tandis que Python encourage le typage, les Java, n’a pas d’objets modules, les entiers Java / flotteurs / etc. les objets ne sont pas des objets, la méta-programmation / introspection en Java nécessite une "réflexion", etc.

Je n'essaie pas de parler de Java (un autre exemple, beaucoup de théorie de la programmation orientée objet tourne autour de Smalltalk, qui est encore très différent de Python), j'essaie simplement de souligner que nous devons réfléchir très soigneusement au contexte et contraintes dans lesquelles des solutions ont été développées, et si cela correspond à la situation dans laquelle nous nous trouvons.

Dans votre cas, un objet de fonction semble être un bon choix. Si vous vous demandez pourquoi certaines directives de "bonne pratique" ne mentionnent pas les objets fonction comme solution possible, c'est peut-être tout simplement parce que ces instructions ont été écrites pour les anciennes versions de Java!

Warbo
la source
0

En termes pragmatiques, quand j’ai une "chose diverse qui fait quelque chose d’important et qui devrait être séparée", et qui n’a pas de foyer clair, je le mets dans une Utilitiessection et l’utilise comme convention de nommage. c'est à dire. FeatureExtractionUtility.

Oubliez le nombre de méthodes dans une classe; aujourd'hui, une seule méthode peut nécessiter cinq méthodes demain. Ce qui compte, c'est une structure organisationnelle claire et cohérente, telle qu'un espace utilitaires pour diverses collections de fonctions.

Dom
la source