Как избежать вызова Viritual методов из базового конструктора

у меня есть абстрактный класс в библиотеке. Я пытаюсь максимально упростить правильную реализацию деривации этого класса. Беда в том, что мне нужно инициализировать объект в три этапа: возьмите файл, сделать несколько промежуточных шагов, а затем работать с файлом. Первый и последний шаг для производного класса. Вот урезанный пример.

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();
    }
}

проблема, конечно, заключается в вызове виртуального метода в конструкторе. Боюсь, что потребитель библиотеки будет ограничен при использовании класса, если он не может рассчитывать на полную инициализацию производного класса.

я мог бы вытащить логику из конструктора в защищенном Initialize() метод, но тогда исполнитель может вызвать Step1() и Step3() непосредственно вместо вызова Initialize(). Суть проблемы заключается в том, что не было бы очевидной ошибки, если Step2() пропускается; просто ужасная производительность в определенных ситуациях.

I почувствуйте, что в любом случае есть серьезная и неочевидная "gotcha", с которой будущим пользователям библиотеки придется работать. Есть ли какой-то другой дизайн, который я должен использовать для достижения такого рода инициализации?

Я могу предоставить более подробную информацию, если это необходимо; я просто пытался предоставить самый простой пример, который выразил проблему.

8 ответов


Это слишком много для размещения в конструкторе любого класса, а тем более базового класса. Я предлагаю вам разложить это на отдельные Initialize метод.


Я бы рассмотрел возможность создания абстрактная фабрика, который отвечает за создание и инициализацию экземпляров производных классов с помощью метод шаблона для инициализации.

в качестве примера:

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>();

этот заводской код может быть значительно улучшен-особенно если вы готовы использовать контейнер IoC для обработки этой ответственности...

Если у вас нет контроля над производными классами, вы можете невозможно запретить им предлагать общедоступный конструктор, который можно вызвать , но, по крайней мере, вы можете установить шаблон использования, которого могли бы придерживаться потребители.

не всегда возможно запретить пользователям ваших классов стрелять в ногу , но вы можете предоставить инфраструктуру, чтобы помочь потребителям правильно использовать ваш код, когда они знакомятся с дизайном.


во многих случаях материал инициализации включает назначение некоторых свойств. Можно сделать эти свойства сами abstract и имеют производный класс переопределить их и вернуть некоторое значение вместо передачи значения в базовый конструктор для установки. Конечно, применима ли эта идея, зависит от характера вашего конкретного класса. Во всяком случае, наличие такого количества кода в конструкторе воняет.


на первый взгляд, я бы предложил переместить такую логику в методы, основанные на этой инициализации. Что-то вроде

public class Base
{
   private void Initialize()
   {
      // do whatever necessary to initialize
   }

   public void UseMe()
   {
      if (!_initialized) Initialize();
      // do work
   }
}

поскольку Шаг 1 "захватывает файл", было бы неплохо инициализировать(IBaseFile) и пропустить Шаг 1. Таким образом, потребитель может получить файл, как ему заблагорассудится, поскольку он в любом случае абстрактен. Вы все равно можете предложить "StepOneGetFile ()" как абстрактный, который возвращает файл, чтобы они могли реализовать его таким образом, если захотят.

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

Edit: я почему-то ответил на это для C++. Извиняюсь. для C# я рекомендую против Create() метод-используйте конструктор и убедитесь, что объекты остаются в допустимом состоянии с самого начала. C# разрешает виртуальные вызовы из конструктора, и их можно использовать, если вы тщательно документируете их ожидаемую функцию и предварительные и последующие условия. Я сделал вывод C++ в первый раз, потому что он не позволяет виртуальные вызовы из конструктора.

сделать индивидуальные функции инициализации private. Может быть как private и virtual. Тогда предложите публичный, не виртуальный Initialize() функция, которая вызывает их в правильном порядке.

если вы хотите убедиться, что все происходит по мере создания объекта, сделайте конструктор protected и используйте static Create() функция в ваших классах, которая вызывает Initialize() перед возвратом вновь созданного объекта.


можно использовать следующий трюк, чтобы убедиться, что инициализация выполняется в правильном порядке. Предположительно, у вас есть некоторые другие методы (DoActualWork) реализован в базовом классе, который полагается на инициализацию.

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");
    }
}

Я бы не стал этого делать. Обычно я нахожу, что любая "реальная" работа в конструкторе заканчивается плохой идеей.

как минимум, иметь отдельный метод для загрузки данных из файла. Вы можете сделать аргумент, чтобы сделать шаг дальше и иметь отдельный объект, ответственный за создание одного из ваших объектов из файла, отделяя проблемы "загрузка с диска" и операции в памяти на объекте.