Составной шаблон с вариациями классов

У меня есть ситуация, когда классы A, B и C являются производными от X для создания составного шаблона.

А, В и С очень похожи, но у них разные внутренние атрибуты. Мой вопрос заключается в том, как мне получить доступ к их атрибутам и изменить их после того, как они будут добавлены в композит без приведения их к типу?

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

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

Для наглядности предположим, что у нас есть композит merchandise (базовый класс композита), у которого есть цена. Теперь магазин — это товар, в нем отдел — это товар, в котором фактический товар называется potэто товар, в нем может быть набор горшков с комбинированными горшками, который также является товаром, и так далее.

class Merchandise
{
public:
    virtual void add(Merchandise* item) = 0;
    virtual Merchandise* getMerchandise() = 0;
    virtual void show() = 0;

    // assume we have the following key operations here but I am not implementing them to keep this eitemample short
    //virtual setPrice(float price) = 0;
    //virtual float getPrice() = 0;
};


class Store : public Merchandise
{
    vector< Merchandise*> deparments;
    std::string storeName = "";

public:
    Store(std::string store_name) : storeName(store_name) {}

    virtual void add(Merchandise* item)
    {
        deparments.push_back(item);
    }

    virtual Merchandise* getMerchandise()
    {
        if (deparments.size() > 0)
            return deparments[0];

        return 0;
    }

    virtual void show()
    {
        cout << "I am Store " << storeName << " and have following " << deparments.size() << " departments" << endl;

        for (unsigned int i = 0; i < deparments.size(); i++)
        {
            deparments[i]->show();
        }
    }
};

class Department : public Merchandise
{
    std::string depName;
    vector<Merchandise*> items;
public:
    Department(std::string dep_name) : depName(dep_name) {}
    virtual void add(Merchandise* item)
    {
        items.push_back(item);
    }

    virtual Merchandise* getMerchandise()
    {
        if (items.size() > 0)
            return items[0];

        return 0;
    }

    virtual void show()
    {
        cout << "I am department " << depName << " and have following " << items.size() << " items" << endl;

        for (unsigned int i = 0; i < items.size(); i++)
        {
            items[i]->show();
        }
    }
};

class Shirt : public Merchandise
{
    std::string shirtName;
public:
    Shirt(std::string shirt_name) : shirtName(shirt_name) {}
    virtual void add(Merchandise* item) {}
    virtual Merchandise* getMerchandise() { return 0; }
    virtual void show()
    {
        cout << "I am shirt " << shirtName << endl;
    };

};

class Pot : public Merchandise
{
    std::string potName;
public:
    Pot(std::string pot_name) : potName(pot_name) {}

    virtual void add(Merchandise* item) {  }
    virtual Merchandise* getMerchandise() { return 0; }
    virtual void show()
    {
        cout << "I am pot " << potName << endl;
    };

    int num = 0;
};

class CookSet : public Merchandise
{
    std::string cooksetName;
    vector<Merchandise*> pots;
public:
    CookSet(std::string cookset_name) : cooksetName(cookset_name) {}
    vector<Merchandise*> listOfPots;
    virtual void add(Merchandise* item) { pots.push_back(item); }
    virtual Merchandise* getMerchandise() { return 0; }
    virtual void show()
    {
        cout << "I am cookset " << cooksetName << " and have following " << pots.size() << " items" << endl;

        for (unsigned int i = 0; i < pots.size(); i++)
        {
            pots[i]->show();
        }
    };

    int num = 0;
};

int main()
{
    // create a store
    Store * store = new Store( "BigMart");

    // create home department and its items
    Department * mens = new Department( "Mens");

    mens->add(new Shirt("Columbia") );
    mens->add(new Shirt("Wrangler") );

    // likewise a new composite can be dress class which is made of a shirt and pants.

    Department * kitchen = new Department("Kitchen");

    // create kitchen department and its items
    kitchen->add(new Pot("Avalon"));

    CookSet * cookset = new CookSet("Faberware");
    cookset->add(new Pot("Small Pot"));
    cookset->add(new Pot("Big pot"));

    kitchen->add(cookset);

    store->add( mens );
    store->add(kitchen);

    store->show();

    // so far so good but the real fun begins after this.

    // Firt question is, how do we even access the deep down composite objects in the tree?

    // this wil not really make sense!
    Merchandise* item = store->getMerchandise()->getMerchandise(); 

    // Which leads me to want to add specific method to CStore object like the following to retrieve each department 
    // but then does this break composite pattern? If not, how can I accomodate these methods only to CStore class?

    //store->getMensDept();
    //store->getsKitchenDept();

    // Likewise a shirt class will store different attributes of a shirt like collar size, arm length etc, color.
    // how to retrieve that?

    // Other operations is, say if item is cookset, set it on 20% sale.

    // Another if its a shirt and color is orange, set it on 25% sale (a shirt has a color property but pot doesn't).

    // how to even dispaly particular attributes of that item in a structure?
    // item->getAttributes();


    return 0;
}

Проблема с этой строкой, как только я заполнил композит.

Merchandise* item = store->getMerchandise()->getMerchandise();

Во-первых, из моей структуры кода я знаю, что это должен быть определенный тип, но, как рекомендуется, мы не должны приводить это к типу!? Но я хочу изменить его уникальные свойства, так как мне этого добиться?

Предположим, что в этом магазине также продаются рубашки, и я хочу изменить его свойства (или даже просто показать их!), он сильно отличается от горшка.

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

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

Обновить

Обратите внимание, что я использовал пример merchandise для объяснения проблемы. В моем реальном примере все A, B, C являются производными от X. A содержит несколько элементов B, которые содержат несколько элементов C. Когда операция выполняется над A, ее необходимо выполнять над ее составными частями, поэтому я использую композит. Но тогда у каждого композита есть разные атрибуты. Композит не подходит для этого?


person zar    schedule 10.08.2018    source источник
comment
Все зависит от реального кода. Если вам нужно получить доступ к атрибутам конкретных классов, у вас возникнут проблемы с правильной абстракцией кода. В идеале вы никогда не должны полагаться на конкретный тип, но на общедоступный интерфейс. Зная, что вы можете изменить абстракции или найти способ обойти эту проблему   -  person bartop    schedule 17.08.2018
comment
Возможно, составной шаблон является проблемой в вашем дизайне. Делать рубашку товаром для меня не имеет смысла. Кроме того, составному шаблону присуща потеря информации о типе.   -  person OutOfBound    schedule 17.08.2018
comment
Позвольте мне добавить контекст, что я делаю. Это приложение для подсчета очков. В матче есть иннинги (счет/результат), у обеих сторон есть свои иннинги (есть очки), затем у каждого играющего игрока есть свои иннинги (счет с более подробной информацией). Когда матч продолжается, к матчу добавляется событие, которое добавляется к текущим иннингам и к иннингам текущего игрока. Таким образом, я строю счет таким образом, но в конце я хочу отобразить его, и каждый тип иннинга довольно разный, и настройка текущего состояния требует разных операций. Композит не подходит?   -  person zar    schedule 17.08.2018
comment
Пожалуйста, ответьте с примером товара, обратите внимание на приведенную выше информацию о праве собственности! Приведенная выше информация предназначена только для того, чтобы вы знали контекст.   -  person zar    schedule 17.08.2018
comment
Я думаю, что в этом вопросе отсутствуют некоторые детали того, как его предполагается использовать. Как вы заметили, store->getMerchandise()->getMerchandise(); здесь не имеет особого смысла, но в то же время очень неясно, как вы собираетесь использовать эту древовидную структуру Merchansdise. В интерфейсе нет открытого метода, кроме show, и вы также не приводите пример того, как вы собираетесь его использовать. Я думаю, что некоторые примеры использования от вас прояснят ситуацию и дадут вам более подходящие ответы.   -  person super    schedule 19.08.2018


Ответы (4)


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

class Shirt;
class Pot;

class visitor{
public:
//To do for each component types:
virtual void visit(Shirt&) =0;
virtual void visit(Pot&) =0;
};

class Merchandise
{
public:
    //...

    //Provides a default implementation for the acceptor that
    //that do nothing.
    virtual void accept(visitor& x){}

    // assume we have the following key operations here but I am not implementing them to keep this eitemample short
    //virtual setPrice(float price) = 0;
    //virtual float getPrice() = 0;
    //  => implementable with a visitor
};


class Store : public Merchandise
{
    //...

    void accept(visitor& v) override
    {

        for (unsigned int i = 0; i < deparments.size(); i++)
        {
            //forward the visitor to each component of the store.
            //do the same for departments
            deparments[i]->accept(v);
        }
    }
};

class Shirt : public Merchandise
{
//...
void accept(visitor& v) override{
     //now *this as the type Shirt&, pass this to the visitor.
     v.visit(*this);
     }

};

class Pot : public Merchandise
{
//...
void accept(visitor& v) override{
   v.visit(*this);
   }
};

class SpecialPromotion
  :public visitor{
     public:
     void visit(Shirt& s) override {
        //25% discount on orange shirts, 15% otherwise;
        if (s.color="orange")
          s.price*=0.75;
        else
          s.price*=0.85
        }
     void visit(Pot& p) override{
        //one pot offered for each pot pack
        ++p.num;
        }
     };

//example of usage:
void apply_special_promotion(Store& s){
   SpecialPromotion x;
   s.accept(x);
   }

 class EstimateStockMass
  :public visitor{
     public:
     double mass=0;
     void visit(Shirt& s) override {
        if (s.size="XL") mass+=0.5;
        else mass+=0.1;
        }
     void visit(Pot& p) override{
        mass += p.num * 1.2;
        }
     };
     //example of usage:
double get_stock_mass(Store& s){
   EstimateStockMass x;
   s.accept(x);
   return x.mass;
   }
person Oliv    schedule 17.08.2018
comment
Я улучшил это решение — вам нужен шаблон посетителя, потому что разные операции по-разному применяются к разным товарам в вашем каталоге товаров. Может быть, в целом вам нужно комбинированное решение? Существует определенная операция, с которой вы хотите работать как с деревом (скажем: getMerchandise()), но когда вы хотите обновить цвет, у нее есть особая методология. Вы можете переименовать «принять» в «обновить цену» и применить тот же шаблон. Посетители тоже довольно мило относятся к деревьям. - person Christian Bongiorno; 21.08.2018

Похоже, что вы хотите собрать RTTI (информацию о типе времени выполнения), поэтому dynamic_cast — это решение. Кроме того, если вы используете C++17, я рекомендую вам использовать std::variant (или boost::variant, если вы используете более раннюю версию C++, но используете boost.) Если вы не хотите чтобы использовать их, то, возможно, вы можете сделать свою add функцией шаблона и вернуть ссылку на базовый тип.

Кстати

  • в вашем основном есть куча динамических распределений, которые никогда не освобождаются. Используйте интеллектуальные указатели, если у вас есть C++ версии не ниже C++11.
  • ваши базовые классы не имеют виртуальных деструкторов, это вызовет огромные проблемы при уничтожении вашего магазина
  • Не используйте using namespace std
  • Если у вас C++11, используйте ключевое слово override, когда хотите переопределить виртуальную функцию.
  • Вы должны отметить show() const.
person Lesley Lai    schedule 14.08.2018
comment
Обратите внимание, что я использовал пример merchandise для объяснения проблемы. В моем реальном примере все A, B, C являются производными от X. A содержит несколько B, которые содержат несколько элементов C. Когда операция выполняется над A, ее необходимо выполнять над ее составными частями, поэтому я использую композит. Но затем каждый композит имеет разные атрибуты. Вы все еще думаете, что композит не подходит для этого? - person zar; 15.08.2018
comment
У вас есть рекурсивные композиции? Как A может содержать себя? В противном случае нет причин для дополнительных сложностей. В любом случае, если вы не хотите использовать dynamic_cast, то лучшим выбором будет вариантная структура, которую вы можете использовать. - person Lesley Lai; 16.08.2018
comment
Да, у меня есть рекурсивная композиция. Я пытаюсь использовать составной шаблон для создания и заполнения данных - эта часть работает хорошо, но теперь, когда я хочу получить эти данные, возникает проблема. На самом деле приведение типов будет работать нормально, но я изучал, есть ли лучший способ, поскольку приведение типов не является лучшей практикой. - person zar; 16.08.2018
comment
Поскольку ваша проблема в том, что я хочу RTTI, у вас не так много вариантов. Я должен сказать, что использование dynamic_cast само по себе не является практикой, которую осуждают, но проектирование вашего кода так, чтобы вы должны полагаться на RTTI. В любом случае variant - это чистое альтернативное решение, если вы не хотите приведения. - person Lesley Lai; 16.08.2018

Товар: товар, выставленный на продажу.

Теперь магазин - это товар,

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

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

в нем отдел - это товар,

Отделы внутри магазина редко продаются другим магазинам, поэтому я сильно сомневаюсь в этом утверждении. Опять же, у вас есть что-то, что содержит товары, а не что-то, состоящее из товаров.

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

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

Говоря о полке, это был бы еще один пример контейнера с товарами.


Похоже, вам нужны контейнеры, а не композиты. В магазине может быть vector отделов (или, возможно, set, если вы хотите найти их по названию). У отдела, в свою очередь, может быть контейнер с товарами внутри этого отдела. Или, возможно, в отделе будут проходы, в которых затем будут полки, а в них — товары.

Если вам нужен инвентарь всего магазина, есть несколько вариантов. Один из них заключается в том, чтобы магазин перебирал свои отделы, которые затем перебирали свои запасы. В идеале вы должны реализовать это в классе хранилища, чтобы коду вне класса не нужно было знать об этой структуре. Другим вариантом было бы, чтобы магазин содержал свой собственный контейнер с товарами. Это означало бы, что один товар логически должен находиться в нескольких контейнерах (например, в магазине и в отделе). Это предполагает использование контейнеров shared_ptr, чтобы каждый товар по-прежнему представлялся одним объектом данных.

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

person JaMiT    schedule 20.08.2018
comment
Вы читали это: zar, В моем реальном примере отношение часть-целое имеет смысл, и все элементы поддерживают основную операцию, поэтому мне это и нужно. - person Oliv; 20.08.2018
comment
Поскольку это утверждение прямо противоречит рабочему коду примера, неудивительно, что люди предпочитают обращаться к конкретному и видимому коду примера, а не к нечетко описанному и невидимому реальному коду. - person Useless; 21.08.2018
comment
@ Олив, я читал это. Я также читал пример, который должен был продемонстрировать тип отношения часть-целое, существующий в реальном примере. В этом (ненастоящем?) примере не удалось продемонстрировать взаимосвязь части и целого. Я пришел к выводу, что автор не понимает концепцию часть-целое. Учитывая отсутствие подробностей о реальном примере, вполне разумно предположить, что реальный пример может быть не более чем отношением части к целому, чем пример с товаром. - person JaMiT; 21.08.2018
comment
@Oliv Более короткая версия моего предыдущего комментария: я подозреваю проблему с XY; любое заявление о том, что что-то совершенно разумно, вызывает подозрение. - person JaMiT; 21.08.2018

Относительно вашего выбора дизайна

Давайте сравним описание намерения шаблона Composite в книге GoF с вашими требованиями:

  • #P2#
    #P3# #P4#
  • #P5#
    #P6#

Так что, по крайней мере, неочевидно, что Composite подходит, потому что описание не соответствует вашему варианту использования (в лучшем случае оно наполовину соответствует описанному вами варианту использования, но не вашему примеру кода).

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

В матче есть иннинги (счет/результат), у обеих сторон есть свои иннинги (есть очки), затем у каждого играющего игрока есть свои иннинги (счет с более подробной информацией). Когда матч продолжается, к матчу добавляется событие, которое добавляется к текущим иннингам и к иннингам текущего игрока. Таким образом, я строю счет таким образом, но в конце я хочу отобразить его, и каждый тип иннинга довольно разный, и настройка текущего состояния требует разных операций.

Хорошо, подача является частью матча, но есть ли у вас еще уровни? Иннингс состоит из мячей или других событий, или это просто... номер иннинга, игрок и счет?

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

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


Относительно вашей составной реализации

Интерфейс getMerchandise плохой. Почему он возвращает только первый из потенциально многих объектов? Зачем вообще нужно их получать?

Вы упоминаете два варианта использования:

  1. изменение свойств объекта

    По-видимому, вы знаете, какой объект вы хотите изменить, верно? Допустим, вы хотите изменить цену объекта, условно обозначенного как Mens>Shirts>Wrangler. Либо

    • попросить магазин перенаправить посетителя к этому объекту по имени (чтобы магазин нашел отдел с именем "Mens" и попросил этот перенаправить посетителя дочернему элементу, соответствующему Shirts>Wrangler).

    • просто найдите объект Shirt("Wrangler") непосредственно в каком-либо другом индексе (например, по номеру акции) и работайте с ним напрямую. Вам не нужно делать все с помощью шаблона Composite, даже если вы его используете.

  2. отображение объекта

    Но весь смысл шаблона Composite в том, что каждый объект должен реализовывать виртуальный display (или любой другой) метод. Вы используете это, когда хотите, чтобы каждый тип знал, как отображать себя.

person Useless    schedule 20.08.2018
comment
Вы читали это: zar, В моем реальном примере отношение часть-целое имеет смысл, и все элементы поддерживают основную операцию, поэтому мне это и нужно. - person Oliv; 20.08.2018
comment
Да, он погребен посреди горы нерелевантных деталей и работающего примера, который плохо соответствует заявленному варианту использования. - person Useless; 21.08.2018