Недавно на форуме (vingrad), обсуждали проблему "Связывание UI и функционала", собственно вопрос был в том, как уменьшить зависимости между классами, то-есть код реализующий UI не должен ничего знать о реализации.
Мое решение - использовать принцип inversion of control. Для этого нужен объект посредник, через который происходило-бы взаимодействие.
Ниже приведен код, который это реализует. GUI класс (клиент) просит пользователя ввести данные, но с тем-же успехом он мог-бы, к примеру создать модальный диалог для редактирования свойств.
#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 T& 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; double dbl_value; std::string str_value; public: Service(int i, std::string s, double d) : int_value(i), dbl_value(d), str_value(s) { } //метод создает proxy объекты для своих полей и передает их клиенту template<class C> void apply(C& 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<double>, &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<double>& v) { dbl_value = (double)v; } void set_str_value(const std::string& s) { str_value = s; } int get_int_value() { return int_value; } value_wrapper<double> get_dbl_value() { return value_wrapper<double>(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() << ", " << (double)srv.get_dbl_value() << std::endl; system("pause"); }Данная схема хорошо расширяется, это показано на примере свойства типа double, для которого написан враппер проверяющий диапазон введенного числа.
9 комментариев:
В принципе, можно сделать все то-же самое используя виртуальные функции.
Как я понял, в классе value_wrapper оператор присваивания описан не по шаблону:
value_wrapper& operator = (const double& v) {value_ = v;}
ЗЫ. Код большой, местами не влазит в шаблон.. Можно ли файл отдельной ссылкой? Еще хотелось бы узнать побольше подробностей по этому подходу :)
Да, это ошибка, нужно было написать
value_wrapper& operator = (const T& v) {value_ = v;}
торопился :)
код действительно не помещается в шаблон, нужно будет что-то с этим сделать...
В простом случае я бы передал классу Client ссылку на Service, а тот вызывал-бы get-set функции Servicе-a напрямую, но в случае когда Service к примеру базовый класс огромной иерархии объектов, это будет сложно сделать, так как набор свойств у всех отличается. Помимо этого, класс Client будет зависеть от класса Service, а в этом то-же хорошего мало. Можно еще добавить функцию для работы с GUI в класс Service, а это еще хуже, получится большой и всемогущий объект, решающий несколько задач.
А в моем примере вводится еще один класс - proxy_t, который содержит информацию о каком-то одном свойстве объекта. Класс Client ничего не знает о классе Service, класс Service знает о Client, но это не обязательно... В общем случае Service просто должен создать набор proxy объектов для всех своих переменных, которые может редактировать пользователь...
В результате я могу заменить Service любым другим классом и все будет работать.
А сам принцип описан здесь: Inversion of control
подправил пост
Спасибо за разъяснения, теперь все понятно :)
apply(int_proxy); client
1) std::runtime_error объявлен в <stdexcept>, не в <exception>. Ну и для вызова system надо воткнуть <stdlib.h>.
2) Как я и ожидал, g++ отказался есть вот эту строку:
std::cin >> static_cast<std::string>(p);
Двумя строками выше на чтение - пожалуйста, а тут - фигушки.
Вообще, зачем вся это возня с operator X, разве нельзя сделать просто сделать геттер в proxy, а для записи пользоваться например описаным оператором присваивания?
В простом случае я бы передал классу Client ссылку на Service, а тот вызывал-бы get-set функции Servicе-a напрямую, но в случае когда Service к примеру базовый класс огромной иерархии объектов, это будет сложно сделать, так как набор свойств у всех отличается. Помимо этого, класс Client будет зависеть от класса Service, а в этом то-же хорошего мало. Можно еще добавить функцию для работы с GUI в класс Service, а это еще хуже, получится большой и всемогущий объект, решающий несколько задач.
А в моем примере вводится еще один класс - proxy_t, который содержит информацию о каком-то одном свойстве объекта. Класс Client ничего не знает о классе Service, класс Service знает о Client, но это не обязательно... В общем случае Service просто должен создать набор proxy объектов для всех своих переменных, которые может редактировать пользователь...
В результате я могу заменить Service любым другим классом и все будет работать.
А сам принцип описан здесь: Inversion of control
Отправить комментарий