пятница, 18 июля 2008 г.

IoC

Недавно на форуме (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 комментариев:

Unknown комментирует...

В принципе, можно сделать все то-же самое используя виртуальные функции.

Анонимный комментирует...

Как я понял, в классе value_wrapper оператор присваивания описан не по шаблону:

value_wrapper& operator = (const double& v) {value_ = v;}

ЗЫ. Код большой, местами не влазит в шаблон.. Можно ли файл отдельной ссылкой? Еще хотелось бы узнать побольше подробностей по этому подходу :)

Unknown комментирует...

Да, это ошибка, нужно было написать
value_wrapper& operator = (const T& v) {value_ = v;}
торопился :)
код действительно не помещается в шаблон, нужно будет что-то с этим сделать...

Unknown комментирует...

В простом случае я бы передал классу Client ссылку на Service, а тот вызывал-бы get-set функции Servicе-a напрямую, но в случае когда Service к примеру базовый класс огромной иерархии объектов, это будет сложно сделать, так как набор свойств у всех отличается. Помимо этого, класс Client будет зависеть от класса Service, а в этом то-же хорошего мало. Можно еще добавить функцию для работы с GUI в класс Service, а это еще хуже, получится большой и всемогущий объект, решающий несколько задач.
А в моем примере вводится еще один класс - proxy_t, который содержит информацию о каком-то одном свойстве объекта. Класс Client ничего не знает о классе Service, класс Service знает о Client, но это не обязательно... В общем случае Service просто должен создать набор proxy объектов для всех своих переменных, которые может редактировать пользователь...
В результате я могу заменить Service любым другим классом и все будет работать.
А сам принцип описан здесь: Inversion of control

Unknown комментирует...

подправил пост

Анонимный комментирует...

Спасибо за разъяснения, теперь все понятно :)

Unknown комментирует...

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, а для записи пользоваться например описаным оператором присваивания?

Lazin комментирует...

В простом случае я бы передал классу Client ссылку на Service, а тот вызывал-бы get-set функции Servicе-a напрямую, но в случае когда Service к примеру базовый класс огромной иерархии объектов, это будет сложно сделать, так как набор свойств у всех отличается. Помимо этого, класс Client будет зависеть от класса Service, а в этом то-же хорошего мало. Можно еще добавить функцию для работы с GUI в класс Service, а это еще хуже, получится большой и всемогущий объект, решающий несколько задач.
А в моем примере вводится еще один класс - proxy_t, который содержит информацию о каком-то одном свойстве объекта. Класс Client ничего не знает о классе Service, класс Service знает о Client, но это не обязательно... В общем случае Service просто должен создать набор proxy объектов для всех своих переменных, которые может редактировать пользователь...
В результате я могу заменить Service любым другим классом и все будет работать.
А сам принцип описан здесь: Inversion of control