3 августа 2010 г.

98/97: Отделяйте интерфейс пользователя от кода

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

"По мотивам" этой серии я хотел бы озвучить очередной факт из "98-ми вещей, которые должен знать каждый Delphi программист" (где остальные 97 вещей взяты из указанной выше серии ;) ). Это действительно must-know, но он не покрыт в указанной серии (видимо потому, что он достаточно специфичен?).

Типичный код в "типичной" Delphi-программе

В любом случае, надо сказать, что возникает это не на пустом месте - в определённом смысле Delphi "поощряет" подобную практику. Заключается она в следующем: вы пишете весь код программы в обработчиках событий формы. Например:
procedure TForm1.Button1Click(Sender: TObject);
var
  FS: TFileStream;
  // ...
begin
  FS := TFileStream.Create(Edit1.Text, fmOpenRead or fmShareDenyWrite);
  try
    ... // какая-то обработка файла из Edit1
  finally
    FreeAndNil(FS);
  end;
end;
Зачастую вообще весь код программы находится в обработчиках событий.

Что с этим не так?

Ну, что не так с этим подходом? Ведь когда ты щёлкаешь по кнопке на форме, Delphi услужливо генерирует и ассоциирует обработчик события, открывает редактор, позиционирует курсор на begin/end, как бы приглашая вас к написанию тут логики программы.

Давайте считать:
  • Ваш код зависит от формы. Нужна ли вам форма, чтобы обработать файл? Нет. Форма служит только для ввода параметров и запуска процесса работы. Отсюда следует несколько следующих пунктов.
  • Если к вам подойдёт начальник и скажет: "слушай, тут заказчик попросил сделать консольный вариант твоей программки - они хотят запускать её в своих скриптах автоматизации", то скажете ли вы ему, что это потребует написание программы с нуля? "Ну у тебя же всё готово?" Да, готово. Только совершенно не предназначено для таких изменений.
  • Потом к вам снова подойдёт начальник и скажет: "слушай, нашего заказчика уже достало, что после каждого изменения в твоей программе что-то плывёт. Давай-ка ты напишешь тестики, которые закрепят основной функционал, ага?" Уммм... вот только чтобы запустить тест вам придётся создать форму, ввести данные в контролы на форме, щёлкнуть по кнопке... и ещё как-то распознать результат с формы!
  • И снова к вам подходит начальник и говорит: "слушай, может я тебя и достал, но у заказчика многопроцессорная машина, и они хотят, чтобы можно было открыть несколько окон с обработкой - пусть машина себе молотит по четыре файла сразу". Упс. Вот только у вас всё завязано на форму, которая только одна, да и вообще вы использовали глобальные переменные.
Это только самые яркие примеры. К примеру, не слишком красивые ситуации будут получаться, когда вам придётся изменять интерфейс - и перелопачивать весь код, который с ним работает. Ещё пример? Ну, размещение всего кода в обработчиках событий ведёт к непомерному их разрастанию - часто вся логика программы будет заключаться в огромном монолитном куске кода, в котором без комментариев просто не разобраться. Или, ещё пример: потом вы обнаружите, что обработка сколько-нибудь больших файлов занимает время и захотите вынести это дело в фоновый поток, чтобы не тормозить интерфейс, и... не сможете это сделать, потому что всё завязано на форме, а VCL однопоточна, и снова всё придётся переписывать...

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

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

Что с этим делать?

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

Что значит "разграничить"? В простейшем случае это означает выполнение правила "код логики программы не должен ссылаться на интерфейс пользователя". К примеру, в нашем коде в начале поста рабочий код ссылается на Edit1 - ведь там лежит имя файла. Так вот, такого быть не должно: а вдруг вам имя файла передают параметром в командной строке? А вдруг его задаёт модульный тест?

Как это можно сделать? Ну, подумайте сами: если вы не можете ссылаться на интерфейс пользователя, то данные, приходящие с этого интерфейса, нам должен кто-то передавать. Оставим пока этого "кто-то": куда он должен передавать данные? Если вы практикуете процедурный подход, то, наверное, он должен передавать вам данные в параметрах. Если вы практикуете ООП, то к указанному способу добавляется ещё два: данные могут передаваться в конструктор или через свойства объекта. Окей, если вы сейчас читаете этот пост, то навряд ли вы вообще практикуете эти подходы - скорее вы придерживаетесь подхода "пиши весь код в Button1Click" ;) Оставим пока в стороне покрытый мхом процедурный подход - обратимся к ООП.

ООП говорит нам, что у нас должен быть объект, который будет заключать в себе логику программы. Объект, заключающий в себе интерфейс пользователя, у нас уже есть - это наша форма TForm1. Тогда создаём новый модуль File/New/Unit и начинаем писать новый объект, имя которого, за неимением пока лучших вариантов, пусть будет TWorker (ну или TИмяВашейПрограммыWorker):
unit Unit2;

interface

type
  TWorker = class
  end;

implementation

end.
Зачем нужен отдельный модуль: почему бы не пихнуть класс в Unit1? В-принципе, можно. Но лучше так не делать. Вынос рабочего кода в отдельный модуль имеет одно существенное преимущество (оно, впрочем, не единственное): форсирование разделение кода. Иными словами, вы не должны указывать в uses модуля Unit2 свой модуль с формой (Unit1) - это автоматически обеспечит выполнение требования "рабочий код не должен зависеть от интерфейса". В противном случае (размещение нового класса прямо в модуле с формой) вы можете ошибочно использовать в рабочей логике ссылки на форму.

Далее, вы смотрите, что должна уметь делать ваша программа - и создаёте по методу на каждое действие. Давайте пока ограничимся не слишком сложной программой, которая выполняет небольшой диапазон действий. Понятно, что для больших программ вы не ограничитесь одним объектом - у вас их будет много, и имена они будут тогда иметь поосмысленнее, а не просто TWorker (например, TDocument, TDocumentPrinter, TDocumentOptions и т.п.).

Для первого приближения вполне можно взять методы, которые копируют действия, которые вы выполняли в обработчиках событий. Как этот код будет работать с данными, которые сейчас у вас задаются на форме? Ну, есть несколько вариантов:
  • Вы передаёте их параметрами в метод.
  • Вы передаёте их в конструктор объекта.
  • Вы передаёте их через свойства объекта.
Ограниченный объём поста не позволит мне подробно разбирать каждый случай, поэтому для простоты возьмём только первый способ:
...

type
  TWorker = class
    procedure ProcessFile(const AFileName: String { и другие параметры, если надо } );
  end;

implementation

procedure TWorker.ProcessFile(const AFileName: String { ... } );
var
  FS: TFileStream;
  // ...
begin
  FS := TFileStream.Create(AFileName, fmOpenRead or fmShareDenyWrite);
  try
    ... // какая-то обработка файла AFileName
  finally
    FreeAndNil(FS);
  end;
end;

...
Обратите внимание, что теперь этот код не связан с формой - в нём нет ссылок на Form1 и на Edit1. И вы можете вызвать его и из консольного процесса, передав имя файла, заданное в командной строке. Вы можете вызвать его из модульного теста, передав фиксированное имя тестового файла. Вы можете создать много экземпляров объектов и запустить их все одновременно. Короче, теперь вы можете делать с кодом целую кучу вещей.

Как использовать такой объект в программе? Ну, во-первых, очевидно, что надо вписать имя модуля Unit2 в uses нужного модуля :D Далее вариантов немного:
  • Создать объект на всё время жизни формы.
  • Создать объект только на время, в момент вызова обработчика события.
Какой из подходов выбирать - сильно зависит от того, как вы реализовали объект. Если у вас большой объект, который включает в себя всю функциональность программы (т.е. у вас среднего размера программа) - вероятно, вы предпочтёте первый вариант. Если же объект состоит из единственного метода или двух-трёх (т.е. у вас маленькая программа) или у вас большая программа с кучей разных объектов - вероятно, вы захотите создавать объекты по требованию.

Ну, наш пример совсем прост, поэтому второй случай будет в тему:
procedure TForm1.Button1Click(Sender: TObject);
var
  Worker: TWorker;
begin
  Worker := TWorker.Create;
  try
    Worker.ProcessFile(Edit1.Text { и другие параметры, если надо });
  finally
    FreeAndNil(Worker);
  end;
end;
Если же вы выберите первый способ, то FormCreate и FormDestroy, вероятно, будут хорошими кандидатами для помещения в них создания и удаления объекта:
type
  TForm1 = class(TForm)
  ...
  private
    FWorker: TWorker;
  end;

...

procedure TForm1.FormCreate(Sender: TObject);
begin
  FWorker := TWorker.Create;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  FWorker.ProcessFile(Edit1.Text { ... });
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  FWorker.КакойТоДругойМетод({ ... });
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FWorker);
end;
К сожалению, у меня нет возможности дать развёрнутый пример большой и запутанной программы, где преимущества подобного подхода были бы очевидны. В частности, в примере выше может быть не ясно:
  • Чем объект лучше простой процедуры ProcessFile?
  • Чем метод ProcessFile в TWorker лучше метода ProcessFile в TForm1?
  • И т.д....

Какой код должна выполнять форма?

Разумеется, не весь код из обработчиков событий нужно выносить в рабочие объекты. Какой код нужно оставить? В форме должен остаться код, который манипулирует самой формой (в общем случае: UI вообще) и вызывает методы рабочих объектов. Например:
procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.Enabled := False; // не должно находится в TWorker,
                            // т.к. это user interface, а не логика программы
  try
    FWorker.ProcessFile(Edit1.Text { ... });
  finally
    Button1.Enabled := True;
  end;
end;

На самом деле...

А на самом деле я рекомендую вам почитать банду четырёх о шаблонах проектирования. Я действительно рекомендую это сделать, потому что эта заметка - очень краткое и упрощённое изложение, чья цель - дать начальное представление, а не служить мануалом. В частности, я ничего не сказал, что делать, если вашему рабочему коду нужно взаимодействовать с пользователем (показать сообщение или спросить дополнительные данные).

Читать далее: "Дружественность" в Delphi.

39 комментариев :

  1. К сожалению, этот пост весьма актуален =)
    Даже выделен анти-паттерн такой http://ru.wikipedia.org/wiki/Магическая_кнопка

    ОтветитьУдалить
  2. Еще небольшое дополнение:
    Если допустим метод FWorker.BlaBla() выполняется длительное время, и пользователя надо осведомлять о процессе выполнения, то классу TWorker следовало бы назначить события, например:
    OnMsg(const Msg: string);
    А в форме написать обработчик события с выводом сообщения в Label, Memo, etc.
    Таким образом:
    1. TWorker по-прежнему ничего не знает про форму и не зависит от интерфейса. ему по барабану как его сообщение будет обработано, и будет ли обработано вообще.
    2. Форма сама определяет как ей отоброжать сообщение. Достаточно изменить код обработчика.

    ОтветитьУдалить
  3. А как лучше сделать взаимодействие с пользователем из такого класса, или допустим создание других форм. Можно пример?

    ОтветитьУдалить
  4. Как я уже сказал - это просто введение, а не руководство к действию.

    Тем не менее, один из ответов на этот вопрос уже дал G-Host выше: заведите у TWorker событие (callback). Когда вам нужно в TWorker взаимодействовать с пользователем - вы вызываете событие.

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

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

    ОтветитьУдалить
  5. такой подход - первый шаг к выполнению программы в асинхронном виде. Никто не мешает сделать TWorker классом, способным выполняться в отдельном потоке, с последующим уведомлением главного потока о завершении задачи.

    К тому же такой подход ведет к изучению вариаций MVC, MVP, и ище с ними - там задачи каждого блока (модели, представления, контроллера, презнетера) - расписаны. К сожалению, живых и интересных библиотек или фреймвоков для дельфей не наблюдал. только в области ORM/OPF что то теплиться, а в области MVC - глухо. Видимо, разбаловала народ визуальная среда разработки ... если кто то даст ссылки на интересные фреймвоки - буду признателен.

    ОтветитьУдалить
  6. Народ не разбаловал кто-то, а развратил подход к созданию глубоких иерархий классов. Это порок, с которым вынужден мириться любой программист на дельфи, ибо так написана VCL.

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

    Поддерживаю полностью автора поста - GoF (http://www.ozon.ru/context/detail/id/2457392/) обязателен к прочтению. Придется, конечно, поболеть после прочтения болезнью вставлять в любые дыры Мосты и Посредников, но это, как я думаю, полезно.

    Еще можно на библиотеку Swing из Java (я смотрел для версии Java 1.4), чтобы понять, как можно организовать гибкость GUI без многоуровневого наследования. Хотя к Swing'у у проф. разработчиков немало претензий, но, я думаю, что ознакомиться будет полезно.

    ОтветитьУдалить
  7. Могу добавить, что для выделения бизнес логики удобно собирать процедуры в классовые методы, типа:

    TWorker = class
    class ProcessFile;
    end;

    Делаем вызовы:
    TWorker.ProcessFile;

    Отпадает необходимость создавать и чистить класс. Читаемось повышается.

    Есть еще хитрый прием создания абстрактного класса с бизнес логикой в виде классовых методов. Создание его потомков и использование
    нужного класса в зависимости от ситуации. Этот метод широко использует DevExpress для реализации различных видов прорисовки.

    ОтветитьУдалить
  8. Во многих ситуаций имеет смысл делать полноценный класс, потому что вы можете инициализировать его с параметрами (в конструкторе), а также хранить промежуточные результаты или параметры в экземпляре класса - что не получится с классовыми методами.

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

    Поэтому в виде классовых методов реализуются только относительно простые вещи.

    ОтветитьУдалить
  9. Для понимания необходимости MVP надо нарваться на второй и третий подводные камни:
    1. Хочется на одни и те же формы вешать разные модели (Workers).
    2. Логику интерактивного редактирования также желательно вынести из формы в невизуальный редактор (Presenter), оставив ей лишь отображение данных и получение информации и команд от пользователя.
    Фреймворк станет побочным продуктом первой же MVP-реализации крупного проекта. Собственно на работе зарождение такого и наблюдаю.

    ОтветитьУдалить
  10. По поводу классовых методов нужно немного изменить подходы к разработке и Вы получите дополнительную гибкость. Объекты с классовыми методами можно наследовать, как любые другие. По поводу данных: они должны приниматься в виде параметров метода.

    Простые вещи? Все гениальное просто...

    ОтветитьУдалить
  11. Иными словами, это означает:

    Было:

    type
    TObj = class
    FField: Integer;
    procedure DoSomething;
    end;

    Стало:

    type
    TObjParams = class
    FField: Integer;
    end;

    TObj = class
    class procedure DoSomething(Params: TObjParams);
    end;

    Понятно, что когда параметр 1 - его и передать напрямую можно. А когда много - получается ещё больше кода, чем было.

    Вероятно, такой подход будет удачно смотреться только в некоторых случаях - когда параметров мало, либо же параметры принадлежат другому объекту (так что мы можем передать этот объект за раз). Частным случаем второго варианта является случай, когда нам надо разделять один набор параметров между несколькими объектами.

    ОтветитьУдалить
  12. Классовые методы удобны при необходимости изменить поведение объекта. Например, функции рисования объекта в скинах можно засунуть в отдельный класс и пользовать нужный. А данные, которые передтся классу - это отскиненный объект.

    ОтветитьУдалить
  13. Как только рисовальщику понадобятся собственные данные, то методы класса сделают ручкой. Всяко лучше сразу работать не с классами, а с объектами. Единственное исключение - большие коллекции, где затраты на создание-удаление объектов велики.

    ОтветитьУдалить
  14. @bonart: ну - не совсем представляю, зачем рисовальщику могут потребоваться собственные данные кроме всех имеющихся у отскиненого объекта данных. Впрочем, никто же не говорил, что наличие классовых методов убивает необходимость создавать объекты. Просто можно принять к сведению, что набор логически связанных процедур и функций можно связать пучком в классе и обозвать классовыми методами - и применять по необходимости, в дополнение, а не вместо объектов.

    ОтветитьУдалить
  15. До ВСЕХ имеющихся данных рисовальщику вообще не добраться - только до публичных.
    Вязать пучком процедуры в псевдоклассы не нужно - слава богу не Ява. А использование класса вместо объекта во-первых хуже читается, во-вторых не дает использовать данные экземпляра, в-третьих, не имеет никаких преимуществ, кроме мизерного - минус одно создание-уничтожение объекта.

    ОтветитьУдалить
  16. В общем, можно сойтись на том, что классовые методы в объекте-заглушке, принимающие параметром другой объект - это что-то вроде класс-хэлперов для этого второго объекта.

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

    ОтветитьУдалить
  17. Я решил вступиться за классовые методы.
    Думаю, что у многих бывают проблемы с классификацией утилитарных функций. Вот надо тебе спец. функцию. А она ну никак не подходит целиком в какой-то класс. Безусловно, ее можно побить на несколько простых функций. Но это так удобно - одна непонятная функция используется 1000 раз в программе и экономит много строк кода. Куда ее девать? Как ее называть? Куда помещать?

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

    Т.е. статические классовые методы - хорошее средство для временных утилитарных функций, которые не знаешь куда деть сейчас, но надеешься, что придумаешь им место потом.

    ОтветитьУдалить
  18. Я бы развил защиту классовых функций. Во первых, у них есть возможность стать виртуальными. Во вторых, они объединяются единым классом-это важно, например, для описания скина. То есть классовые функции не просто объединены в класс, а при этом связаны некоей общей концепцией - например, отрисовать управляющий элемент в определенном стиле. Или сюда могут подойти классы, описывающие API (directX/d2d).

    Важное замечание про то, почему сложно создать объект. Основная сложность при создании объектов-это определение времени его жизни и своевременное удаление. В этом и есть сложности. Поэтому классовые методы в этом случае упрощают ситуацию.

    ОтветитьУдалить
  19. Если возникают сложности со временем жизни объекта - значит настала пора использовать заменить прямую ссылку на него интерфейсной.
    Класс как эрзац-пространство имен использовать не люблю - нечитабельно и нерасширяемо.

    >классовые методы в объекте-заглушке, принимающие параметром другой объект - это что-то вроде класс-хэлперов для этого второго объекта.

    Разве не проще передать этот другой объект один раз как параметр конструктора?

    ОтветитьУдалить
  20. >>> Разве не проще передать этот другой объект один раз как параметр конструктора?

    От ситуации зависит. Вы же не создаёте классовый хэлпер перед использованием - а просто используете его. Так же и тут.

    ОтветитьУдалить
  21. совсем не понимаю предмета разгоревшегося спора!))

    есть классовые методы, которые можно использовать в определенных ситуациях. Какой смысл спорить с тем, что они не нужны? Да, решение не расширяемое в плане добавления локальных данных для класса - прийдется создавать объект. Но не во всех случаях нужно иметь расширяемое решение - чтобы не наступить на грабли переинжиниринга.

    Имеет смысл разобрать конкретную ситуацию использования классовых методов и указать - какие альтернативные подходы лучше использовать и почему.

    ОтветитьУдалить
  22. >> Имеет смысл разобрать конкретную ситуацию использования классовых методов и указать - какие альтернативные подходы лучше использовать и почему.
    По мне у чисто классовых методов множество приемлемых применений пустое. Дело в том, что они, как и хелперы, вносят явные ограничения, устранение которых требует крупных переделок. Но в отличие от хелперов, методы классов нельзя использовать с немодифицируемым чужим кодом. В результате все преимущество методов классов перед объектами в отсутствии явных инициализации-финализации. Это экономит пару строк кода на стороне "клиента", но делает громоздким код "исполнителя" (инициализацию-финализацию вместо конструктора и деструктора приходится размещать прямо в методе класса). Полагаю это недостаточной компенсацией за отсутствие своих данных.

    ОтветитьУдалить
  23. К сожалению в посте, да и в его обсуждении, практически не упоминаются интерфейсы (IInterface), а между тем это очень мощный инструмент при проектировании приложений.
    Лично я вообще стараюсь придерживаться следующих принципов (грубо):
    1. Со всеми более или менее значимыми объектами в системе работаем только через соответствующий интерфейс.
    2. Объект реализющий тот или иной интерфейс создается только посредством фабрик.
    Например:
    CoMyObject = class
    class function Create(const AParam: IMyParam): IMyObject;
    end;
    3. Четко разделять бизнес-модель (т.е. объектную модель) и дополнительные функции типа аутентификации, авторизации, логирования и пр.

    Таким образом для реализации сущности предметной области создаются 4 модуля:
    1. untMyObjectInterface - объявление интерфейса
    2. untMyObjectClass - объявление класса реализующего интерфейс
    3. untMyObjectCoClass - описание фабрик (см. паттерн "Фабрика"), создающих экземпляр класса реализующего интерфейс
    4. untMyObjectHelper - описание сервисов (т.е. вспомогательных процедур, классов и пр.) для работы с этим интерфейсом

    Выгоды:
    - Описание бизнес-модели четко отделено от ее реализации;
    - При помощи фабрик легко меняем реализацию интерфейса (путем создания экземпляра другого класса);
    - Весь служебный код оформлен отдельно и не "захламляет" реализацию объектной модели;
    - Громадный простор для рефакторинга не только отдельных методов, но и целых классов;
    - Маcштабируемость. Создаем в фабрике другой класс и вот мы уже работаем не с Oracle, а с MS SQL Server (или вообще с WEB-сервисом);
    - Модульное тестирование;
    - Ну и как дополнительный плюс - это сборщик мусора;

    ОтветитьУдалить
  24. Я напомню, что этот пост написан "открыть глаза приверженцам Магической Кнопки". Вы слишком многого хотите от простой заметки.

    ОтветитьУдалить
  25. Вы слишком суровы. Комментарии к посту постом не являются. Это уже дискуссия по проблеме, которая вполне может открыть "приверженцам" глаза еще шире, т.к. в одном месте смогут увидеть несколько разных подходов. От себя добавлю, то поскольку VCL - библиотека компонентов, то обозначенную проблему можно решать при помощи компонентов, при этом GUI-решениях (на основе TForm) нет необходимости описывать инициализацию объекта (ну в определенном смысле, ведь, например, при использовании таймера его конструктор не прописывается в коде, а инициализация осуществляется при помощи свойств, правда обработчик OnTimer страдает болезнью, описываемой в посте), и связывать компоненты можно будет при помощи инспектора объектов...
    Об этом давным давно писал дятька Рэй Конопка.

    ОтветитьУдалить
  26. Еще неплохо бы было написать пост с описанием и сравнением существующих фреймворков ORM, DAO (Data Access Object), Repository, Аналога LINK, аспектов и пр. для Delphi.

    Информации в сети крайне мало, по крайней мере на русском языке. Или я просто искать не умею :)

    ОтветитьУдалить
  27. Эээээээ..... ну напишите.

    Какое я имею отношение к, скажем, ORM? Никакого.

    ОтветитьУдалить
  28. Насколько я понимаю, исходя из темы топика и жажды знаний про ORM, можно направить в сторону MDA (есть книжка на русском!) и Bold for Delphi (на английском точно что-то есть). Была еще одна штука (попроще, чем MDA), но название я уже не помню.

    ОтветитьУдалить
  29. VCL однопоточна? Вот теперь я понимаю, почему у приверженцев одной кнопки программа виснет почти всегда, когда используется sleep...

    ОтветитьУдалить
  30. Странный комментарий. А VCL тут при чём? Вызов Sleep в UI-потоке - это диагноз.

    ОтветитьУдалить
  31. Вызов Sleep в UI-потоке - это диагноз.
    Это простой способ наглядно показать, почему надо разносить код формы и полезный код. К сожалению, взят из практики.

    Забавно, но стоит заменить TForm на TObject(а еще лучше - просто дописать IWorker к TForm) в объявлении формы и, после удаления ненужных полей, готов чистый TWorker. Это несколько умаляет значимость доводов о требованиях начальника.

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

    ОтветитьУдалить
  32. >>> Забавно, но стоит заменить TForm на TObject(а еще лучше - просто дописать IWorker к TForm) в объявлении формы и, после удаления ненужных полей, готов чистый TWorker.

    В таком простом примере - да. Но это всего лишь пример.

    А в реальном проекте, написанном на принципе волшебной кнопки, сделать так будет очень сложно, потому что UI и рабочий код перемешаны в нём страшнейшим образом. В особо запущенных случаях будет проще переписать код заново.

    ОтветитьУдалить
  33. Интересно, у какого процента дельфи-программистов проект перестанет компилироваться если перенести дефолтную глобальную переменную класса формы(var Form1: TForm1) в dpr?

    ОтветитьУдалить
  34. перенести глобальную переменную формы(var Form1: TForm1) в dpr?

    Не могу даже представить себе, что дельфя в таком случае перестанет компилировать (Delphi7).

    ОтветитьУдалить
  35. >>Не могу даже представить себе, что дельфя в таком случае перестанет компилировать (Delphi7).
    Так прикол в том что если есть ссылки на "Form1" в модулях проекта то перестанет. Эта переменная нужна для Application.CreateForm, не более. А я у многих видел обращение к главной форме из глобальных подпрограмм и других классов через эту переменную. И это как раз признак привязки к интерфейсу. Так что, это можно считать маленьким тестом архитектуры =)

    ОтветитьУдалить
  36. Допустим я использую процедурный подход, как мне в реализации процедуры не ссылаться на объект Form1 чтобы добраться до свойств и методов объектов расположенных на форме, скажем тотже clHttp1 расположенный на форме:

    procedure GetHtml(URL: string);
    var
    html: TStrings;
    begin
    html := TStringList.Create;
    try
    Form1.clhttp1.Get(URL, html);
    finally
    html.Free;
    end;
    end;

    вот не хочется мне создавать объект динамически и настраивать его свойства при создании, есть ли выход?

    И ещё вопрос, когда необходимо определять свои процедуры и функции в классе формы, а когда это запрещается?

    ОтветитьУдалить
  37. procedure GetHtml(HTTP: TIdHTTP; const URL: string);

    Кому надо - сам передаст. Может с формы, а может - создаст сам, динамически.

    Но бред какой-то получается. В чём смысл существования процедуры GetHtml, если по своей сути она является методом к объекту, только что вынесенному отдельно в виде процедуры? Так и делать её надо методом, а не процедурой.

    А так получается, что постановка задачи не корректна: вы изначально делаете "неправильное" действие (вынос метода объекта в автономную процедуру), а потом спрашиваете: а правильно-то как? А правильно-то было этого не делать.

    >>> вот не хочется мне создавать объект динамически и настраивать его свойства при создании

    Если вопрос про лень - то тут или-или. Если ленимся - то передаём IdHTTP извне. Если делать правильно - то делаем наследника IdHTTP или декоратор. В промежутке между этими вариантами - создание IdHTTP в GetHtml (кстати, можно же не IdHTTP, можно WinInet).

    >>> И ещё вопрос, когда необходимо определять свои процедуры и функции в классе формы, а когда это запрещается?

    Ответ на вопрос зависит от: кем запрещается?

    Если это был вопрос про создание хорошего кода, то тут так просто не ответить. Могу посоветовать взять в руки GoF или Макконнела.

    Если кратко:
    - Форма должна быть единым объётом, чёрным ящиком
    - Манипулирование компонентами формы должно производиться самой формой (её внутренними методами)
    - Наружу должна выставлять методы по манипулированию собой
    - Каждый метод должен выполнять одну задачу. Если метод выполняет две задачи, его нужно разбить на два
    - Логика программы не должна содержаться в форме

    Как-то так.

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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

Примечание. Отправлять комментарии могут только участники этого блога.