Comment le concept d'une classe change-t-il lors du passage de données au constructeur au lieu des paramètres de méthode?

12

Disons que nous faisons un analyseur. Une mise en œuvre pourrait être:

public sealed class Parser1
{
    public string Parse(string text)
    {
       ...
    }
}

Ou nous pourrions passer le texte au constructeur à la place:

public sealed class Parser2
{
    public Parser2(string text)
    {
       this.text = text;
    }

    public string Parse()
    {
       ...
    }
}

L'utilisation est simple dans les deux cas, mais qu'est-ce que cela signifie d'activer la saisie des paramètres Parser1par rapport à l'autre? Quel message ai-je envoyé à un collègue programmeur, quand il regarde l'API? Existe-t-il également des avantages / inconvénients techniques dans certains cas?

Une autre question se pose lorsque je me rends compte qu'une interface n'aurait aucun sens dans la deuxième implémentation:

public interface IParser
{
    string Parse();
}

... où une interface sur la première pourrait au moins servir à quelque chose. Cela signifie-t-il quelque chose en particulier, qu'une classe est "interfaçable" ou non?

ciscoheat
la source
C'est la première fois que je vois des questions / réponses utiles sur la façon dont la sémantique de OO exprime réellement l'intention. Jusqu'à présent, tout cela n'était que du sucre syntaxique, pour moi.

Réponses:

11

Sémantiquement, dans la POO, vous ne devez transmettre au constructeur qu'un ensemble de paramètres nécessaires à la construction de la classe - de même, lorsque vous appelez une méthode, vous ne devez lui transmettre que les paramètres dont il a besoin pour exécuter sa logique métier.

Les paramètres que vous devez passer dans le constructeur sont des paramètres qui n'ont pas de valeur par défaut sensible et si votre classe est immuable (ou bien a struct), alors les propriétés toutes non par défaut doivent être passées.

Concernant vos deux exemples:

  • si vous passez textau constructeur, cela indique que la Parser2classe sera spécifiquement construite pour analyser cette instance de texte ultérieurement. Ce sera un analyseur spécifique. C'est généralement le cas lorsque la construction de la classe est très coûteuse ou subtile, un RegEx peut être compilé dans le constructeur, donc une fois que vous détenez une instance, vous pouvez la réutiliser sans avoir à payer le coût de la compilation; un autre exemple est l'initialisation du PRNG - il vaut mieux que cela soit fait rarement.
  • si vous passez textà la méthode, elle signale qu'elle Parser1peut être réutilisée pour analyser différents textes par des appels.
Sklivvz
la source
3
J'ajouterais qu'il y a un signal faible que Parser1 maintient l'état - c'est-à-dire qu'une chaîne de texte particulière peut produire des résultats différents en fonction de ce qui a été fait précédemment sur l'instance tat. Ce n'est pas nécessairement le cas, mais c'est possible.
jmoreno
8

Eh bien, rappelons ce que signifie passer une variable comme paramètre constructeur: vous initialisez un objet afin d'utiliser ses variables d'instance dans les méthodes de l'objet. Le fait est que vous voulez probablement l'utiliser dans plusieurs méthodes, car vous voulez avoir une cohésion élevée dans votre classe.

Passer un paramètre directement à une méthode signifie en quelque sorte envoyer un message à un objet et probablement recevoir une réponse. Par cela, le client veut que l'objet lui fournisse un service.

Donc, en conclusion, ce sont deux moyens très différents de transmettre des paramètres et vous devez choisir si votre objet doit fournir un service ou fournir des fonctionnalités de manière inhérente tout en gérant certaines informations en interne.

McMannus
la source
+1. La cohésion est la façon dont je décide si elle appartient à la méthode ou au constructeur.
jhewlett
4

L'utilisation est simple dans les deux cas, mais qu'est-ce que cela signifie d'activer l'entrée de paramètres dans Parser1, par rapport à l'autre?

C'est un changement de conception fondamental. Et le design doit transmettre l'intention et le sens. Avez-vous besoin d'avoir des objets séparés pour chaque chaîne que vous souhaitez analyser? En d'autres termes, pourquoi avons-nous besoin d'une instance d'analyseur avec stringX et d'une autre instance avec stringY? Qu'en est-il de l'analyse et de la chaîne donnée que les deux doivent vivre et mourir ensemble? En supposant que «l'implémentation [d'analyse] sous-jacente» (comme le dit Robert Harvey) ne change pas, cela ne semble d'aucune utilité. Et même alors son IMHO discutable.

Comment le concept d'une classe change-t-il lors du passage de données au constructeur au lieu des paramètres de méthode?

Les paramètres du constructeur me disent que ces choses sont requises pour un objet. Un bon état n'est pas garanti sans eux. De plus, je sais comment / pourquoi un analyseur est fondamentalement différent d'un autre.

Les paramètres du constructeur m'empêchent d'avoir à en savoir trop sur la façon d'utiliser la classe. Si, à la place, je suis censé définir certaines propriétés - comment savoir cela? Toute une boîte de vers s'ouvre. Quelles propriétés? Dans quel ordre? Avant d'utiliser quelles méthodes? etc.

Une autre question se pose lorsque je me rends compte qu'une interface n'aurait aucun sens dans la deuxième implémentation:

Une interface, comme dans l'API, est les méthodes et propriétés exposées au code client. Ne vous enveloppez pas public interface { ... }exclusivement. Ainsi, la signification de l'interface est dans le dilemme du paramètre soit-ou constructeur vs méthode, PAS public interface Iparservspublic sealed class Parser

La sealedclasse est bizarre. Si je pense à différentes implémentations d'analyseur - vous avez mentionné "Iparser" - alors l'héritage est ma première pensée. C'est juste une extension conceptuelle naturelle de ma pensée. IE tous les ParserXs sont fondamentalement Parsers. Comment le dire autrement? ... Un berger allemand est un chien (héritage), mais je peux entraîner mon perroquet à aboyer (agir comme un chien - "interface"); mais Polly n'est pas un chien, prétendant simplement, ayant appris un sous-ensemble de dogness. Les classes, abstraites ou non, servent parfaitement d' interfaces .

radarbob
la source
S'il marche comme un analyseur et parle comme un analyseur, c'est ... un canard!
2

La deuxième version de la classe peut être rendue immuable.

L'interface peut toujours être utilisée pour permettre d'échanger l'implémentation sous-jacente.

Robert Harvey
la source
Ne peut-il pas être rendu immuable dans la première version également, en transmettant les données de manière fonctionnelle au sein de la classe?
ciscoheat
2
Absolument. Mais le modèle général d'immutabilité consiste à définir les membres de la classe à l'aide du constructeur et à avoir des propriétés en lecture seule. Pour la programmation fonctionnelle, vous n'avez même pas besoin d'une classe.
Robert Harvey
Scylla et Charybdis: est-ce que je choisis OO ou des données immuables? Je ne l'avais jamais entendu dire ainsi.
2

Parser1

Construire avec un constructeur par défaut et passer du texte d'entrée dans une méthode implique que Parser1 est réutilisable.

Parser2

Passer le texte d'entrée dans le constructeur implique qu'un nouveau Parser2 doit être créé pour chaque chaîne d'entrée.

Mike Partridge
la source
Bien sûr, si nous passons au niveau supérieur, que conclure d'un objet réutilisable par rapport à un objet non réutilisable?
ciscoheat
Je n'assumerais pas plus que cela, au lieu de cela, je m'en remettrais à la documentation.
Mike Partridge
Un «objet réutilisable» où son identité change n'est pas un séquençage. Et avec des cadres gérés signifiant que vous n'avez même pas à jeter les choses, simplement les écraser ou les faire sortir du champ d'application, ce n'est certainement pas nécessaire. Construisez loin!