"Век живи - век учись" как говорится. Я это понимаю как "век живи - век читай". Читай книги. Хорошие и не очень. Только хорошие - медленно и внимательно, а плохие - наоборот. Но и их читать нужно (иначе как понять, что они плохие-то, не прочитав их?)
К чему я все это пишу? Просто сегодня лишний раз в этом убедился, перечитывая товарища Мартина Фаулера, его книгу "Рефакторинг". Как раз читая раздел "подъем конструктора (pull up constructor body)" нашел решение одной из самых давних и изрядно уже наболевших своих проблем в архитектуре текущего проекта.
Вкратце проблема такова:
Есть библиотека сложных графических фигур. На первый взгдяд, библиотека написана неплохо: несколько иерархий фигур наследуются от абстрактных классов, которые в свою очередь наследуются от стандартных базовых примитивов, применяется переопределение стилей, сложное переопределение поведения и т.п. Абстрактная фабрика создает по требованию ту или иную фигуру, причем при создании сложной фигуры она сама еще раз использует фабрику для наполнения себя более простыми фигурами.
Но есть и проблемы. Методы инициализации в частности. Нужно при создании любой фигуры произвести над ней несколько обязательных действий, до того, как он попадет в руки клиенту. Но до того, как будет вызван конструктор конкретного класса это сделать нельзя. Как же обязать любой производственный класс при своем создании производить над собой эти действия? Как гарантировать, что любой производный класс "не забудет" этого сделать?
Было бы хорошо объявить в "самом базовом" классе :) какой-нибудь абстрактный метод, который каждый из производных классов будет переопределять, вызывая выполнение обязательных действий (иначе компилятор просто откажется компилировать такой класс). А сам этот метод вызывать в базовом классе (т.е. использовать один из самых любимых моих GoF паттернов- "шаблонный метод")
Только где этот метод вызывать-то? Я не буду приводить здесь мотивировку, приведенную Фаулером в описании паттерна "подъем конструктора" дабы не провоцировать религиозные споры... Скажу только что "шаблонный метод" в его чистом виде для конструкторов неприемлем. (Я надеюсь, что читатель в состоянии сам знакомиться как с GoF паттернами, так и с паттернами товарища Фаулера).
Одним словом, в конструкторе базового класса то, что мне нужно, делать нельзя (этот конструктор вызовется раньше конструктора конкретного типа, т.е. не будет еще данных для работы этого метода). А надеяться на вызов метода после конструктора нельзя - где гарантия, что клиент, который будет создавать эту фигуру, не забудет этот метод вызвать?
Да и зачем это клиенту, сами подумайте? Он подключил мою библиотеку, увидел открытый конструктор нужной ему фигуры, вызвал этот конструктор. И он вправе получить то, что хотел. Ну а то, что мы еще что-то хотели от него... ну - это ведь не его проблемы, правда?
Мы или даем ему фигуру, или не даем. А "даем, но не фигуру", "даем, но не до конца фигуру" и т.п. Это, сами понимаете...
Ну вот. Какой-никакой, более - менее приемлемый, выход был найден таким: в фабрике, создающей фигуры, после того, как конструктор конкретного класса отработал, и вызывать этот самый метод. Это - и просто, и надежно, и клиента не напрягает.
Взаимодействие выглядело примерно таким: клиент создает ссылку на нужную ему фигуру и просит фабрику инициализировать эту ссылку (попросту создать такую фигуру). Фабрика вызывает конструктор конкретного класса фигуры (сначала отрабатывает вся цепочка конструкторов базовых классов, естественно). А перед тем, как отдать фигуру клиенту, фабрика вызывала метод Initialize(), абстрактный метод одного из базовых классов фигур, переопределенный каждой из конкретных фигур.
псевдо код ниже
говоря, фигура не в состоянии сама себя создать и правильно инициализировать, без посторонней помощи). Второе - нужно не забыть теперь вызвать этот метод в фабрике (хотя это проще, чем вызов метода на клиенте: фабрика одна, мест, где этот метод нужно вызывать - мало (на каждую иерархию фигур, на каждый фабричный метод - одно место)... клиентов же - может быть много и "разных", сами понимаете).
Эти проблемы станут более ощутимы, если клиент сам захочет создать свою фабрику фигур (ведь классы фигур - открытые, использование разработанной фабрики - требование опционное, не обязательное). Вот забудет клиент прописать в своей фабрике вызов нашего метода, и получит вместо фигуры - много проблем (разной степени тяжести, как говорится (грустная шутка).
Выход - применение "подъема конструктора" Фаулера (а, точнее, гибрида "подъема метода" применительно к конструктору). Чтобы не постить еще одну простынь кода, просто скажу - в каждый из конструкторов конкретных классов нужно прописать вызов нашего метода инициализации. Примерно так
Зато вызовы этого метода удаляются из кода методов фабрики, да и сам метод закрывается (становится не public, и даже не internal, а просто protected). И теперь никто, кроме того, кто данный конкретный класс создал, не может повлиять на инициализацию его объектов.
Кроме того, если иерархия абстрактных классов (а не конкретных фигур) достаточно развита - можно перенести вызов метода Initialize() (или хотя бы какую-то часть его функциональности) выше по иерархии, все арвно таким образом сделав инициализацию более надежной.
В любом случае подход стоящий (как по мне, конечно :) )
К чему я все это пишу? Просто сегодня лишний раз в этом убедился, перечитывая товарища Мартина Фаулера, его книгу "Рефакторинг". Как раз читая раздел "подъем конструктора (pull up constructor body)" нашел решение одной из самых давних и изрядно уже наболевших своих проблем в архитектуре текущего проекта.
Вкратце проблема такова:
Есть библиотека сложных графических фигур. На первый взгдяд, библиотека написана неплохо: несколько иерархий фигур наследуются от абстрактных классов, которые в свою очередь наследуются от стандартных базовых примитивов, применяется переопределение стилей, сложное переопределение поведения и т.п. Абстрактная фабрика создает по требованию ту или иную фигуру, причем при создании сложной фигуры она сама еще раз использует фабрику для наполнения себя более простыми фигурами.
Но есть и проблемы. Методы инициализации в частности. Нужно при создании любой фигуры произвести над ней несколько обязательных действий, до того, как он попадет в руки клиенту. Но до того, как будет вызван конструктор конкретного класса это сделать нельзя. Как же обязать любой производственный класс при своем создании производить над собой эти действия? Как гарантировать, что любой производный класс "не забудет" этого сделать?
Было бы хорошо объявить в "самом базовом" классе :) какой-нибудь абстрактный метод, который каждый из производных классов будет переопределять, вызывая выполнение обязательных действий (иначе компилятор просто откажется компилировать такой класс). А сам этот метод вызывать в базовом классе (т.е. использовать один из самых любимых моих GoF паттернов- "шаблонный метод")
Только где этот метод вызывать-то? Я не буду приводить здесь мотивировку, приведенную Фаулером в описании паттерна "подъем конструктора" дабы не провоцировать религиозные споры... Скажу только что "шаблонный метод" в его чистом виде для конструкторов неприемлем. (Я надеюсь, что читатель в состоянии сам знакомиться как с GoF паттернами, так и с паттернами товарища Фаулера).
Одним словом, в конструкторе базового класса то, что мне нужно, делать нельзя (этот конструктор вызовется раньше конструктора конкретного типа, т.е. не будет еще данных для работы этого метода). А надеяться на вызов метода после конструктора нельзя - где гарантия, что клиент, который будет создавать эту фигуру, не забудет этот метод вызвать?
Да и зачем это клиенту, сами подумайте? Он подключил мою библиотеку, увидел открытый конструктор нужной ему фигуры, вызвал этот конструктор. И он вправе получить то, что хотел. Ну а то, что мы еще что-то хотели от него... ну - это ведь не его проблемы, правда?
Мы или даем ему фигуру, или не даем. А "даем, но не фигуру", "даем, но не до конца фигуру" и т.п. Это, сами понимаете...
Ну вот. Какой-никакой, более - менее приемлемый, выход был найден таким: в фабрике, создающей фигуры, после того, как конструктор конкретного класса отработал, и вызывать этот самый метод. Это - и просто, и надежно, и клиента не напрягает.
Взаимодействие выглядело примерно таким: клиент создает ссылку на нужную ему фигуру и просит фабрику инициализировать эту ссылку (попросту создать такую фигуру). Фабрика вызывает конструктор конкретного класса фигуры (сначала отрабатывает вся цепочка конструкторов базовых классов, естественно). А перед тем, как отдать фигуру клиенту, фабрика вызывала метод Initialize(), абстрактный метод одного из базовых классов фигур, переопределенный каждой из конкретных фигур.
псевдо код ниже
//библиотека
public static class ShapeFactory
{
public static BaseShape CreateShapeFirst(int i)
{
BaseShape result = null;
//не будем "отягощять интелектом" псевдо-фабрику :)
switch (i)
{
case 0: result = new ConcreteComplexShapeFirst(); break;
//....
}
if (result != null)
{
result.Initialize();
}
return result;
}
public static BaseShape CreateShapeSecond(int i)
{
//здесь код, аналогияный аналогичный коду CreateShapeFirst
}
}
public abstract class BaseShape : System.Windows.Shapes.Shape
{
public abstract void Initialize();
}
public abstract class BaseComplexShapeFirst : BaseShape
{
public override void Initialize()
{
//make something special for ComplexShapeFirst
}
}
public sealed class ConcreteComplexShapeFirst : BaseComplexShapeFirst
{
public override void Initialize()
{
base.Initialize();
//make something special for ConcreteComplexShapeFirst
}
}
public abstract class BaseComplexShapeSecond : BaseShape
{
public override void Initialize()
{
//make something special for BaseComplexShapeSecond
}
}
public sealed class ConcreteComplexShapeSecond : BaseComplexShapeSecond
{
public override void Initialize()
{
base.Initialize();
//make something special for ConcreteComplexShapeSecond
}
}
//клиент BaseShape shapeFirst = ShapeFactory.CreateShapeFirst(0); BaseShape anotherShapeFirst = ShapeFactory.CreateShapeFirst(1); BaseShape shapeSecond = ShapeFactory.CreateShapeFirst(0); BaseShape anothershapeSecond = ShapeFactory.CreateShapeFirst(1);Проблемы с таким подходом было две: метод Initialize() имеет отношение только для фигур, а использоуется достаточно для них посторонним классом - фабрикой. Соответственно здесь налицо - нарушение инкапсуляции инициализации фигур (попросту
говоря, фигура не в состоянии сама себя создать и правильно инициализировать, без посторонней помощи). Второе - нужно не забыть теперь вызвать этот метод в фабрике (хотя это проще, чем вызов метода на клиенте: фабрика одна, мест, где этот метод нужно вызывать - мало (на каждую иерархию фигур, на каждый фабричный метод - одно место)... клиентов же - может быть много и "разных", сами понимаете).
Эти проблемы станут более ощутимы, если клиент сам захочет создать свою фабрику фигур (ведь классы фигур - открытые, использование разработанной фабрики - требование опционное, не обязательное). Вот забудет клиент прописать в своей фабрике вызов нашего метода, и получит вместо фигуры - много проблем (разной степени тяжести, как говорится (грустная шутка).
public static class ClientShapeFactory
{
public static BaseShape CreateShapeFirst(int i)
{
switch (i)
{
case 0: return new ConcreteComplexShapeFirst();
//....
}
throw new ApplicationException("Something is wrong with this library!");
}
public static BaseShape CreateShapeSecond(int i)
{
switch (i)
{
case 0: return new ConcreteComplexShapeSecond();
//....
}
throw new ApplicationException("Something is wrong with this library!");
}
}
}
Выход - применение "подъема конструктора" Фаулера (а, точнее, гибрида "подъема метода" применительно к конструктору). Чтобы не постить еще одну простынь кода, просто скажу - в каждый из конструкторов конкретных классов нужно прописать вызов нашего метода инициализации. Примерно так
public sealed class ConcreteComplexShapeSecond : BaseComplexShapeSecond
{
public ConcreteComplexShapeSecond()
{
Initialize();
}
public override void Initialize()
{
base.Initialize();
//make something special for ConcreteComplexShapeSecond
}
}
Вы скажете "теперь нужно не забыть при создании каждого конкретного класса вызвать этот метод"? Соглашусь :)Зато вызовы этого метода удаляются из кода методов фабрики, да и сам метод закрывается (становится не public, и даже не internal, а просто protected). И теперь никто, кроме того, кто данный конкретный класс создал, не может повлиять на инициализацию его объектов.
Кроме того, если иерархия абстрактных классов (а не конкретных фигур) достаточно развита - можно перенести вызов метода Initialize() (или хотя бы какую-то часть его функциональности) выше по иерархии, все арвно таким образом сделав инициализацию более надежной.
В любом случае подход стоящий (как по мне, конечно :) )