9
votes

Comment éviter d'appeler des méthodes virtuelles à partir d'un constructeur de base

J'ai une classe abstraite dans une bibliothèque. J'essaie de le rendre aussi simple que possible de mettre en œuvre correctement une dérivation de cette classe. Le problème est que je dois initialiser l'objet dans un processus en trois étapes: saisissez un fichier, faites quelques étapes intermédiaires, puis travaillez avec le fichier. La première et dernière étape sont particulières à la classe dérivée. Voici un exemple dépouillé.

abstract class Base
{
    // grabs a resource file specified by the implementing class
    protected abstract void InitilaizationStep1();

    // performs some simple-but-subtle boilerplate stuff
    private void InitilaizationStep2() { return; }

    // works with the resource file
    protected abstract void InitilaizationStep3();

    protected Base()
    {
        InitilaizationStep1();
        InitilaizationStep2();
        InitilaizationStep3();
    }
}


0 commentaires

8 Réponses :


4
votes

C'est trop trop placer dans le constructeur de n'importe quelle classe, beaucoup moins de classe de base. Je vous suggère de définir cela dans une méthode distincte initialiser .


7 commentaires

Je suis d'accord. Une autre approche serait de conserver les dépendances de cet objet lors de la création.


J'aime votre solution, mais l'autre possibilité d'envisager peut être de casser le code dans les étapes 1 et 3 dans une paire d'interfaces transmises au constructeur. Le constructeur peut les appeler au bon moment pour faire leur part. En bref, cela pourrait bien être l'un de ces endroits où la composition bat héritage.


Cela ne répond pas à ses préoccupations concernant l'appel des étapes de la séquence et un moyen de simplifier la création de classes dérivées.


Premièrement, sauf s'il y a quelque chose à propos de cette hiérarchie de classe qui le rend inhérent à l'init prendra toujours trois étapes, dans cette séquence, elle ne doit pas être imposée par la classe de base. Deuxièmement, si c'est est inhérent, la méthode d'initialisation de la classe de base peut appeler des méthodes d'initialisation Virtual Step1, Step2, Step3, dans la séquence: le modèle de fonction de modèle.


La séquence est inhérente. Fondamentalement, un fichier est chargé à l'étape1 et Step3 définit une liaison des données du programme sur le fichier. Étant donné que le processus de liaison est coûteux, STEP2 garantit que la liaison est paresseuse et mise en cache. Ma mise en œuvre actuelle est fondamentalement un motif de méthode de modèle, mais le TMP idiomatique sûr de C # nécessite un appel explicite à la méthode Initialize () dans le constructeur de base ou dans le code d'appel. Le premier est préférable à celui-ci, mais les conséquences d'une défaillance dans les deux cas pourraient être une énorme consommation de mémoire se produisant longtemps après que le code soit envoyé dans la nature.


Vous venez de dire que la mise en œuvre est inhérente aux classes. Le fait qu'un fichier soit utilisé est un problème de mise en œuvre. L'existant d'une cartographie est une question de mise en œuvre. Ceux-ci ne devraient pas faire partie de la nature de ces classes. Je vous recommanderais de définir ce code dans une hiérarchie des classes d'assistance, appelées par les classes d'origine.


Une correction rapide: lorsque j'ai dit "C # nécessite un appel explicite à la méthode d'initialisation () dans le constructeur de base ou dans le code d'appel", est bien sûr conçu pour dire le constructeur dérivé. Cela de côté, alors que j'ai refactoré le code pour essayer quelques idées, dans tous les cas, tous les cas, tout le chargement et le code de liaison ont naturellement trouvé leur chemin dans les classes d'assistance. En bref, vous avez raison que la classe de base a eu beaucoup de responsabilités.



1
votes

EDIT: J'ai répondu à cela pour C ++ pour une raison quelconque. Désolé. pour C # Je recommande contre un Créer () méthode - utilisez le constructeur et assurez-vous que les objets restent dans un état valide du début. C # Permet aux appels virtuels du constructeur et il est correct de les utiliser si vous documentez soigneusement leur fonction et vos conditions préliminaires attendues. J'ai déduit C ++ la première fois parce qu'il n'autorise pas les appels virtuels du constructeur.

Fonctionnez les fonctions d'initialisation individuelles privées . Le code peut être à la fois privé et virtuel . Ensuite, offrez une fonction publique d'initialisation (code> Initialiser () () qui les appelle dans le bon ordre.

Si vous voulez vous assurer que tout se passe car l'objet est créé, rendez le constructeur protégé et utilisez une fonction statique () fonction dans vos classes qui appelle Initialiser () avant de retourner l'objet nouvellement créé.


3 commentaires

Quel est le point d'une fonction virtuelle privée - il ne peut pas être remplacé.


@John: désolé. C # Permet aux appels virtuels du constructeur et il est correct de les utiliser si vous documentez soigneusement leur fonction et vos conditions préliminaires attendues. J'ai déduit C ++ la première fois parce qu'il n'autorise pas les appels virtuels du constructeur.


La méthode Create (), comme je le vois, est similaire à un motif d'usine. Bien qu'il soit un peu plus limité, il a une empreinte beaucoup plus petite en termes de taille de l'API et pourrait répondre à mes besoins dans cette situation. Vous avez recommandé contre elle; Y a-t-il une limitation critique que je néglige-t-elle? C'est un modèle que j'ai utilisé auparavant (pour un code beaucoup moins important) et cela semblait fonctionner d'accord. Convaincu moi c'est une mauvaise idée, ou je vais lui donner une idée sérieuse. :)



1
votes

Dans de nombreux cas, les choses d'initialisation impliquent l'attribution de certaines propriétés. Il est possible de faire ces propriétés elles-mêmes abstrait et ont une classe dérivée les remplacer et renvoyer une certaine valeur au lieu de passer la valeur au constructeur de base à définir. Bien sûr, si cette idée est applicable dépend de la nature de votre classe spécifique. Quoi qu'il en soit, avoir que beaucoup de code dans le constructeur est malodorant.


1 commentaires

Une idée intéressante, mais malheureusement non applicable dans mon cas. Merci quand même!



1
votes

À première vue, je suggérerais de déplacer ce type de logique aux méthodes qui s'appuient sur cette initialisation. Quelque chose comme xxx


1 commentaires

Valdv et Valdv avaient des suggestions similaires; Fait arbitrairement, j'ai répondu à son: Stackoverflow.com/Questtions/1155305/... .



11
votes

Je envisagerais de créer un abstrait usine qui est responsable de l'instanciation et de l'initialisation des instances de votre dérivée. classes utilisant un Méthode de modèle pour l'initialisation.

Par exemple: P>

public abstract class Widget
{
    protected abstract void InitializeStep1();
    protected abstract void InitializeStep2();
    protected abstract void InitializeStep3();

    protected internal void Initialize()
    {
        InitializeStep1();
        InitializeStep2();
        InitializeStep3();
    }

    protected Widget() { }
}

public static class WidgetFactory
{
    public static CreateWidget<T>() where T : Widget, new()
    {
        T newWidget = new T();
        newWidget.Initialize();
        return newWidget;
    }
}

// consumer code...
var someWidget = WidgetFactory.CreateWidget<DerivedWidget>();


5 commentaires

Sa question est de manière claire de mettre en œuvre une classe abstraite et non la création des versions dérivées.


Je ne suis pas d'accord. Il cherche un modèle où les cours dérivés effectueront une initialisation structurée - et il est préoccupé par le fait que ces dérivés ne pouvaient pas exécuter toutes les étapes d'initialisation nécessaires dans l'ordre approprié. Une usine qui effectue la construction et l'initialisation peut aider à résoudre ce problème. En particulier, l'alternative d'appeler des méthodes virtuelles dans un constructeur étant (en général) une pratique indésirable.


Lbushkin a raison de mes besoins. Je ne suis pas sûr que votre suggestion est une mise en œuvre littérale d'une usine abstraite, mais j'aime que ces génériques prennent le fardeau de la mise en œuvre, en ce sens qu'il n'y a pas besoin d'une classe dérivée. Demander à l'écrivain de tirer le constructeur protégé est assez raisonnablement. Ma principale préoccupation serait le coût de la simplicité du code d'appel. De plus, pour la cohérence de l'API, je voudrais probablement déplacer toute la génération d'objets dans des usines. Cela pourrait être bénéfique dans l'ensemble, mais c'est quelque chose que je devrai considérer.


Citation "J'essaie de le rendre aussi facile que possible de mettre en œuvre correctement une dérivation de cette classe"


Comment cela aidera-t-il dans le cas si un client crée directement une instance d'un héritier de widget en appelant le constructeur par défaut? Et si un client ne sait pas que une sorte d'usine doit être utilisée?



1
votes

Étant l'étape 1 "attrape un fichier", il peut être utile d'initialiser (ibasefile) et de sauter l'étape 1. De cette façon, le consommateur peut obtenir le fichier mais ils veulent - car il est abstrait de toute façon. Vous pouvez toujours offrir un «steponeetfilefile ()» comme résumé qui renvoie le fichier, afin qu'ils puissent la mettre en œuvre de cette façon s'ils choisissent.

DerivedClass foo = DerivedClass();
foo.Initialize(StepOneGetFile('filepath'));
foo.DoWork();


1 commentaires

J'aime l'idée de pousser des objets séparés dans la méthode d'initialisation; Il supprime les trous potentiels créés par les méthodes STEP1 () et STEP3 (). Il met un peu plus de fardeau sur la classe dérivée, mais cela en fait de telle sorte qu'il n'y ait qu'un moyen d'utiliser la classe de base. La route est plus longue, mais plus droite. Je pense que je préférerais continuer à initialiser protégé pour mes besoins, mais le principe est le même.



1
votes

Vous pouvez utiliser le truc suivant pour vous assurer que l'initialisation est effectuée dans le bon ordre. Vraisemblablement, vous avez d'autres méthodes ( doactualwork em>) implémentées dans la classe de base, qui s'appuie sur l'initialisation.

abstract class Base
{
    private bool _initialized;

    protected abstract void InitilaizationStep1();
    private void InitilaizationStep2() { return; }
    protected abstract void InitilaizationStep3();

    protected Initialize()
    {
        // it is safe to call virtual methods here
        InitilaizationStep1();
        InitilaizationStep2();
        InitilaizationStep3();

        // mark the object as initialized correctly
        _initialized = true;
    }

    public void DoActualWork()
    {
        if (!_initialized) Initialize();
        Console.WriteLine("We are certainly initialized now");
    }
}


1 commentaires

Lorsque j'ai examiné à l'aide de ce modèle, j'ai découvert que ma classe avait trop de points d'entrée pour que cette approche soit propre et pratique. En particulier, il existe un certain nombre de surcharges pour recevoir des informations de liaison et récupérer ces informations dans divers formats. Cela a à son tour suggéré de vouloir refroidir ces responsabilités dans une nouvelle classe. Bonne idée quand même, bien que peut-être un peu hacky. Lorsque plusieurs méthodes nécessitent une initialisation appropriée, la prise en compte du code commun vous emmène dans la direction d'une patten d'usine? Quoi qu'il en soit, merci pour les conseils et l'inspiration.



0
votes

Je ne ferais pas ça. Je trouve généralement que faire tout travail «réel» dans un constructeur finit par être une mauvaise idée sur la route.

Au minimum, disposez d'une méthode distincte pour charger les données d'un fichier. Vous pouvez faire une dispute pour que cela soit plus approfondi et avoir un objet distinct responsable de la construction de l'un de vos objets du fichier, de séparer les préoccupations du "chargement à partir de disque" et des opérations en mémoire sur l'objet.


0 commentaires