Отделение логики, отображения и данных


26 0

Несколько лет назад я читал книгу "Совершенный код" и выразил свое несогласие с автором, когда он говорил, что нужно писать не на языке, а с использованием языка. Как пример автор приводил утверждение, что если в языке нет объектов, то их нужно изобрести и написать. Это слишком радикальный метод, который я не могу поддержать. В данном случае проще и эффективнее будет поменять язык на объектный. Такие языки, как Delphi и Java слишком разные, чтобы писать на них одинаково, потому что у языков слишком разная идеология.

В последнее время я начинаю думать, что лучше бы все послушали Макконелла и всегда программировали с использованием языка, особенно программисты Delphi и С#. Почему? Потому что это визуальные языки и в них среды разработки очень мощные и балуют программиста простотой. Достаточно кликнуть мышкой, чтобы создать обработчик события и написать в нем код логики. Для очень и очень маленьких приложений это терпимо, а вот для больших проектов - это зло.

Существует множество подходов к программированию, но мне кажется, что лучший вариант - это отделение логики, данных и представления. Такие походы, как MVC придуманы уже давно и до сих пор успешно используются во многих компаниях. Только используются далеко не всеми. Я практически не видел подходы с разделением в проектах на Delphi и это ужасно. Народ почему-то пишет логику прямо в модулях представления или перепрыгивает сразу на трехуровневое программирование, когда код логики (тут любят применять красивое выражение "бизнес логика") выносится на промежуточный уровень.

Чем плох подход, когда логика находится прямо в модулях представления? Это идеальное бревно из дуба, у которого напрочь отсутствует гибкость. На последней работе за долгие годы существования компании было написано очень много кода с использованием устаревших технологий. Один только BDE чего стоит. Его уже давно используют, но при этом понимают, что вечно так продолжается не может. Так как логика доступа к базе данных внедрена непосредственно в модули представления (форм), то переход на другую технологию доступа к базам данных превращается в ад. Нужно перекалбасить все модули громадного количества проектов, созданных за долгие годы. Поэтому в компании особо не торопятся переходить на что-то новое и застряли в прошлом веке.

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

Чужие модули и проекты я критиковать не буду (я уже много раз говорил, что я не критик, и критиков и без меня полно), я лучше расскажу на примере своего проекта, что и почему я делал. У меня есть один проект, который я выкинул в свободный доступ с открытым кодом - Database Modeller. В нем далеко не все идеально, потому что я создал его давно и на скорую руку на конкурс от компании Sun Microsystems. Просто однажды зашел на сайт и увидел, что у них мало проектов зарегистрировано на конкурс, а у меня как раз на работе была скука-печаль и я на скорую руку набросал проект, который может быть кто-то доведет до ума :). Хотя прекрасно понимаю, что никто не будет этого делать. Но может хоть источники кому пригодятся.

Короче. Тем кто не в курсе - проект позволяет визуально строить простую модель базы данных и применять созданную модель на базу. Программа также позволяет построить модель по текущей базе.

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

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

Теперь, создаем наследника от этого базового класса DBInterface с именем DBInterfaceOracle, который реализует абстрактные функции с учетом базы данных Oracle. Таким образом, моя программа научится работать с этой БД. Можно создать еще одного наследника и реализовать необходимые функции с учетом любой другой базы данных. Таким образом, программа Database Modeller, сможет работать с любыми базами. Я сейчас точно не помню, но я кажется реализовал для примера работу с Oracle и MySQL.

Функции обращения к базе желательно было описать только в базовом классе, а в наследниках только использовать эти функции. В этом случае, если на рынке появится что-то более крутое для доступа к БД, достаточно будет переписать только один класс.

Благодаря такому разрешению, я могу изменять движок программы, не изменяя визуального представления, а так же изменять представление, не трогая движка. Работать с кодом удобно и непринужденно.

Точно так же построены программы CyD Careful Observer - Сетевой Монитор и Сетевые утилиты и безопасность, правда их исходные коды я показать не могу :). Поверьте мне на слово, в этих проектах код написан более аккуратно и логика напрочь отделена от представления. С большим распространением .NET 3.0 и 3.5, визуальный интерфейс будет переписан на WPF, при этом двиг программы переписывать не нужно будет.

Не знаю почему, но когда я писал на Delphi, то меня тянуло писать логику прямо в модулях визуального интерфейса. Когда я пишу на С# или Java, то меня тянет к отделению представления от логики. Мне сами языки нравятся и от программирования на С# в Visual Studio я получаю больше удовольствия.

Если ты знаком с Java, то советую посмотреть исходные коды этого о проекта. Он не идеален, но может быть, ты что-то найдешь для себя интересное в реализации. Напоминаю, проект создан на быструю руку.


Понравилось? Кликни Лайк, чтобы я знал, какой контент более интересен читателям. Заметку пока еще никто не лайкал и ты можешь быть первым


Комментарии

Alexo

05 Июня 2009

А в чем разница между RAD Studio и VS C#? И там и там обработчик событий создается двумя кликами.


Михаил Фленов

06 Июня 2009

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


pat

07 Июня 2009

Что сказать! Изучайте паттерны проектирования и антипаттерны тоже )))
Я не смотрел код, но судя по описанию движок использует паттерн Factory.
Вот уже 2 паттерна получается MVC и Factory.


.: Программер с нуле

07 Июня 2009

Михаил Фленов: Разницы технически нет, а вот не знаю почему, но на Delphi меня так и тянет написать код логики прямо в обработчиках событий.


Может дело в привычке?
К примеру, автор мог начать писать свой первый код на Delphi, и как раз таки без отделения логики от представления.
А изучая C# (случайно или перенял опыт учителя), приучил себя не писать код логики прямо в обработчиках событий :-)


Михаил Фленов

07 Июня 2009

А изучая C# (случайно или перенял опыт учителя), приучил себя не писать код логики прямо в обработчиках событий


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

P.S. Однажды на работе я писал программу, в которой попытался отделить код логики от представления, но начальник сказал, что это глупо и никто на Delphi так не пишет. Я не стал переубеждать, потому что понял, что это бесполезно. Этот случай окончательно отбил попытаки писать хороший код на работе.


Ronin

07 Июня 2009

Этот случай окончательно отбил попытки писать хороший код на работе


возникает вопрос - а где нибудь существует этот "идеальный код" так сказать, или он только в книжках и теории остаётся таковым? есть ли конкретные примеры?

неужели описанные тобою методы ты сам не применяешь кроме как в своих разработках?


Михаил Фленов

07 Июня 2009

Идеального кода наверно нет, поэтому я говорю о хорошем.

неужели описанные тобою методы ты сам не применяешь кроме как в своих разработках?


Применял, но в последний раз в 2004-м году, когда был сам себе хозяином. Тогда я работал в отделе электронщиков и в программировании начальник не соображал, поэтому я писал так, как хотел и сам принимал решения по архитектуре. После этого работал в программистских отделах и всегда подстраивался под людей, и под то, как они пишут. Стерался не выпендриваться и не выделятся. Могу высказать свое мнение, но не более.


pat

08 Июня 2009

к идеальному коду все стремятся...
А какая разница твоему шефу была, Михаил. Если ты напишешь код, который будет работать!?
Могу предположить, что все эти паттерны трудно понять, если их не знаешь. И это усложняет код для понимания...


Евгений

11 Июня 2009

Честно говоря так и не понял сути разделения.

Можно ли хоть какой-то пример реализации привести? Хотя бы одну функцию/процедуру или больше. Для наглядности. Ну и желательно бы на Delphi.


julik

12 Июня 2009

однако...


Alexo

15 Июня 2009

2Евгений

Там основная идея в том, что GUI можно заменить на CMD без преобразования логики программы.

Например

procedure Button1Clock (Sender:TObject);
begin
  // Вычисления
  Label1.Caption := Result;
end;

А теперь поменяем GUI на командную строку, и получиться что нам придеться полностью переписать код.
А если бы вычисления у нас были в отдельной процедуре, то выглядело это примерно так

procedure Button1Clock (Sender:TObject);
begin
  Label1.Caption := FuncName;
end;

А для команжной строки

WriteLn(FuncName);

Т.е. переписывать практически ничего ненадо. Пример канечно неахти какой, но надеюсь смысл будет понятен :)

Хотя я могу и неправельно понимать эту концепцию :)


sob-mk

15 Июня 2009

отделение логики от интерфейса было желательно всегда.
Но всегда ли это нужно? Наверное реально автоматизировать разработку кода и, тем более, интерфейса. Тогда возникает вопрос, нужно ли разделять что-либо, если при компиляции все становится одинаковым и двоичным?


Михаил Фленов

15 Июня 2009

Даже если интерфейс генерится, разделение не будет лишним. Допустим, что ты писал программы на С++ и тут решил перейти на современный С#. Если у тебя код "два в одном", то тебе придется переписывать абсолютно все заново. Сделать это для большого проекта за короткий срок нереально. Намного проще будет переносить по частям - сначала интерфейс (логику вызывать через invoke), а потом логику.

Любую большую задачу лучше делить на более маленькие и решать их по отдельности.


olegmaster

16 Июня 2009

да тут абсолютно Михаил прав. накладные расходы на резделение бизнес-логики, данных и остального обверточного кода невелики (если делать это с самого начала), а эффект по удобству доработки и масштабируемости - огромен. что с того, что в конечном двоичном счете все сольется воедино? машине удобно выполнять цельное, а человеку раскладывать на подзадачи и выполнять каждую по отдельности.


Alexo

16 Июня 2009

Любую большую задачу лучше делить на более маленькие и решать их по отдельности.


В "Совершенном Коде" даже описываеться почему это так :)


Евгений

17 Июня 2009

2 Alexo.
В основном я так и делаю, если правильно понимаю.

В обработчиках событий пишу по возможности только вызовы процедур и функций. А сами процедуры и функции хранятся в отдельных юнитах, и никак не взаимодействуют с интерфейсом. Мне кажется мигрировать со своей настольной БД на клиент-серверную смогу без полного переписывания кода. Хотя некоторые куски SQL-скриптов придется подрихтовать.
А вот перенос с Delphi на С я себе не представляю.


Alexo

17 Июня 2009

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

Лично я признаю только MySQL :) У меня из за этого в универе были проблемы, у нас препод больше любит Access, и велел все делать там, и все же он меня не убедил :)


Voprositel

20 Ноября 2009

Я создаю GUI в C++ Builder'е.
Я правильно понял, что мои классы (в которых логика) не должны взаимодействовать напрямую с формой и ее элементами?
Т.е., если я пишу программу, которая тестирует знания (простые тесты), и у меня есть класс формы (и объект этого класса) и отдельно класс самого тестера (в котором логика), я не должен из класса тестера напрямую взаимодействовать с элементами формы? Я должен все методы взаимодействия с формой писать в классе формы, а класс Тестер должен просто обращаться к этим методам, передавая им нужные данные? Например, раньше, если мне нужно было вывести какую-то информацию на форму, я в "своем" классе писал метод, который обращался напрямую к элементам формы, и использовал их. Это ведь не правильно?
Михаил, насколько я знаю, вы не используете билдер, но поскольку и в Delphi и в C++ builder'е используется VCL, и один и тотже принцип создания приложений, вы можете ответить на вопрос? Естественно, ответы других участников мне тоже интересны.


Михаил Фленов

20 Ноября 2009

Да, в твоей форме не должно быть никакой логики. В форме может создаваться класс, который будет реализовывать логику и ее вызывать.


Voprositel

20 Ноября 2009

Спасибо за ответ! Но хочу уточнить, так как в вашем сообщении не увидел ответа на мой вопрос.
-------------------------------------------------------
При создании программ я делаю примерно так:
1. Создаю интерфейс
2. Создаю новый юнит, а в нем класс (в котором логика)
3. Открываю cpp файл формы и подключаю к нему (include) h файл моего класса с логикой, чтобы в cpp файле формы можно было создать экземпляр (объект) класса с логикой.
4. Открываю cpp файл класса с логикой и подключаю к нему h файл формы, для того, что бы класс с логикой имел доступ к форме.

Потом просто в обработчиках событий формы вызываю методы класса. Когда я писал Тестер, у меня в обработчиках событий было в общей суме 6 строк (вызовы методов объекта Тестер).
------------------------------------------------------
Я все правильно делаю? Правильно ли, что объект Тестер обращается к элементам формы?

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

void __fastcall TMainForm::NextButtonClick(TObject *Sender)
{
tester.SledyuschiyTest();
}

И метод "SledyuschiyTest()" занимается загрузкой следующего теста, выводом его на форму и т.п.

Это правильная архитектура?


Михаил Фленов

20 Ноября 2009

Если твои обработчики событий не превышают 6 строк кода, то ты пишешь идеальный код. То, что твой класс тестера обращается к форме - не очень хорошо. Это значит, что он привязан к форме и знает о именах компонентов на ней. Нежелательно этого делать, потому что класс у тебя получается слишком привязанным к форме.

Ты приводишь правильный пример обработчика, но не понял, как у тебя класс Tester знает о форме? Обери эту связь. Если тебе нужно, чтобы на форме отображались какие-то данные от результата работы класса Tester, то перенос информации на форму делай в классе формы.


Voprositel

22 Ноября 2009

не понял, как у тебя класс Tester знает о форме?

Точно также, как форма знает о классе Tester. Я уже писал об этом:

Открываю cpp файл класса с логикой и подключаю к нему h файл формы, для того, что бы класс с логикой имел доступ к форме.



Если тебе нужно, чтобы на форме отображались какие-то данные от результата работы класса Tester, то перенос информации на форму делай в классе формы.

ОК, спасибо. Теперь буду писать более правильные программы.

И еще, извини за вопрос не в тему, но почему в C# события лучше делегатов? Прочитал о делегатах и событиях в разных источниках, но так и не понял их функционального отличия. События как бы упрощают работу с делегатами, но чем - мне не понятно.


Михаил Фленов

22 Ноября 2009

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


Voprositel

22 Ноября 2009

Нет, ты не понял (или что-то путаешь).

Вот пример:


События в C# основаны на делегатах. Это означает, что событие имеет тип определенного делегата. Вот пример класса с событием в нем:
using System;
namespace test
{
    //Объявление делегата.
    delegate void EventHandler();
    //Объявление класса с событием.
    class MyEvent
    {
        //Объвление события на основе делегата.
        public event EventHandler f;
        //Объвление метода, в котором вызывается событие.
        public void func()
        {
            f();
        }
    }
    //Класс для тестирования события класса MyEvent.
    class Test
    {
        //Обработчик для события.
        public static void z()
        {
            Console.WriteLine("Вызов обработчика");
        }
        //Метод main.
        static void Main(string[] args)
        {
            //Создаем экземпляр класса с событием.
            MyEvent w=new MyEvent();
            //Добавление обработчика события.
            w.f+=new EventHandler(z);
            //Вызов метода, в котором вызывается событие.
            w.func();
        }
    }
}


Сергей

23 Ноября 2009

Voprositel, я не программист, но позволю себе тоже сюда "внедриться"
Для того чтобы организовать взаимодействие экземпляра класса формы с классом вашего Тестера, нужно в классе Тестера создать метод, имеющий в своём определении объявление TForm, например, или TLabel - в зависимости от того куда вы хотите выводить результаты
Затем из класса формы обращаться к методам тестера подставляя в метод ту форму (или метку), куда вы хотите вывести результат. При этом модуль где описан класс Тестера может ВООБЩЕ не ссылаться на класс вашей формы так как он в ней не нуждается. В дальнейшем этот факт облегчит повторное использование вашего Тестера, потому что он не привязан к конкретной форме.
Так форма будет отделена от модуля теста. А модуль теста не будет привязан ни к одной конкретной форме. Вместо этого он будет "привязан" к объектам типа TForm или TLabel или к любому другому по вашему желанию
Можно также перегрузать несколько методов. Создать несколько методов с одинаковыми именами, но с разными входными параметрами, Тогда вообще этому методу можно "подсовывать" почти что угодно.

Я мог в чём то ошибиться. Давно не открывал Дельфи. Поправте меня если где то я соврал.


Михаил Фленов

23 Ноября 2009

Ты предлагаешь передавать в класс форму и класс будет заполнять данными форму.

Тестер.Метод(виуальный элемент);

Это слишком крутое связывание и гибкость отсутствует.

Чтобы этого небыло, нужно чтобы тестер ничего не знал о форме:

Визуальный элемент = Тестер.Метод();

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


Добавить Комментарий

Еще что-нибудь

Хотите найти еще что-то интересное почитать? Можно попробовать отфильтровать заметки на блоге по категориям.

О блоге

Программист, автор нескольких книг серии глазами хакера и просто блогер. Интересуюсь безопасностью, хотя хакером себя не считаю

Обратная связь

Без проблем вступаю в неразборчивые разговоры по e-mail. Стараюсь отвечать на письма всех читателей вне зависимости от страны проживания, вероисповедания, на русском или английском языке.

Пишите мне