Архитектура. Данные и их представление.
Есть проект экспертной системы. В этом проекте есть несколько различных диагностик. Каждая диагностика это отдельный диалог. Но также есть диагностика, которая разделяется на 3 вида, т.е. в 3х различных диалогах. В этих 3х диалогах очень много одинаковых данных.
Вопрос следующий: как правильно организовать доступ этих 3х диалогов к одним и тем же данным (т.е. данным присущим одной диагностике)?
Я сделал следующим образом: сделал один базовый класс для всех эти 3х диалогов (ну не в глобальных же переменных хранить).
Class CBaseForSomeDiagnostic
{ ...
const int m_somevar;
}
class CSomeDiagnostic1: public CDialog, public CBaseForSomeDiagnostic
{
}
...
правильный это подход или нет?
Короткий ответ: НЕТ.
Подробнее.
Увязать данные и их отображение в один класс - это частая ошибка начинающих, особенно тех, кто использует продукты Борланда.
Правильное проектирование предполагает, что данные и представление их пользователю должны быть раздельны.
Слышал наверное понятия "документ/вид", "модель/контроллер/представление"?
Подробнее.
У тебя есть некоторые данные (int count, char* name, foat age), есть некоторый диалог, который отображает эти данные (Dialog). Так вот данные не должны быть членами класса диалога, а должны быть самостоятельным классом или системой классов. Класс данных должен инкапсулировать данные, может иметь методы для манипуляции с этими данными (get, set, load, save, calc, etc.). В архитектуре "документ/вид" этот класс будет "документом".
Класс диалога - это "вид". Он имеет линк на данные (попросту, указатель на класс данных) и некоторые механизмы по получению данных и их отображению
{
private:
int m_count;
char* m_name;
foat m_age;
public:
int getCount() const { return m_count; }
const char* getName() const { return m_name; }
float getAge() const { return m_age; }
void load(const char* filename);
void save(const char* filename);
};
class Dialog
{
private:
Data* m_data;
public:
Dialog(Data* data) :data(m_data) {}
void showData()
{
m_data->getCount();
..........
}
};
Т.о. данные существуют независимо от интерфейса пользователя, что очень удобно, например, при необходимости переделки программы с консольной на оконную или измекнения внешнего представления данных. Но это не удобно если меняется сама модель данных или когда один диалог должен отображать различную информацию в зависимости от условия.
В этом случает полезнее архитектура "модель/контроллер/представление", в которой данные ничего не знают о диалогах (как и в "документ/вид"), а диалоги ничего не знают о модели данных. Всем управляет контроллер, который знает и о модели данных и о диалогах. Он управляет тем какие данные и где должны отображаться.
{
private:
int m_count;
char* m_name;
foat m_age;
public:
int getCount() const { return m_count; }
const char* getName() const { return m_name; }
float getAge() const { return m_age; }
void load(const char* filename);
void save(const char* filename);
};
class Dialog
{
public:
void showName(const char* name);
void showAge(float age);
};
class Controller
{
private:
Data* m_data;
Dialog* m_dialog;
public:
void showData()
{
m_dialog->showName( m_data->getCount() );
m_dialog->showAge( m_data->getAge() );
}
};
Т.о. при изменении модели данных или при изменении отображения у нас будет менятся только контроллер.
Например: имеется БД, есть окно с перечнем неких объектов предметной области. Тут проблем никаких нет. На конструктор данного окна мы просто и быстро загружаем данные в грид. Далее. Часто объекты предметной области наделены многочисленными атрибутами (множество полей определенной таблицы БД) и их все выдавать в грид с точки зрения читабельности неправильно. В гриде должны быть самые актуальные данные, по которым пользователю можно понять, с каким имеенно экземпляром объекта он имеет дело. Поэтому для получения детальной инфоримации необходимо дополнительное окно. Пользователь выбирает нужную запись в перечне, нажимает на кнопку либо на пункт выпадающего меню "редактировать". При этом вызывается конструктор детального окна, в который передаётся значение ключегого поля/полей(если ключ составной). На конструкторе детальной формы происходит
FROM ...
WHERE (ключевое поле=значение, передаваемое на конструктор)
Данные запроса идут в соответствующие компоненты отображения данных окна (различные комбобоксы, текстбоксы и прочее). Всё. Быстро в разработке, быстро работает, легкость поддержки приложения.
А с чего ты взял, что представленное тобой ниже описание проще?
ведь конечному пользователю важно не как это устроено внутри, а чтобы приложение находилось в работоспособном состоянии и быстро работало.
Программный код пишется для программистов, а не для конечного пользователя.
Например:
<skip>
Всё. Быстро в разработке, быстро работает, легкость поддержки приложения.
Ты ведь не избавился ни от одного из членов модели "модель/контроллер/представление":
БД - модель,
SQL-запрос - можно посчитать за контроллер,
GUI - представление.
Просто ты "зашил" контроллер в представление, чем сделал систему менее расширяемой и поддерживаемой.
Несколько вопросов:
1 Если мне надо открыть два одинаковых окна, запрос к БД будет повторяться? А как же быстродействие?
2. Если я внесу изменения в одном таком окне, как они попадут во второе?
3. Как будут обрабатываться ошибки обращения к БД, например, при потери связи?
Во всех этих случаях, как и многих других, помог бы отдельно существующий контроллер.
Почитай:
http://ru.wikipedia.org/wiki/Model-view-controller
http://chtivo.webhost.ru/articles/mvc.php
http://lib.juga.ru/article/view/163/1/68/
P.S. Как ты умудрился откопать и поднять тему трехлетней давности?
1. Если мне надо открыть два одинаковых окна, запрос к БД будет повторяться? А как же быстродействие?
Тут я с Вами согласен. При этом запрос к БД повторится. Но давайте задумаемся о практической ценности открытия двух одинаковых окон. Нужно ли это? Я например, в своём подобном приложении БД сделал так, что если пользователь открывает окно, то перед открытием окна запускается функция, которая проверяет, открыто ли уже окно такого же класса. Если такое окно уже есть, то оно закрывается и открывается заново, тем самым не давая возможность пользователю открывать два окна одного и того же класса. Кривовато, зато просто.
2. Если я внесу изменения в одном таком окне, как они попадут во второе?
Да, в этом случае два окна будут показывать разные данные. Но опять же повторюсь, что в дублировании одинаковых окон никакой практической ценности я не вижу.
3. Как будут обрабатываться ошибки обращения к БД, например, при потери связи?
Ошибки обрабатываются при получении данных по выбранной записи в защищённом блоке try/catch в конструкторе окна с выдачей соответствующих сообщений пользователю.
P.S. Как ты умудрился откопать и поднять тему трехлетней давности?
Я открыл тему "ADO.NET: Архитектура клиентского приложения БД" в разделе "Microsoft .NET Framework" форума. Если открыть эту тему, то в самом низу страницы, в перечне "Похожие темы" представлена именно эта тема.
Хочу Вас поблагодарить за представленную информацию. Обязательно всё изучу и прочитаю. Большое Вам спасибо. Всегда готов учиться от более опытных специалистов.
<skip>
Да, в этом случае два окна будут показывать разные данные. Но опять же повторюсь, что в дублировании одинаковых окон никакой практической ценности я не вижу.
Пути пользователя неисповедимы... :)
Ситуация с двумя одинаковыми окнами лишь для упрощения. Допустим, это два разных окна для представления одних данных, например, в табличной форме и в виде графика.
Ошибки обрабатываются при получении данных по выбранной записи в защищённом блоке try/catch в конструкторе окна с выдачей соответствующих сообщений пользователю.
Т.е. каждое окно ещё и управляет некоторыми исключительными ситуациями и рядом штатных служебных действий (соединиться с БД и т.п.). Кроме банального дублирования кода это приводит к сложностям с изменениями: при малейших изменениях реакции или способа доступа к БД, менять придется все окна!!!
А теперь предположим такую ситуацию. Раньше среднее арифметическое нескольких полей хранилось в третьем поле. Позднее это было оптимизировано и среднее арифметическое стало вычисляться, а не храниться. Получается, что теперь надо переписать все окна, которые завязаны на среднее арифметическое?
Далее мы изменили способ подсчета среднего арифметического, например, изменили способ округления. Опять меняем все окна?
А представь, где-то забыли поменять способ округления. При отсутствии хорошего тестового покрытия, такая ошибка выявиться бухгалтерами лишь в конце квартала хорошенькой недостачей... :)
Более глобальный вариант. Раньше программа работала с одним видом БД, теперь она должна работать с другим видом. Не важно, что изменилось СУБД или просто размещение данных по таблицам. Наверное, странным было бы менять при этом GUI, если визуальное представление данных не меняется. Проще изменить то, что предоставляет данные для визуального представления, т.е. контроллер.
Аналогия с просмотром графических файлов: пользователю все равно открывает он bmp или jpg, внешний вид программы от этого не меняется, контроллер (конвертор) заботиться о том, что разнородные на входе данные преобразуются к единому визуальному представлению.
А то я чтото не могу предположить как юзер может обратиться прямо к контроллеру. Полагаю такое обращение может происходить по разному и ето не должно косаться контроллера. Поскольку в моделе нет сущности ответственной за ввод информации ето ИМХО должно делать "представление" потому что оно отвечает за интерфейс пользователя.
БД и SQL-запрос - модель,
GUI - контроллер и представление.
Однако, все равно, работу с информацией берет на себя самостоятельная логика - "модель", а не интерфейс пользователя.
Я же в своих предыдущих постах имел в виду несколько иную схему, не MVC, а MVP, что является логическим развитием первой:
http://aviadezra.blogspot.com/2007/07/twisting-mvp-triad-say-hello-to-mvpc.html
http://martinfowler.com/eaaDev/uiArchs.html
http://www.martinfowler.com/eaaDev/PresentationModel.html
Пути пользователя неисповедимы... :)
Ситуация с двумя одинаковыми окнами лишь для упрощения. Допустим, это два разных окна для представления одних данных, например, в табличной форме и в виде графика.
Хороший пример, тут я с вами согласен.
Т.е. каждое окно ещё и управляет некоторыми исключительными ситуациями и рядом штатных служебных действий (соединиться с БД и т.п.). Кроме банального дублирования кода это приводит к сложностям с изменениями: при малейших изменениях реакции или способа доступа к БД, менять придется все окна!!!
Если контроллер и представление единое целое-да, действительно придётся менять все окна. Если же их разделить и учесть, что один контроллер (единственный и неповторимый) ставится в соответствие с одним единственным представлением, то также придётся менять каждый контроллер в отдельности. Конечно, если контроллер является частью приложения. Работы от этого меньше не станет. Если сделать контроллер на стороне сервера, то в приложении ничего менять не придётся.
А теперь предположим такую ситуацию. Раньше среднее арифметическое нескольких полей хранилось в третьем поле. Позднее это было оптимизировано и среднее арифметическое стало вычисляться, а не храниться. Получается, что теперь надо переписать все окна, которые завязаны на среднее арифметическое?
Далее мы изменили способ подсчета среднего арифметического, например, изменили способ округления. Опять меняем все окна?
А представь, где-то забыли поменять способ округления. При отсутствии хорошего тестового покрытия, такая ошибка выявиться бухгалтерами лишь в конце квартала хорошенькой недостачей... :)
Таких и им подобных ситуаций можно избежать, если все вычисления проводить на стороне сервера. Приложению должно быть всё равно что это за поле: либо это значение поля в таблице, либо это поле является результатом запроса. Такая возникшая ситуация приложение совершенно не затронет.
Более глобальный вариант. Раньше программа работала с одним видом БД, теперь она должна работать с другим видом. Не важно, что изменилось СУБД или просто размещение данных по таблицам. Наверное, странным было бы менять при этом GUI, если визуальное представление данных не меняется. Проще изменить то, что предоставляет данные для визуального представления, т.е. контроллер.
В случае создания клиентского приложения базы данных, контроллер необходимо располагать на стороне сервера. При этом необходимо будет изменить только объекты БД: хранимые процедуры, триггеры, функции и представления. Если изменилась СУБД, то в большинстве случаев меняется только ConnectionString.
То, о чём вы говорите-это всё совершенно верно, но всё зависит от специфики приложения. Если приложение однопользовательское или несетевое, то надо делать именно так, как вы и говорите: отдельно классы модели, отдельно классы, описывающие контроллер и собственно окна-представление. В случае написания приложения БД контроллер должен быть видимо на стороне сервера.
Вся логика обработки данных в хорошо спроектированных приложениях выполняется на сервере БД (если говорить о 2-хзвенной архитектуре).
Генерация исключений также там - клиентское приложение его просто ловит и каким-то образом обрабатывает - в простейшем случае просто отображает пользователю.
За подключение к БД обычно отвечает специальный модуль (например DataModule в Delphi\C++Builder), в котором располагаются компоненты связи с БД, и если есть какие-то изменения в способе коннекта, то изменения осуществляются только в нем.
Далее мы изменили способ подсчета среднего арифметического, например, изменили способ округления. Опять меняем все окна?
А представь, где-то забыли поменять способ округления. При отсутствии хорошего тестового покрытия, такая ошибка выявиться бухгалтерами лишь в конце квартала хорошенькой недостачей... :)
Опять-таки - логика на сервере. Для запросов с клиента можно создать в БД представления, и клиентскому приложению будет все равно - из каких таблиц и колонок реально вытягиваются данные.
Аналогия с просмотром графических файлов: пользователю все равно открывает он bmp или jpg, внешний вид программы от этого не меняется, контроллер (конвертор) заботиться о том, что разнородные на входе данные преобразуются к единому визуальному представлению.
Ситуация, когда меняется СУБД, имхо, довольно редки. Если изменилось размещение данных по таблицах, как я уже сказал, есть представления (VIEW).
А если етой БД вообще нет ?
Да ну?!
А как же тогда классификация на "толстых" и "тонких" клиентов?
Я правильно понял, что " хорошо спроектированные приложения" - это неоптимальные по скорости и объему трафика приложения? :)
Опять-таки - логика на сервере. Для запросов с клиента можно создать в БД представления, и клиентскому приложению будет все равно - из каких таблиц и колонок реально вытягиваются данные.
Ситуация, когда меняется СУБД, имхо, довольно редки. Если изменилось размещение данных по таблицах, как я уже сказал, есть представления (VIEW).
Так чем это противоречит моему высказыванию про MVC и MVP ?!
На самом деле нет никакой разницы - где выполняется обработка данных - на клиенте ли, на сервере ли. Хорошо спроектированное приложение в большинстве своем будет соответствовать документ-контроллер-представление. Или документ-представление как минимум.
Нарушение этого принципа в средах разработки Borland (а так же помоему в среде VBasic и VFoxPro)- это не то, что стоит брать за пример. Это скорее пример как делать не нужно.
Естественно если речь идет о утилите, реализующей от силы 2-5 функций, то скорей всего сильно переживать по данному поводу не стоит. Если же речь идет о проекте содержащем свыше 10 модулей компиляции - если вы не разделяете данные и их представление - никто вас конечно не накажет. И вы вольны придумывать себе какие угодно трюки - хоть сиквел-запросы в конструкторе, хоть чтение мантр на рассвете - но проблемы вы вобщем-то создадите себе сами. Почему - Green помоему, объяснил достаточно подробно еще 2 года назад. :)
А как же тогда классификация на "толстых" и "тонких" клиентов?
Я правильно понял, что " хорошо спроектированные приложения" - это неоптимальные по скорости и объему трафика приложения? :)
Так чем это противоречит моему высказыванию про MVC и MVP ?!
Как раз толстый клиент - это тот, у которого логика сконцентрирована на клиетской части, а не на серверной. Вот как раз там и получается неоптимальный по объему трафика и по скорости приложения - т.к. надо тянуть с БД данный на клиент, там их обрабатывать и закачивать обратно. А в тонких клиентах обработка идет на сервере - и на клиентскую часть передается только результат.
А насчет чему это противоречит - я просто ответил на вашу ремарку, что при изменении в хранении данных нужно переделывать "окошки". А я ответил - что не нужно ничего будет переделывать.
Все зависит от конкретной задачи. Если, к примеру, БД хранит лишь сумму чисел, то нет смысла передавать все слагаемые на сервер, чтоб посчитать и сохранить сумму.
Если к примеру, БД хранит слагаемые, то нет смысла передавать на клиент ещё и посчитанную сумму.
Поэтому твое утверждение про "хорошо спроектированные приложения" ("Вся логика обработки данных в хорошо спроектированных приложениях выполняется на сервере БД (если говорить о 2-хзвенной архитектуре).) не верно в корне.
Хорошо спроектированное приложение - это приложения отвечающее требованиям.
А теперь покажи, как ты сможешь обойтись без изменения кода формы при изменении формата хранения данных:
FROM ...
WHERE (ключевое поле=значение, передаваемое на конструктор)
заметь этот код жестко зашит в код конструктора формы.
Если к примеру, БД хранит слагаемые, то нет смысла передавать на клиент ещё и посчитанную сумму.
В большинстве случаев БД хранит и слагаемые, и (если это необходимо, сумму). Поэтому если нужна сумма действительно, нет смысла тянуть слагаемые - достаточно вытянуть только сумму.
Ну насчет неверно в корне все ж таки можно поспорить. Давайте тогда уж приведем плюсы и минусы толстого и тонкого клиента и сравним.
Ну а это уж бесспорно)
заметь этот код жестко зашит в код конструктора формы.
На конструкторе детальной формы происходит
Код:
SELECT ... FROM ... WHERE (ключевое поле=значение, передаваемое на конструктор)
Запросто. Повторяю еще раз, достаточно в поле FROM подставить VIEW. При изменении расположения данных, меняем запрос в VIEW, при этом для клиента все остается как было (view по сути - это интерфейс). не нравится вьюха - можно написать хранимую процедуру, возвращающую нужные данные. Можем менять внутренности процедуры на сервере - для клиента опять все прозрачно
В большинстве случаев БД хранит то, что туда поместили.
Ну насчет неверно в корне все ж таки можно поспорить. Давайте тогда уж приведем плюсы и минусы толстого и тонкого клиента и сравним.
А зачем?
Давайте приведем плюсы и минусы трактора и спортивного автомобиля, и сравним. :)
Запросто. Повторяю еще раз, достаточно в поле FROM подставить VIEW. При изменении расположения данных, меняем запрос в VIEW, при этом для клиента все остается как было (view по сути - это интерфейс). не нравится вьюха - можно написать хранимую процедуру, возвращающую нужные данные. Можем менять внутренности процедуры на сервере - для клиента опять все прозрачно
О чем ты споришь?
Я утверждаю, что если логику работы с моделью "зашить" в GUI, то при изменении модели придется изменять GUI. Что бы этого избежать вводится промежуточное звено - "представление" ("предъявитель", Presenter).
Ты начинаешь с этим спорить, и тут же предлагаешь ввести такое звено, только называя его по-разному: VIEW, хранимая процедура и т.п.
Тебе не кажется, что в своем стремлении поспорить ты противоречишь сам себе?
Получается примерно такой диалог:
OC: Нифига! Я вот выхожу в своих ледоступах и не падаю!
Если обувь правильно спроектирована, с шипами, то не упадешь!
BU: Б..ть! Ледоступы по-твоему это скользкая обувь?
Ты бы ещё ледорубы в руки взял и встал на четыре точки!
OC: Тогда давайте приведем все плюсы и минусы ледоступов и ледорубов, и сравним.
:)