На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: Qraizer, Hsilgos
Страницы: (2) [1] 2  все  ( Перейти к последнему сообщению )  
> двухшаговая инициализация
    Добрый день.
    Постоянно сталкиваюсь со следующей проблемой: есть фабричный метод для создания объектов, в зависимости от некоторых условий, также есть некий набор входных данных, который нужно передать во вновь созданный объект.
    Ниже псевдокод, описывающий текущее решение
    ExpandedWrap disabled
      class IObject
      {
      public:
          virtual ~IObject() {}
       
          virtual void initialize( const ObjectData &objectData ) = 0;
      };
       
      class AbstractObject : public IObject
      {
      public:
          virtual void initialize( const ObjectData& objectData ) override
          {
              m_objectData = &objectData;
          }
       
      private:
          ObjectData *m_objectData;
      };
       
      class ObjectA : public AbstractObject {};
      class ObjectB : public AbstractObject {};
       
      IObject* create( int type )
      {
          if( type == 1 ) return new ObjectA;
          return newObjectB;
      }


    соответственно используется примерно так:
    ExpandedWrap disabled
      IObject *obj = create( type );
      obj->initialize( objectData );

    в целом, данный подход устраивает, если бы не одно НО: так как ObjectData передается не в конструктор, то внутри AbstractObject мне приходится держать не константную ссылку, как хотелось бы, а простой указатель. С одной стороны я знаю, что данный указатель получен из ссылки, и, таким образом, не может быть nullptr. Однако при разрастании кода, об этом можно забыть и все-таки влепить проверки из серии if( m_objectData ).

    Альтернативный подход, который я вижу - это передача ObjectData в конструкторе:
    ExpandedWrap disabled
      class IObject
      {
      public:
          virtual ~IObject() {}
      };
       
      class AbstractObject : public IObject
      {
      public:
          AbstractObject( const ObjectData &objectData ) {}
      };
       
      class ObjectA : public AbstractObject
      {
      public:
          ObjectA( const ObjectData &objectData ) : AbstractObject( objectData ) {}
      };
       
      class ObjectB : public AbstractObject
      {
      public:
          ObjectB( const ObjectData &objectData ) : AbstractObject( objectData ) {}
      };
       
      IObject* create( int type, const ObjectData &objectData )
      {
          if( type == 1 ) return new ObjectA( objectData );
       
          return new ObjectB( objectData );
      }


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

    Т.е. получается, что для решения проблемы варианта 1 ( хранение указателя, вместо ссылки ) отлично подходит варинт 2, но с другой стороны он привносит необходимость прокидывания ObjectData через всю иерархию наследования, чего как раз нет в варианте 1.

    Возможен ли "третий" вариант, в котором нет вышеперечисленных минусов?
      Вообще-то в понятии фабричного метода, как и понятии фабрики, обычно подразумевается, что он занимается инициализацией создаваемого экземпляра. Пока объект не создан, его нет, на его месте куча мусора. Не даром же у нас этим занимаются конструкторы – специальные методы класса, от которых зависит наполнение кучи мусора на месте атрибутов объекта его полноценными инвариантами. Так что создание подразумевает инициализацию в том числе. Выходит, что первый вариант априори несостоятелен, если рассматривать его с позиции корректности реализации парадигмы фабричного метода.
      Другое дело, что конструкторы не зря умеют перегружаться, т.к. архитектура класса может предусматривать для инициализации, вообще говоря, несколько разных способов задавать эти инварианты. Но это и привносит неудобство в том смысле, что фабрикам приходится как-то с этой неоднозначностью разбираться. ИМХО наиболее простой, хоть и далеко не изящный, вариант – это предусмотреть в фабричном методе все те действия, которые иначе делали бы перегруженные конструкторы, и имитировать перегрузку конструкторов перегрузкой фабричного метода и/или простыми if()/switch()-ами.
      Более изящный, но и посложнее, вариант. Методы классов, которые фабричному методу, возможно, понадобятся для инициализации, сделать приватными, и пусть он занимается инвариантами посредством них (вместо того, чтобы явно стучаться к полям объекта), и при этом сам фабричный метод должен быть другом создаваемых им классов. Идеально, если интерфейс инициализации может быть определён в базовом классе и для всех производных останется неизменным. Это позволит объявить фабричный метод другом только одному, базовому, классу и не заморачиваться приватными методами инициализации в производных.
        Twilight, посмотри такой вариант, может понравится:

        ExpandedWrap disabled
          #include <iostream>
           
          struct BaseData {
            virtual std::string getValue() = 0;
          };
           
          struct ObjectData1: public BaseData {
            std::string str;
            ObjectData1(std::string iStr):str(iStr){}
            std::string getValue() override {
              return str;
            };
          };
           
          struct ObjectData2: public BaseData {
            int Int;  
            ObjectData2(int iInt):Int(iInt){}
            std::string getValue() override {
              return std::to_string(Int);
            };
          };
           
          template <typename T>
          class Factory {
            public:
              static T* generate(BaseData *objectData) {
                T* Tmp = new T();
                Tmp->init(objectData);
                return Tmp;
              }
          };
           
          class Initializer: public Factory<Initializer> {
               BaseData *objectData;
            public:
              void init(BaseData *iObjectData) {
                objectData = iObjectData;
                std::cout << objectData->getValue() << ": " << typeid(*objectData).name() << "\n";
              }
          };
           
          int main() {
            auto *Data1 = new ObjectData1("String");  
            auto *Data2 = new ObjectData2(10);
            auto *I1 = Factory<Initializer>::generate(Data1);
            auto *I2 = Factory<Initializer>::generate(Data2);
            return 0;
          }
          Qraizer я понимаю, что в данном случае это не совсем "эталонный" фабричный метод, но его всегда можно переписать следующим образом
          ExpandedWrap disabled
            IObject* create( int type, const ObjectData &objectData )
            {
                IObjectData *result = nullptr;
             
                if( type == 1 ) result = new Object1;
                else result = new Object2;
             
                result->initialize( objectData );
             
                return result;
            }


          В реальном коде вызовов initialize вынесен из метода create() в силу архитектуры ПО и контракта с прикладной частью.

          Что касается перегрузки конструкторов или создание с помощью дружественных методов: в моем случае ObjectData это один тип, с помощью которого инициализируются все типы объектов. В 99% случаев там нет данных, которые относились бы к Object1 и не относились бы к Object2. Получается, что разделение ObjectData на части CommonObjectData и Object1Data, Object2Data будет несколько искусственным. В дополнение, в один момент времени существуют только один экземпляр IObject ( по-факту - это работа с подключенным железом )


          JoeUser если честно я не совсем понял, твое предложение. Оно выглядит как усложненный вариант 1, но не решающее его проблему: хранение в классе указателя на BaseData
            В таком случае, Twilight, коли, в связи с особенностями архитектуры приложения, у тебя не полностью соблюдаются принципы надёжного кода, и ты перекладываешь ответственность за безгрешность с компилятора на себя, то на вот это:
            Цитата Twilight @
            Однако при разрастании кода, об этом можно забыть и все-таки влепить проверки из серии if( m_objectData ).
            можно ответить, что ты можешь в суматохе написать и
            ExpandedWrap disabled
              obj->initialize( *(ObjectData*)nullptr );
            так что проверки ничуть не лишни.
              Да, из-за особенности архитектуры я не могу вызывать initialize в точке создания объекта, однако я могу создавать объект в точке его инициализации.
              Но вопрос был совсем не про это.

              Если единственный вариант решения проблемы с хранением указателя при использовании метода initialize( ObjectData ) - это передача ObjectData через всю иерархию наследования ( что в моем случае выглядит не совсем красиво, так как в половине случаев, данный параметр просто передается ниже по цепочке наследования ), то вопрос я снимаю.

              Я все-таки надеялся, что к существующим решениям
              1 - мало однотипного кода и однотипных описаний классов, но при этом сам следи за указателем
              2 - много однотипного кода, но никаких указателей
              можно добавить
              3 - мало однотипного кода и нет указателей
                Цитата Twilight @
                ... Однако при разрастании кода, об этом можно забыть и все-таки влепить проверки из серии if( m_objectData ).

                Это в данном случае будет бесполезно.
                Поскольку в классе "AbstractObject" указатель не обнуляется.
                ---
                По моему, ты не о том беспокоишься.
                Вот что получится, если в связи с ростом объёма
                будет забыта 2-ая строчка ? Вот так:
                ExpandedWrap disabled
                      IObject *obj = create( type );
                  //    obj->initialize( objectData );

                При этом не инициализированный указатель может иметь любое значение.
                ---
                Также, не удачно выглядит возврат void функцией initialize.
                Даже если на первый взгляд функция примитивна.
                Но естественно предположить, что однажды, перед присваиванием указателя
                захочется проведить правильность данных objectData.
                И тогда функция станет не столь примитивна. А указатель может остаться
                равным NULL (несмотря на то, что указатель должен быть получен из ссылки) - бессмысленно его использовать,
                если данные дефектны. Значение, равное NULL, может служить индикатором "что-то пошло не так".
                Сообщение отредактировано: ЫукпШ -
                  Цитата Twilight @
                  Если единственный вариант решения проблемы с хранением указателя при использовании метода initialize( ObjectData ) - это передача ObjectData через всю иерархию наследования ( что в моем случае выглядит не совсем красиво, так как в половине случаев, данный параметр просто передается ниже по цепочке наследования ), то вопрос я снимаю.
                  Я в свою очередь не понял источника проблемы. Почему нельзя создать прямо в нужной точке вместо инициализации, а нужно именно создать заранее и только потом инициализировать. Всё равно ж между ними объекта ещё нет, только кучка байтиков. И даже если надо именно так, то почему нельзя поместить это в саму фабрику, чтобы снаружи всё равно было одно действие, а не два. В конце концов когда бы делаем new SomeClass(bla_bla_bla), компилятор выполняет два действия, зато атомарно: распределение памяти и вызов конструктора; и то же с delete. И всё ради того, чтоб человек случайно не ошибался, и снаружи делал одно действие.
                  Ну да ладно, нельзя так нельзя. Но вот теперь я уже не понимаю, зачем передавать параметры инициализации вниз по цепочке иерархии, если можно просто ограничиться одним невиртуальным методом в базовом классе? Зовёшь, тот отрабатывает, и вуаля.

                  Добавлено
                  И собственно.
                  Цитата Twilight @
                  Однако при разрастании кода, об этом можно забыть и все-таки влепить проверки из серии if( m_objectData ).
                  Если тебя заботит такая мелочь, то и другие тоже должны бы, причём более не мелочи, но ты согласен с ними мириться. Почему бы и с этой мелочью не смириться?

                  Добавлено
                  В общем, я не понимаю истоков проблемы. Откуда такие странные ограничения на создание, почему не подходят общие паттерны проектирования итп. Потому посоветовать что-то дельное вряд ли выйдет. А вот гадание по кофейной гуще запросто, но как-то это не серьёзно.
                    Цитата ЫукпШ @
                    Это в данном случае будет бесполезно.
                    Поскольку в классе "AbstractObject" указатель не обнуляется.

                    Как я писал в первом посте - это не совсем реальный код, а некий псевдокод, который писался прямо здесь "от руки"


                    Цитата Qraizer @
                    В общем, я не понимаю истоков проблемы. Почему нельзя создать прямо в нужной точке вместо инициализации

                    Потому-что там где ты смотришь их нет.
                    Давай остановимся на варианте - что все можно, потому-что вопрос совсем не про это.

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

                    Вопрос - можно ли избежать данных проблем?
                      Зачем тут вообще указатель или ссылка на ObjectData? Если хочешь избежать копирования, и ты можешь менять ObjectData, то напиши просто move конструктор для него и перемещай в класс.
                        Twilight, ты не понимаешь. Зачастую, гораздо чаще, чем ожидают спрашивающие, задают не тот вопрос: описывают не саму проблему, а следствия, и просят совета, как эти следствия побороть. Тогда как, описав саму проблему, получают хороший совет очень быстро. Обычно проблем, подобных твоей, не возникает, если воспользоваться описанными выше решениями. Они ведь не просто так были озвучены, они дают понять, что твой код столкнулся с проблемами, потому что спроектирован не вполне корректно. Но т.к. у тебя особая ситуация, и эти решения тебе по каким-то, неважно каким, объективным причинам не подходят, для адекватного ответа нам нужно знать больше конкретики. Мне, чтобы посоветовать по существу, нужно понимать что можно, что нельзя, а что и просто необходимо, а я, как выше писал, не могу для себя эти критерии сформулировать по той информации, что есть.
                        Ну попробую ещё раз ткнуть пальцем наобум. Если ты сделаешь базовый класс виртуальным, ты сможешь – и даже должен будешь – указывать его конструирование точно по месту создания объектов. И неважно, какое место в иерархии они занимают, конструирование виртуальных базовых классов указывается всегда в конкретном производном конструкторе и не передаются по цепочке. На, вот, пример:
                        ExpandedWrap disabled
                          #include <iostream>
                           
                          struct A
                          {
                            explicit A(int i=0)               { std::cout << "A::A(" << i << ')' << std::endl; }
                          };
                           
                          struct B: virtual A
                          {
                            explicit B(int i): A(i+1)         { std::cout << "B::B(" << i << ')' << std::endl; }
                          };
                           
                          struct C: virtual B
                          {
                            explicit C(int i): B(i+1)         { std::cout << "C::C(" << i << ')' << std::endl; }
                          };
                           
                          struct D: C
                          {
                            explicit D(int i): C(i+1), B(2*i) { std::cout << "D::D(" << i << ')' << std::endl; }
                          };
                           
                          int main()
                          {
                            D d(10);
                          }
                        ExpandedWrap disabled
                          A::A(0)
                          B::B(20)
                          C::C(11)
                          D::D(10)
                        Но что-то мне подсказывает, это не то, что тебе нужно.

                        Добавлено
                        Если совсем откровенно, я даже не пойму самого вопроса. С одной стороны ты не можешь инициализировать объект одновременно с его созданием, но с другой почему-то можешь, но тебе это неудобно. :huh: Так можешь или таки не можешь? Может быть "хочешь или не хочешь" спросить было правильнее? Следи за мысля́ми. Если можешь, то фабрика с инициализатором внутри вполне отличное решение. Если не можешь, то конструкторы по-любому не помогут никак. А если помогут, и дело лишь в дублировании кода передачи параметра, то это мелочь по сравнению с проверкой поинтера на валидность. Короче, я запутанный.
                          Qraizer, чел (ИМХО) просто загоняется! Все же просто как грабли:

                          1) Хочешь "виртуальную" (отложенную) инициализацию - используй фабрики
                          2) Хочешь изменение инициализации в рантайме - используй наследование и виртуальные функции
                          3) Хочешь изменение инициализации в компиле-тайме - используй CRTP

                          А вот использование ИСКЛЮЧИТЕЛЬНО ссылок щястья не гарантирует ... т.к. такая ссылка может алиасить "нулевой" или "мусорный" указатель. Ну или, как говорится, работай без хипа и обрети бессмертие! :lol:
                            Не обязательно, JoeUser. Ситуации всякие бывают, иногда действительно хочется странного, и не по капризу. C++ много что не запрещает или наоборот требует абсолютно, практически любой аспект можно обойти, было бы желание. Но чем опаснее хотелка, тем сложнее это сделать случайно, т.к. необходимые конструкции нужно писать руками и намеренно.
                            Мне вот как-то надо было, к примеру, изменить порядок вызова конструкторов в классе. Если б я не сообразил, как, то пришлось бы идти на форум, и если б я спросил "как мне изменить порядок инициализации подобъектов", был бы наверняка послан к индусам. Так что я бы спрашивал не это, я бы описал постановку задачи, из которой путём рассуждений следовала бы необходимость смены порядка, и уже как итог спросил бы совета. Нет, я не пришёл, потому что сообразил, как, и сделал. Цель была достигнута. Но не исключено, что спроси я на форуме совета, мне бы предложили куда лучшее решение, не требующее странного, которое однако ускользнуло от моего замыленного взгляда.

                            Добавлено
                            Twilight, считай это развёрнутым объяснением, почему твоя тема кажется... э-э-э, странной.
                            Сообщение отредактировано: Qraizer -
                              Цитата Qraizer @
                              Не обязательно, JoeUser.

                              Согласен - не вопрос! А вопрос в другом, в "деревенской мудрости" (типа) - "семь раз отмерь, один раз отрежь". Это я про предварительный анализ и проектирование архитектуры приложения/либы. Да, порой кажется, вот он САМ Я вступил в неравную борьбу с энтропией, с которой еще никто не сталкивался ... А потом на утро, после жирной яичницы с беконом и стаканом рассола приходит понимание ... тут 98% классических решений вписывается, нужно только 2% предусловий определить и оформить правильно.
                                Цитата Twilight @
                                .. я знаю, что данный указатель получен из ссылки, и, таким образом, не может быть nullptr. Однако при разрастании кода, об этом можно забыть и все-таки влепить проверки из серии if( m_objectData ).

                                Оберни указатель m_objectData классом, сделай свой "smart pointer".
                                В процедуре замены возвращаемого значения типа "bool" вставь
                                диагностику. Или генерируй исключение, по вкусу.
                                Тогда любая попытка сделать "if( m_objectData )" приведёт к сообщению об ошибке.
                                Сообщение отредактировано: ЫукпШ -
                                0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
                                0 пользователей:


                                Рейтинг@Mail.ru
                                [ Script execution time: 0,0518 ]   [ 17 queries used ]   [ Generated: 28.03.24, 22:17 GMT ]