Связывание UI и функционала

Приветствую всех. Задумался над таким, интересным на мой взгляд, вопросом. Хотел бы услышать как его решают другие, наверняка есть какие-то более удобные и правильные универсальные схемы о которых я не знаю.Предположим есть набор классов, который отвечает за реализацию функционала ядра некоторой системы. Все эти классы так или иначе сходятся к одному суперклассу, точнее этот суперкласс их агрегирует, либо является связь является композиционной. В общем на диаграмме это может выглядеть так:У приложения есть GUI. Чтобы скрыть детали реализации между GUI и внутренней структурой существует некоторый промежуточный слой. Т.е. общая диаграмма выглядит так:Теперь собственно вопрос. Мне понадобилось отобразить данные "data" из класса "e" в окне подсистемы "GUI". Как лучше получить к ним доступ? Точнее вопрос даже стоит так: как определить ассоциативные связи IAbstract с остальными классами? Тут есть два пути(по-крайней мере я придумал только два):1. Первый - это дать доступ IAbstract ко всем классам напрямую. Это будет выглядеть примерно так:2. IAbstract имеет доступ только к методам SuperClass, а последний через серию переходников получает доступ ко всем данным. Т.е. вот так:Что, на ваш взгляд, лучше? Есть ли другие варианты? 
14 ответов

Да, 1 способ.
 К тому же в способе №2 добавляется множество переходников во все классы, т.е. такие конструкции:
Дело даже не в этом, а в оптимальности. Ну это что-то вроде:
bool a = ns1::check();....bool ns1::check(){return ns2::check();}... и т.д.
Смысл устанавливать лишние программные связи - никакого. Плюс, первый подход обеспечивает некую гибкость приложения.


Еще одно правило - всегда если возможно ********** связи. В 1-ом случае у тебя их больше, чем во 2-ом, поэтому он хуже.
С этой точки зрения хуже, да. Но вот, как уже Mal Hack упомянул, по части оптимизации как раз лучше. Если эти данные будут запрашиваться постоянно, то каждый раз придётся проходить цепочку вызовов от IAbstract до класса "e" в полном объёме, т.е. через все классы. Считаешь, это оптимально? Даже с т.з. зрения архитектуры, не говоря уже о реализации.
И конечно нужно идти от того, что за задачи выполняет интерфейс - можно ли его разделить на несколько независимых интерфейсов ?
Классов будет порядка 5-6 и делить их как раз не хочется, т.к. по логике они являются одним целым и в самом общем случае выполняют одну задачу.
А почему нельзя сделать метод у SuperClass который будет возращать get_data ?
Ну вот опять же мы увеличиваем кол-во связей. Поидее, классу SuperClass вообще пофиг до внутренней структуры класса "e" в котором хранятся нужные данные. Эти данные нужны только класу-владельцу и GUI для отображения. 


С этой точки зрения хуже, да. Но вот, как уже Mal Hack упомянул, по части оптимизации как раз лучше. Если эти данные будут запрашиваться постоянно, то каждый раз придётся проходить цепочку вызовов от IAbstract до класса "e" в полном объёме, т.е. через все классы. Считаешь, это оптимально? Даже с т.з. зрения архитектуры, не говоря уже о реализации.
Геттеры скорее всего будут встраиваться и никакого ущерба для быстродействия не будет, а если и будет, то вы все равно занимаетесь преждевременной оптимизацей (Саттер, Александреску, “Стандарты кодирования на C++”, одно из первых правил). А вот на уровне архитектуры такая цепочка это плохо, нарушает закон Деметры (“Only talk to your immediate friends”). Хотя в реальной жизни используется часто ;)


Ulysses4j, за ссылки спасибо. Но я всё-таки не понял, вы за какой подход?


Я не знаю, это слишком общая постановка вопроса, но то, о чем вы говорите, похоже на Façade pattern. Соответственно, второй подход.


Что бы таких ситуаций не было делается это так:Делай интерфейс на каждый из классов, не только на "супер класс", но  и на A, B, C, D, e, f, g (IA, IB, IC, ID, Ie, If, Ig соответственно).После этого, реализуй следующую связь  "IСупер класс" предоставляет доступ к интерфейсу IA, IB, IC, ID, пусть даже если они не предоставляют никаких данных (вся реализация скрыта, пока скрыта! ), если тебе понадобится открыть данные ты всегда сможешь добавить в интерфейс нужные данные. Интерфейс IA, IB должен предоставлять доступ к интерфейсу к Ie и If, а IC и ID к Ig.Доступ к Ie осуществляется таким образом: Получаешь интерфейс ISuperClass, от него интерфейс IA, от него Ie  у него get_data.Таким образом каждый класс отвечает за свою задачу, и если тебе прийдется что-то поменять, ты не затрагиваешь всю структуру приложения.. так как каждый работает только с наследуемым. Я делаю так, очень удобно, проблем не возникает.


Что, на ваш взгляд, лучше? Есть ли другие варианты? 
3й вариант:  совместить оба подхода:1.  определить структуры данных2.  раделить ядро  на компоненты     каждый компонент состоит из одного или множества объектов контролирующих данные     к внутренним объектам нет доступа извне      компонент предоставляет доступ к любым данным своих объектов 3. Разделить Гуи на компоненты     несколько виджетов реализующих общую смысловую функциональность представлены одним компонентом (точнее  делегируют функции одного компонента)4. реализовать логику взаимодействия между гуи- и ядро- компонентами     


Делай интерфейс на каждый из классов, не только на "супер класс", но  и на A, B, C, D, e, f, g (IA, IB, IC, ID, Ie, If, Ig соответственно).
это вызовет рост количества кода, при добавлении нового класса, тебе потребуется добавить еще и интерфейс для его, да и вообще я тут не вижу решения...Эту проблему можно решить используя принцип инверсии зависимости (IoC)Допустим у нас есть некоторый объект, который что-то реализует, и имеет набор свойств и функций - сервис. В общем случае нужно ввести дополнительную сущность(объект посредник), через которую клиент(GUI) сможет влиять на объект сервис...Например в интерфейс базового класса можно добавить вирт. функцию, которая бы возвращала такой proxy(посредник) объект. В качестве посредника, может выступать, к примеру xml документ, содержащий описание состояния (точнее той его части, которую может изменять клиент) и мета-информацию (типы переменных, различные описания), получив proxy объект, клиент может создать форму с виджетами для его редактирования динамически, когда пользователь изменит свойство, клиент(GUI класс) может отправить измененный xml документ( а еще лучше - его изменившуюся часть) сервису, который обработает его и изменит свое состояние... то-есть, в итоге, произойдет вызов метода, но не напрямую.Конечно xml это не очень эффективно, но его я привел только в качестве примера (который может вполне может быть использован). Можно придумать какую угодно реализацию proxy объекта, который, в общем случае должен содержать мета-информацию (описывать тип каждого свойства, название для пользователя и т.д.) и состояние (значение свойства).Можно придумать, к примеру структуру содержащую список указателей на методы объекта...
template<    typename I, // класс - сервис    X (I::*Get)(), // метод класса сервиса, для получения значения    void (I::*Set)(const X&),// метод для изменения свойства    typename X // тип свойства    >class proxy_t{        I& srv_;        std::string name_;    public:        proxy_t(I& serv, const std::string& str) : srv_(srv), name_(str)        {        }        proxy_t operator = (const X& value)        {            (srv_->*Set)(value);        }        operator X ()        {            return (srv_->*Get)();        }        std::string get_name()        {            return name_;        }};template<    template<        class I,         X (I::*Get)(),        void (I::*Set)(const X&),        typename X    >   class Property>void show_gui(CForm* form, Property& prop){    //добавляем к форме виджет для свойства произвольного типа}


Написал пример того, что я имел ввиду, только вместо создания диалога, класс Client использует для ввода значений консоль...
#include <exception>#include <iostream>#include <string>//объект посредникtemplate<    typename I, // класс - сервис    typename X, // тип свойства    X (I::*Get)(), // метод класса сервиса, для получения значения    void (I::*Set)(const X&)// метод для изменения свойства>class proxy_t{        I& srv_;//значение параметра        std::string name_;//имя параметра    public:        proxy_t(I& srv, const std::string& str) : srv_(srv), name_(str)        {        }        proxy_t& operator = (const X& value)        {            (srv_.*Set)(value);            return *this;        }        operator X ()        {            return (srv_.*Get)();        }        std::string get_name()        {            return name_;        }};//враппер для значения с диапазоном допустимых значенийtemplate<class T>struct value_wrapper{    T& value_;    std::pair<T,T> range_;    value_wrapper(T& v, T min, T max) : value_(v), range_(min, max) {}    //value_wrapper    value_wrapper& operator = (const value_wrapper& v) {value_ = v.value_; range_ = v.range_;}    value_wrapper& operator = (const ******& v) {value_ = v;}    operator T() const {return value_;}    bool check_range(T& v)    {        if (v < range_.first)            return false;        if (v > range_.second)            return false;        return true;    }};//класс отвечает за диалог с пользователем, ничего не знает о том объекте, чьи свойства он показываетclass Client//GUI{public:    //метод получает объект посредник и отображает диалог изменения свойства    template<        class Proxy    >    void apply(Proxy& p);    //специализация для посредника любого типа    template<            class I,             class X,            X (I::*Get)(),            void (I::*Set)(const X&)    >    void apply (proxy_t<I, X, Get, Set>& p)    {        //значение произвольного типа        X tmp = static_cast<X>(p);        std::cout << p.get_name() << " = " << tmp << std::endl;        std::cout << "let " << p.get_name() << " = " << std::ends;        std::cin >> tmp;        p = tmp;    }    //специализация для строковых значений    template<            class I,             std::string (I::*Get)(),            void (I::*Set)(const std::string&)    >    void apply (proxy_t<I, std::string, Get, Set>& p)    {        //значение типа std::string        std::cout << "str: " << p.get_name() << " = " << static_cast<std::string>(p) << std::endl;        std::cout << "let " << p.get_name() << " = " << std::ends;        std::cin >> static_cast<std::string>(p);    }    //специализация для числовых значений с проверкой диапазона    template<            class I,            class X,            value_wrapper<X> (I::*Get)(),            void (I::*Set)(const value_wrapper<X>&)    >    void apply (proxy_t<I, value_wrapper<X>, Get, Set>& p)    {        std::cout << p.get_name() << " = " << static_cast< value_wrapper<X> >(p) << std::endl;        std::cout << "let " << p.get_name() << " = " << std::ends;        X tmp;        std::cin >> tmp;        if (! static_cast< value_wrapper<X> >(p).check_range(tmp) )            throw std::runtime_error("input error");        p = value_wrapper<X>(tmp,tmp,tmp);    }};//класс - сервис, реализует какую-то бизнес логику, не работает с GUI самclass Service{    int int_value;    ****** dbl_value;    std::string str_value;public:    Service(int i, std::string s, ****** d) : int_value(i), dbl_value(d), str_value(s)    {    }    //метод создает proxy объекты для своих полей и передает их клиенту    void apply(Client& client)    {        proxy_t<Service, int, &Service::get_int_value, &Service::set_int_value> int_proxy(*this, "int_value");        proxy_t<Service, std::string, &Service::get_str_value, &Service::set_str_value> str_proxy(*this, "str_value");        proxy_t<Service, value_wrapper<******>,    &Service::get_dbl_value, &Service::set_dbl_value> dbl_proxy(*this, "dbl_value");        client.apply(int_proxy);        client.apply(str_proxy);        client.apply(dbl_proxy);    }    //методы для доступа к полям объекта    void set_int_value(const int& v)    {        int_value = v;    }    void set_dbl_value(const value_wrapper<******>& v)    {        dbl_value = (******)v;    }    void set_str_value(const std::string& s)    {        str_value = s;    }    int get_int_value()    {        return int_value;    }    value_wrapper<******> get_dbl_value()    {        return value_wrapper<******>(dbl_value, 0, 10);    }    std::string get_str_value()    {        return str_value;    }};int main(){    Service srv(25, "Foo", 3.14159);    Client gui;    srv.apply(gui);    std::cout << "result: " << srv.get_int_value() << ", " << srv.get_str_value() << ", " << (******)srv.get_dbl_value() << std::endl;    system("pause");}


mes, мне кажется ты представил разновидность варианта №1. В данном случае IAbstract - компонент для всех составных объектов классов SuperClass, A,B,C,D... Единственное, если базовых классов будет несколько, каждый из которых решает свою собственную задачу, то и IAbstract должно быть несколько, каждый из которых реализует интерфейс взаимодействия с внутренними данными составных классов. Lazin, ты взорвал мой мозг  Спасибо, что потратил время на код. На первый взгляд решение действительно неплохое, но сегодня я уже порядком подустал, поэтому отложим его разбор на завтра.


W4FhLF, задача слишком абстрактна для того чтобы выбирать какой из вариантов лучше. Тем не менее я склоняюсь ко 2 варианту...А такие конструкцуии явно указывают на ошибки с проектировании...
data d = SuperClass()->getA()->get_e()->get_data();
В общем надо плясать от конкретной задачи и уже там решать что лучше.


Я, конечно, не большой гуру в сях и не так много сделал, но, что на сях, что на ПХП всегда делал так.Есть гуй, который в зависимости от тех или иных ситуаций вызывает функции модуля, реализующего непосредственно решение задачи.В Qt это у меня вроде бы неплохо получилось разнести, грубо говоря:Class FormImpl {}; -> namespace modul {};Модуль же уже обращается к еще как бы более низкому уровню аля интерфейи для работы с портами. В принципе в ряде случаев, гуй сам обращается к портам минуя модуль, например при подключении...К примеру класс обработки исключений находится на той же ступеньки иерархии, что и модуль. Грубо говоря схема состоит из трех уровней: гуй, модуль, вспомогательные части модуля, которые могут использоваться и гуем, если уж так надо.


Mal Hack, т.е. в общем случае ты описал способ №1? Этот способ кажется довольно неплохим. Если меняется любой из нижележащих классов, GUI об этом ничего не узнает, т.к. детали реализации скрывает IAbstract, но в тоже время насколько верно давать доступ IAbstract ко всем данным? Хотя, с другой стороны, способ номер №2 ничего не лучше, ибо чтобы получить некоторые данные из класса "e" класс IAbstract  в любом случае должен знать их тип и уметь работать с ними. К тому же в способе №2 добавляется множество переходников во все классы, т.е. такие конструкции:
data d = SuperClass()->getA()->get_e()->get_data();
тоже не очень радуют  В общем нужен разумный компромис, а может и совсем иное решение. 


W4FhLF, Одно из ООП гласит - не делайте божественных классов, которые умеют все и вся. Каждый класс, метод должен решать только одну задачу.Еще одно правило - всегда если возможно ********** связи. В 1-ом случае у тебя их больше, чем во 2-ом, поэтому он хуже.И конечно нужно идти от того, что за задачи выполняет интерфейс - можно ли его разделить на несколько независимых интерфейсов ?У меня ни разу не было такого, чтобы интефрейс обьединял так много классов - 2, ну 3 максимум, но такое количество как у тебя на рисунках ни разу.Еще тогда нужно упоминуть о том, что нет универсальных способов для такой задачи. Все зависит от реальной ситуации, этим и сложно программирование, тут нет явных шаблонов проектирования, есть только рекомендации.
data d = SuperClass()->getA()->get_e()->get_data();
А почему нельзя сделать метод у SuperClass который будет возращать get_data ?