Как избежать вызова виртуальных методов из базового конструктора - PullRequest
9 голосов
/ 20 июля 2009

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

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(); просто ужасная производительность в определенных ситуациях.

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

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

Ответы [ 8 ]

10 голосов
/ 20 июля 2009

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

Как пример:

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 для выполнения этой обязанности ...

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

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

3 голосов
/ 20 июля 2009

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

1 голос
/ 20 июля 2009

Вы можете использовать следующий прием, чтобы убедиться, что инициализация выполняется в правильном порядке. Предположительно, у вас есть несколько других методов ( 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");
    }
}
1 голос
/ 20 июля 2009

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

DerivedClass foo = DerivedClass();
foo.Initialize(StepOneGetFile('filepath'));
foo.DoWork();
1 голос
/ 20 июля 2009

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

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

   public void UseMe()
   {
      if (!_initialized) Initialize();
      // do work
   }
}
1 голос
/ 20 июля 2009

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

1 голос
/ 20 июля 2009

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

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

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

0 голосов
/ 21 июля 2009

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

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

...