2009-07-20 2 views
8

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

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

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

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

ответ

3

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

+1

Согласен. Другим подходом было бы обслуживание зависимостей, которые этот объект имеет при создании. –

+2

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

+2

Это не касается его опасений по поводу вызова шагов из последовательности и способа упрощения создания производных классов. – eschneider

1

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

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

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

+0

+1 для статического метода Create. –

+0

В чем смысл частной виртуальной функции - ее нельзя переопределить. –

+0

@John: Извините. C# разрешает виртуальные вызовы от конструктора, и их можно использовать, если вы тщательно документируете их ожидаемую функцию и предварительные и последующие условия. Я впервые сделал C++, потому что он не разрешает виртуальные вызовы от конструктора. –

1

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

+0

Интересная идея, но, к сожалению, не применима в моем случае. Спасибо, в любом случае! – WCWedin

1

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

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

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

У вас и ValdV были похожие предложения; произвольно, я ответил на его: http://stackoverflow.com/questions/1155305/how-to-avoid-calling-viritual-methods-from-a-base-constructor/1155502#1155502. – WCWedin

10

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

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

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, чтобы справиться с этой ответственности ...

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

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

+0

Его вопрос заключается в четком способе реализации абстрактного класса, а не в создании производных версий. – eschneider

+2

Я не согласен. Он ищет шаблон, в котором производные классы будут выполнять структурированную инициализацию, и он обеспокоен тем, что эти производные могут не выполнять все необходимые шаги инициализации в соответствующем порядке. Эта проблема может помочь фабрике, которая выполняет как конструкцию, так и инициализацию. В частности, поскольку альтернатива вызова виртуальных методов в конструкторе (вообще) является нежелательной практикой. – LBushkin

+0

Л.Бушкин прав насчет моих потребностей. Я не уверен, что ваше предложение - это буквальная реализация абстрактной фабрики, но мне все равно, что эти дженерики берут на себя ответственность за реализацию, поскольку нет необходимости в классе DerivedFactory. Просить писателя Derived сделать защитник конструктора довольно разумным. Моя главная забота - это затраты на простоту вызывающего кода. Более того, для согласованности API я, вероятно, хотел бы переместить все объекты на фабрики. Это может быть полезно в целом, но это то, что я должен буду рассмотреть. – WCWedin

1

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

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

Мне нравится идея нажатия отдельных объектов в метод инициализации; он удаляет потенциальные дыры, созданные методами Step1() и Step3(). Это накладывает немного больше нагрузки на производный класс, но это делает его таким, что есть только один способ использования базового класса. Дорога длиннее, но прямая. Я думаю, я предпочел бы, чтобы Инициализация была защищена для моих нужд, но принцип тот же. – WCWedin

1

Чтобы убедиться, что инициализация выполнена в правильном порядке, вы можете использовать следующую информацию. Предположительно, у вас есть другие методы (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"); 
    } 
} 
+0

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

0

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

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

Смежные вопросы