Давно уже задаюсь этим вопросом ;) Прочитав сообщение Cepгея Poщина »вопрос КС №58932« (от 29-01-2008 10:42) решил спросить у Вас. Как же все таки грамотно организовать связь (обмен данными) между несколькими модулями? Хотелось бы узнать не только про обмен данных, но и целесообразность разбиения на юниты, способы задания парамметров функции (записи, массивы в случае большого количества) и прочее, как бы смешно это не звучало. Просто мало опыта написания объемных программ и вот наконец дошел =) Поделитесь секретами или ссылкой на статьи!
Уважаемые авторы вопросов! Большая просьба сообщить о результатах решения проблемы на этой странице. Иначе, следящие за обсуждением, возможно имеющие аналогичные проблемы, не получают ясного представления об их решении. А авторы ответов не получают обратной связи. Что можно расценивать, как проявление неуважения к отвечающим от автора вопроса.
15-09-2008 08:07 | Комментарий к предыдущим ответам
Кстати, matvey таки написал статью
Да, читал конечно, спасибо ему, интересные идеи.
matvey:
Если данная схема реализации взаимодействия представляет интерес для народа, то в принципе для полного раскрытия вопроса необходимо писать статью с примерами кода.
14-09-2008 01:29 | Комментарий к предыдущим ответам
На самом деле, материалов в сети довольно много, натыкался на них, когда искал совершенно другие вещи, потому ссылки не сохранил. Смысл в том, что "внутренняя кухня" программы - то же, что интерфейс, только если интерфейс виден всем, то "внутренняя кухня" видна только программисту. Плохая организация программы отбивает напрочь желание работать с проектом дальше, развивать, совершенствовать, искать и исправлять ошибки. Но хорошая организация заставляет тянуться к проекту, в том числе и не только самого программиста, но и сторонних программистов (пользователи скорее всего в такую волочею вовлечены не будут, так как даже не подозревают, насколько важны пожелания по организацию интерфейса настоящему программисту, особенно если в команде нет дизайнера и тем паче если программист работает в одиночку). Пример тому - Linux с открытым исходным кодом - благодаря простоте своей внутренней организации над этим проектом работают миллионы программистов по всему миру. Потому Microsoft никогда не откроет исходные коды своих операционных систем, даже устаревших, вроде Windows 3.11, Windows 98/98/ME - просто забоится нападок со стороны мировой общественности о многочисленных недочетах. Даже в исходных кодах VCL, специально созданных для обозрения широкой публикой программистов и то обнаруживаются досадные "ляпы". В общем, я надеюсь, что вы прониклись идеей того, что внутренняя кухня - тот же интерфейс: разным людям нужен разный интерфейс, но все-таки определенные правила для большинства существуют. Предлагаю сформулировать ряд из них:
C тем, что количество глобальных переменных должно быть сведено к минимуму, знакомы многие. Но многие не понимают, почему это так. На самом деле, подобное ограничение связано с тем, что глобальные переменные могут меняться непредсказуемо - вызываемыми процедурами, например, или даже другими потоками. Именно поэтому цикл For не допускает для использования в качестве счетчика итераций глобальные переменные - потому что это грозит случайным изменением порядка выполнения работы и нарушением работы цикла. К примеру, возьмем знаменитый DecimalSeparator. Бог с ним, при использовании стандартных функций VCL ничего страшного не наблюдается. Но возьмем, например, некую процедуру MySuperProc из библиотеки стороннего разработчика. Пусть даже у нас будут исходные коды этой библиотеки - но при использовании процедуры мы считаем ее черным ящиком. Пусть до и после вызова мы выполняем действия, связаные с преобразованием чисел в строки и обратно. Мы НЕ МОЖЕМ гарантровать, что наш "черный ящик" оставил DecimalSeparator в том состоянии, в котором он был до вызова, хотя подсознательно пишем код, как если бы это было так. Неимоверная сложность отслеживания взаимодействий ДОЛЖНА ограничивать число глобальных переменных жизненно необходимым минимумом. Это не относится к глобальным функциям, вроде Printer, который часто путают с глобальной переменной. Эта не переменная - она не меняется, а возвращает значение "внутренней" переменной. Именно по этой причине рекомендуется использование свойств (property) в объектах (классах), а не полей. При изменении свойства мы можем оповестить все заинтересованные структуры через сеттер и сознательно РАСЧИТЫВАЕМ на возможность изменения. Если возможность изменения полей внутри выполнения процедуры недопустима, то необходимо создать их локальные копии, или деактивировать сеттер. Я для локальных копий использую префикс l, также как для private полей - префикс f, а для передаваемых в процедуры типа конструктора инициализирующих параметров - префикс a. Посмотрите функции, которые используют TFormatSettings. Пример приведен в »вопрос КС №33804«
Вред глобальных настроек могу продемонстрировать еще одним примером. Вот сейчас ушел на полчаса от компьютера, а когда вернулся - экран горит. Лезу в настройки электропитания, а там - правильно, стоит тема "Intervideo...". И это с учетом того, что проигрыватель я удалил несколько месяцев назад, а тему электропитания приходится менять каждую неделю. На вирус подозрений нет - Касперский от июня с.г. молчит как рыба, но неприятный осадок регулярно все-равно остается. Вот бы настроить все личные настройки и сказать: "А теперь пока я ЛИЧНО не разрешу - никому не трогать настройки". Но такого, увы, нету.
>>> Не бросаться с места в карьер
Точно. Не поддаваться настронию, что "я это за пять минут сделаю". Потом будете год расхлебывать то, что второпях наразрабатывали. Помните - каждый час, вложенный в проектирование, обернется несколькими днями экономии на этапе разработки. Экономить на разработке - себе во вред.
>>> Допустим нужно из главной формы вызвать модальную
Для этого все просто - создаем функцию, которая бы редактировала значение переданного параметра. Например, типа string. Возвращаемый результат - типа boolean, как раз - произошло ли изменение. Ваш пример будет работать гораздо разумнее при оформлении:
function GetSomeData(var S:string):boolean;
begin
Result:=false; // in case of failure
with TSomeEditForm.Create(nil) do try
Edit1.Text:=S; // вносим данные в поля формы
Result:=ShowModal=mrOk; // редактируем.
// При нажатии на Ok ДОЛЖНА проходить полная верификация данных.
// Только если она прошла, ModalResult может принять значение mrOk.
// Отмена должна проходить без дополнительных проверок.
if Result then begin
S:=Edit1.Text; // возвращаем в параметре значение
end;
finally
Free;
end;
end;
Вызов будет очень простым и понятным:
procedure Button1Click(Sender:TObject);
var S:string;
begin
S:=Table1.FieldByName('Family').AsString;
if GetSomeData(S) then begin
Table1.Edit;
Table1.FieldByName('Family').AsString:=S;
Table1.Post;
end;
end;
Вас больше не будут волновать проблемы, связанные с тем, что модули ссылаются друг на друга перекрестно - этого не будет никогда, модуль, в котором размещена GetSomeData никогда не узнает про модуль, в котором размещен Button1Click. Это ему просто не нужно. Мало того, такая процедура может импортироваться из DLL без каких бы то ни было ограничений, помимо передачи строк типа String, но их легко обойти, передавая PChar, или прочитав комментарий, вставляемый средой при генерации проекта DLL.
Вопрос, связанный с тем, что лучше передавать - список параметров, или одну запись или массив - сугубо риторический. В ряде случаев выбора вообще не остается. Когда Вы создаете поток средствами WinAPI (BeginThread на самом деле), то Вы обязаны либо уместить все параметры в один 32 разрядный параметр, либо передавать структуру. Во всех остальных случаях выбор между записью (массивом) и списком параметров диктуется личными предпочтениями программиста. Мне нравятся записи. Их легко копировать одной командой (например, с целью резервного хранения), их можно передавать в несколько разных процедур не раздувая кода. Например, пусть у нас есть программа для сложения двух чисел (трвиально, не правда ли?). Пусть мы хотим разнести ее на три модуля - ввод данных, расчет и печать отчета. Тогда при списке параметров мы будем иметь нечто такое:
if InputData(Data) then try
CountData(Data);
PrintReport(Data);
except
PrintException(Data);
end;
При использовании списка параметров код будет чуть длиннее:
if InputData(A,B) then try
CountData(A,B,C);
PrintReport(C);
except
PrintException(A,B);
end;
Здесь мы видим плюсы и минусы каждого метода. Плюс первого метода - во все процедуры передается одно и то же, с ним процедуры и работают. Это сродни объектно-ориентированному программированию, когда данные и обрабатывающий их код инкапсулирован в единое целое. На двух параметрах и одном результате это практически незаметно, но когда параметров станет много, плюсы этого метода становятся очевидными. Минус - передача лишних данных, например, в InputData неявно передается переменная С, которая будет хранить результат, хотя для ввода данных она явно излишня. А в PrintReport передаются входные параметры, которые там явно ни к чему (хотя с этим можно поспорить). Еще один минус - с точки зрения "черного ящика", у неправильно написанной процедуры в случае передачи списка параметров меньше шансов угробить входные данные - они будут передаваться как константы, тогда как при передаче записи подпрограмма может испортить все данные, или скомпрометировать их, если они являются секретными. Лучшим решением является комбинация этих двух способов. Настройки, с которыми работает подпрограмма, должны находиться в одном параметре (типа TFormatSettings), входные данные - в другом (передается как константа), а выходные - в третьем (передается как out параметр). Это обеспечивает достаточно хорошую защищенность от сбоев, хотя от компрометации данных и не защитит.
>>> Стоит ли передавать параметром компонент
Нет, это излишне жесткое ограничение. Желательно, чтобы процедуры обработки не знали, откуда данные, даже если понятно, что "из лесу, вестимо". Желательно обрабатывать не файлы, а потоки TStream, это обеспечит максимальную универсальность. А процедуру для обрабоки файла я обычно делаю с тем же именем, перекрытую (overload), чтобы упростить себе жизнь.
Это чисто мои мысли по поводу "красоты" написания программного кода, который потом было бы легко отлаживать и поддерживать. Если кого-то эта тематика заинтересует, я могу продолжить обсуждение.
2 Бел Амор
Спасибо, что обратили внимание на мой вопрос :) Очень понравился Ваш пример из »вопрос КС №50045«. Вы определенно поняли какой ответ я хочу получить на свой вопрос ;)
Ещё совет...
Достаточно часто приходится вводить что-либо в поля путём выбора из какого-либо списка. Часто для этой цели используют элементы с выпадающим списком вроде ComboBox'а. Я предпочитаю в таких случаях показывать полноценное окно со списком и кнопочками подтверждения и отмены. При этом возникает вопрос о возврате в вызывающую процедуру выбранного значения. Очень удобным является следующий способ:
1. В функцию выбора передаётся ссылка на целевой элемент. Обычно это поле редактируемого датасета. Передавать ссылку именно на поле очень удобно в том смысле, что, во-первых, в нём уже содержится исходное значение и можно спозиционировать курсор на элемент в списке, соответствующий текущему значению. Во-вторых, в него-же можно записать новое значение при подтверждении выбора прямо в функции выбора, ничего дополнительно не предпринимая на вызывающей стороне. И в-третьих, на вызываемой стороне ничего не надо знать о том, какому датасету принадлежит это поле, его имя и т.д., таким образом уменьшается количество передаваемых параметров и увеличивается универсальность.
2. Для тех редких случаев, когда на вызывающей стороне нужно знать произведён выбор или нет, функция возвращает значение типа Boolean.
Например:
procedure TForm1.Button1Click(Sender: TObject);
begin
ChooseCustomer(tblOrder.FieldByName('CustomerId'));
end;
Полностью рабочий пример, иллюстрирующий такой подход (даже с dfm) можно посмотреть здесь: »вопрос КС №50045«
Хотя в нём присутствуют некоторые корявости, но идею он вполне иллюстрирует, кроме того, там можно почерпнуть и некоторые дополнительные идеи, не относящиеся напрямую к данному вопросу...
Эксперимент провёл с одним связующим модулем, если грамотно продумать вполне можно использовать такой способ.
Допустим нужно из главной формы вызвать модальную, и получить результат в виде текста из поля Edit модальной формы, но не добавляя в Uses обоих модулей ссылки друг на друга. Вот что получилось:
// Добавляем новый модуль, пусть будет Unit3, и прописываем
// его в Uses Unit1 и Unit2
unit Unit3;
procedure TForm2.FormCreate(Sender: TObject);
begin
_GetText:=GetText;
_ShowForm:=ShowForm;
end;
procedure GetText(var Text: string);
begin
Text:=Form2.Edit1.Text;
end;
procedure ShowForm(var Text: string);
begin
Form2.ShowModal;
end;
// Unit1 - главная форма
procedure TForm1.Button1Click(Sender: TObject);
var
S: string;
begin
_GetText(S); // Просто вызываем процедуру из Unit3
ShowMessage(S);
end;
procedure TForm1.Button2Click(Sender: TObject);
var
S: string;
begin
_ShowForm(S);
end;
Как же все таки грамотно организовать связь (обмен данными) между несколькими модулями?
Как я понимаю...
Что есть объект? Объект есть некая сущность, представляющая данные и методы для их обработки. Обычно для реализации объекта используется отдельный модуль. Соответственно, если несколько модулей (объектов) используют одни и те же данные, то значит они используют один и тот же объект, или (в других терминах) модуль с данными (модуль данных). Исходя из этого, ИМХО, обмен данными между объектами можно и нужно реализовывать через отдельный модуль данных, а события об изменении данных передавать через отдельный объект линка (типа линка на datasource, а не через события обекта, чтобы можно было отослать событие нескольким заинтересованным модулям) на экземпляр модуля данных. Тогда внутри каждого объекта можно сделать свой линк на общий обект и ловить специфичные события. При таком раскладе как раз получается, что общая информация может быть типизирована и при её изменении реагировать на это изменение будут только те объекты которые должны обработать данное изменение.
Например, имеется глобальный параметр настройки приложения - размер шрифта в многострочном редакторе. Когда пользователь выбирает новый размер шрифта диалог просто устанавливает данный параметр в общем модуле данных, далее модуль данных пытается через даталинк оповестить все окна, но только окна, которые имеют многострочный редактор изменят размер шрифта. Таким образом, окно настройки не знает об окнах в которых есть многострочный редактор (те огромный плюс - объекты, взаимодействующие через модуль данных не знают друг о друге и деталях реализации объхектов, что позволяет легко их модифицировать (или добавлять/удалять) не боясь наступить на грабли, связанные с тем, что объекты "завязаны" друг на друга), но изменив параметр в общем модуле данных оно изменяет соответствующий вид всех окон внутри приложения.
Недостатки такого подхода есть. То, что я вижу:
1) Параметры прописываются в коде и набор параметров и соответственно методов обработки изменяется от приложения к приложению, те нельзя написать некий универсальный компонент.
2) Возникает необходимость регистрировать объект в каждом модуле для получения событий изменений данных, что приводит практически к обязательной перегрузке конструктора/деструктора объекта и дополнительному коду.
Однако, на данный момент времени плюсы такого решения лично для меня перевешивают минусы.
ЗЫ. Всё выше сказанное является моим личным мнением и не претендует на истину в последней инстанции.
ЗЗЫ. Если данная схема реализации взаимодействия представляет интерес для народа, то в принципе для полного раскрытия вопроса необходимо писать статью с примерами кода.
2 Cepгей Poщин
Все ниже написанное Вами истино, просто задавал вопрос для своего рода хинтов от гуру;)
Большая однако »тема на БП №383« обезательно пробегусь и полностью согласен с Вашим мнением:
>>>Может напишите статью на эту тему с примером реализации? Было бы интересно.
Возможно мне стоит конкретизировать то что меня интересует на данный момент, потому как вопрос слишком абстрактен для ответа, а сама тема заслуживает более детального расмотрения. Ну на счет деталей, это мое личное мнение. Люблю когда "все разложенно по полочкам"... хотя понимаю, что конечному пользователю не важно как и что у тебя написанно - главное чтобы работало =) 2 All Друзья мои, мы же в Королевстве Delphi! Именно поэтому нужно соблюдать правило хорошего тона не только в общении, но и при создании своих шедевров;) А этикету учат еще в школе, так почему бы Вам не научить окружающих?
Вы мне его рекомендовали как пример алгоритма обмена данными между модулями? Да.
Что еще можно порекомендовать.
Почитать http://www.delphikingdom.com/asp/talktopic.asp?ID=383
Смотреть исходники поставляемые с Delphi.
Умные книги прочесть.
Найти общие места и вынести их в отдельные модули.
Не бросаться с места в карьер, а предварительно думать о проекте в целом. Как он будет выглядеть через год-два? Как разбить его на такие модули, что бы при изменении функционала не пришлось править все модули?
Вопрос довольно концептуальный, типа "Четверостишья я уже научился писать, а как правильно написать поэму в стихах?!"
Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter. Функция может не работать в некоторых версиях броузеров.