Как избежать вызова 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"); } }
Я бы не стал этого делать. Обычно я нахожу, что любая "реальная" работа в конструкторе заканчивается плохой идеей.
как минимум, иметь отдельный метод для загрузки данных из файла. Вы можете сделать аргумент, чтобы сделать шаг дальше и иметь отдельный объект, ответственный за создание одного из ваших объектов из файла, отделяя проблемы "загрузка с диска" и операции в памяти на объекте.