Ефективний спосіб застосування інтерфейсів в MDI додатках

У книгах з програмування при розгляді різних прийомів і методів наводяться в основному «іграшкові» приклади. Іноді навіть можна зустріти висловлювання автора: «професійні програми так не пишуть ». На самому початку вивчення сучасного об'єктно-орієнтованого програмування я не замислювався над тим, що значить писати професійно. Задумався, коли став писати масштабний проект. У цій статті хочу поділитися своїм досвідом – описати кілька своїх рішень.

Спочатку ставилося завдання: розробити модель для побудови додатків, орієнтованих на роботу з базами даних (БД). Під таким додатком мається на увазі набір форм, кожна з яких зазвичай відображає одну таблицю БД. Наприклад, в бухгалтерській чи складської програмі таблиці «Накладні», «Клієнти», «Товари» зручно розташувати на окремих формах. Кілька таблиць з малим числом рядків і стовпців можна було б розташувати на одній формі, наприклад: «Категорії товарів», «Типи накладних», «Одиниці виміру». Користувач повинен мати можливість вибирати вікно, з яким він хоче працювати. Тому десь має бути меню або список всіх або майже всіх вікон. Зрозуміло, що вікно «Накладна» в цьому списку відсутня. Воно буде відкриватися зі списку накладних (вікно «Накладні»). Було б так само зручно відкривати останню прибуткову накладну (вікно «Накладна») для товару під курсором з вікна «Товари». Ось для таких додатків і призначена описана в статті модель.

Модель програми можна звести до абстракції «Вікно-> Документ», де Вікно – це список Документів, наприклад «Вікно-Накладні» -> «Документ-Накладна». Щось схоже на модель «Master-> Detail», тільки на різних формах (у нас). У свою чергу Документ може бути Вікном, з якого можна відкрити інший Документ і т.д., тобто знову «Вікно-> Документ». Наприклад «Вікно-Накладна» -> «Документ-Клієнти». І за великим рахунком, чим відрізняється Вікно від Документу? Адже зв'язок може бути і зворотною: Документ-> Вікно. Під зв'язком розуміємо будь-яку дію, ініційоване з поточного вікна (форми) по відношенню до іншого вікна (формі). Ця дія навіть може і не вимагати відображення того іншого вікна. Тому модель можна спростити ще: «Документ <=> Документ». Іншими словами – багато вікон з безліччю зв'язків між ними.

Модель буде розглянута на прикладі Delphi, але може бути реалізована і на інших об'єктно-орієнтованих мовах мають такі конструкції, як класи, спадкування та інтерфейси. Модель побудована на основі багатовіконного інтерфейсу MDI. На Рис.1 зображено кілька рівнів ієрархії класів форм. Початковий, найбільш абстрактний рівень – рівень платформи. Під платформою розуміється бібліотека абстрактних класів та універсальних функцій. На цьому рівні розташовані два базових класу – клас головної форми TBaseMDIForm і клас дочірньої форми TBaseMDIChildForm. Якщо ми пишемо програму складського обліку (для абстрактного замовника), переходимо на інший рівень шляхом успадкування (пунктирні стрілки) необхідних форм від відповідних базових класів. Це я називаю рівнем схожих проектів. Тут міститься вся функціональність вікон конкретного проекту для абстрактного додатки. З цих вікон вже можна будувати повнофункціональне додаток. Але конкретне додаток для конкретного замовника будується з вікон наступного рівня – рівня конкретного застосування. На цьому рівні може бути дещо змінений зовнішній вигляд вікон, перевизначені деякі методи і функції під конкретного замовника. Для більшої ясності наведено Рис.2. Якщо ми пишемо програму для бухгалтерії з базою даних, відмінною від бази даних в програмі складського обліку, то ми переходимо з рівня платформи шляхом успадкування на рівень схожих проектів 2, тобто це буде паралельна гілка. І т.д.


Зв'язки між вікнами (Рис.1) показані суцільними лініями. Оскільки основна функціональність вікон знаходиться на рівні схожих проектів, усі основні зв'язки між вікнами теж. І зараз виникає цікаве питання: як правильно організувати ці зв'язки? Якби ми будували додаток з вікон цього рівня, все було б добре – кожне вікно «знало» б про інших вікнах (класах форм) з секції uses. Але ми щось будуємо додаток із спадкоємців цих вікон. Виходить складна ситуація – спадкоємці повинні «знати» про спадкоємців. Тобто частина функціональності, загальною для ряду замовників, повинна піти на рівень конкретного програми для конкретного клієнта. Це неприпустимо, тому що втрачається перевага об'єктного програмування. Не будемо ж ми кожного разу після змін основної функціональності копіювати програмний код між сусідніми гілками рівня конкретного застосування. Ось тут може допомогти використання інтерфейсів (спеціальна конструкція мови). Можна створити окремі інтерфейси для всіх класів вікон з потрібними властивостями, функціями і методами. Тоді вже вікнам буде нікому «знати» один про одного. Їм потрібно буде «знати» тільки про інтерфейси, які реалізують потрібні класи вікон. Отже, зв'язки між вікнами будуть знаходитися там, де і належить, а спадкоємці вікон будуть нести тільки функціональність для конкретного додатка (замовника). І при необхідності зможуть мати свої зв'язки до інших вікон (використовуючи інтерфейси), яких не передбачено на рівні вище.

Одне з рішень виглядає так. Паралельно зі створенням функціональності безлічі вікон треба паралельно створити для кожної групи зв'язків свій інтерфейс, що містить потрібні функції, властивості, методи. А при виклику інтерфейсу треба перебрати всі вікна в додатку, знайти те, яке реалізує потрібний інтерфейс, потім викликати потрібну функцію (властивість). Оскільки функція (властивість) інтерфейсу може викликати із багатьох місць, ніхто не заважає автоматизувати цей процес шляхом створення якогось універсального механізму пошуку потрібного інтерфейсу серед існуючих і неіснуючих (класів) вікон. Справа в тому, що вікна з потрібним інтерфейсом у момент його пошуку може ще не існувати. Ми не збираємося при запуску програми створювати одразу всі можливі вікна. Адже користувач може взагалі не скористатися багатьма вікнами та їх інтерфейсами в даному сеансі роботи з програмою. Припустимо зараз, знайдено існуюче вікно «Документ», що реалізує зв'язок «Відкрити певний документ». А раптом користувач виробляв там редагування і не закрив його (відклав на якийсь час). Якщо ми дозволимо створити зв'язок з цим вікном, воно вже повинне буде відображати інший документ і всі проведені користувачем зміни можуть пропасти. Значить, необхідний якийсь критерій, що дозволяє універсального механізму пошуку визначати – чи можна встановити зв'язок з вікном, або треба створити інше вікно того ж класу.

Пропонується спосіб вирішити всі вищевказані складності дуже простим механізмом. В абстрактній моделі «Документ <=> Документ» є тільки один об'єкт – Документ. Тому досить використовувати тільки один інтерфейс (IDoc) з однією функцією (ProcessParams), аргументом якої буде масив з будь-яким числом елементів будь-якого типу. Спосіб обробки цього універсального параметра визначає сам програміст без залучення інших інтерфейсів, успадкування, функцій-оболонок. За допомогою такого універсального параметра можна організувати створення великої різноманітності зв'язків між формами. Інтерфейс IDoc буде реалізуватися на рівні платформи класом TBaseMDIChildForm. Тому всі спадкоємці від цього класу автоматично реалізують цей інтерфейс. Оскільки функція ProcessParams повинна бути універсальною, тип єдиного параметра (Params) використовуємо array of const (array of TVarRec) – масив з будь-яким числом членів будь-якого типу. Таким чином, ми зняли необхідність додавати новий інтерфейс для кожного нового класу форми (або набору дій) і додавати в нього нову функцію при створенні нової зв'язку між формами. Інтерфейс IDoc ми будемо викликати не безпосередньо, а за допомогою допоміжного об'єкта DocManager. При запуску програми ми реєструємо (RegisterFormClass) у DocManager класи всіх необхідних вікон конкретної програми. Реєстрація здійснюється із зазначенням номера класу та заголовка форми. Номер класу унікальний для гілки рівня схожих проектів (Рис.2). Заголовок форми необхідний, тому що передбачається автоматично створювати меню зі списком вікон без необхідності відразу створювати всі вікна. При організації зв'язку з іншим вікном будемо користуватися функціями ShowDoc і ProcessDocParams. Як параметри для цих функцій потрібно задати номер класу і параметр типу array of const (Params). Тому для зв'язку з іншим вікном дане вікно повинне «знати» тільки номер класу. Посилання на клас (викликається форми) і інтерфейс IDoc не потрібні. ShowDoc відображає вікно з передачею в нього потрібного параметра. ProcessDocParams організовує обробку параметра без необхідності відображати вікно (у фоновому режимі). Обидві функції створюють при необхідності вікно потрібного класу і потім викликають ProcessParams (IDoc) створеного вікна.

Цей механізм дуже нагадує технологію COM в ОС Windows, тільки всередині однієї програми.

Розглянемо один з випадків застосування вищезазначеного принципу. Зі списку накладних (вікно «Накладні») ми хочемо побачити вміст накладної під курсором. Для цього ми викликаємо ShowDoc із зазначенням номера класу. Як параметр Params масив, один з членів якого є унікальним номером накладної зі списку накладних. DocManager створює вікно «Накладна» і передає туди масив Params з номером накладної (і ін параметрами при необхідності). У вікні «Накладна» з цього номеру ми завантажуємо список товарів відповідної накладної. А що буде, якщо користувач не закривши це вікно, повернеться до списком накладних і знову ініціює відкриття вікна «Накладна»? Тут можливо два випадки – користувач хоче переглянути вміст тієї ж накладної або він хоче переглянути вже іншу накладну. Для таких випадків існує ось який механізм. IDoc має допоміжні процедури SetParams для збереження Params у формі і ParamsIs для визначення ідентичності з Params, збереженим через SetParams. При виклику DocManager.ShowDoc якщо знайдено вже існуюча форма потрібного класу, відбувається виклик ParamsIs для перевірки рівності Params з ShowDoc і Params існуючої форми. Якщо вони рівні, показуємо існуючу форму на передньому плані, якщо Params `и не рівні, то створюємо нову форму на передньому плані з передачею туди нового Params.

У формі TBaseMDIChildForm після виклику SetParams відбувається збереження Params не у вигляді array of const, а у вигляді динамічного масиву типу Variant. Конвертація відбувається функцією VarOpenArrayToVarArray в модулі Misc. Там же є функція VarEqual, яка викликається з ParamsIs. VarEqual і VarOpenArrayToVarArray побудовані спеціальним чином, який визначає ступінь свободи завдання елементів масиву Params типу array of const. У ньому можна задавати елементи практично будь-яких типів. Ординарні типи, посилання на об'єкти, адреси змінних з відповідним перетворенням при їх інтерпретації. Навіть можна задати як елемента динамічний масив типу Variant, елементами якого можуть бути теж масиви типу Variant. При цьому VarEqual буде працювати коректно (на основі рекурсії). Помічене обмеження – Неможливість передачі рядків String зі службовими кодами типу 0х0, 0х1, 0х2 і т.д. Нічого з цим поки зробити не зміг.

Ще кілька особливостей. ProcessDocParams не впливає на Params, збережений у TBaseMDIChildForm за допомогою SetParams (тобто з ShowDoc). ProcessDocParams не викликає ParamsIs і SetParams форми. ProcessDocParams і ShowDoc викликають допоміжні методи інтерфейсу IDoc DocInit і ProcessParams. Їх можна визначити у спадкоємців. DocInit призначений для ініціалізації форми, там можна відкривати таблиці БД, обробляти Params з ShowDoc. А ProcessParams призначений для обробки Params з ShowDoc і з ProcessDocParams.

У DocManager вбудований механізм заповнення пункту меню списком заголовків зареєстрованих класів форм з метою надання користувачу способу відкриття бажаної форми. Функція CreateMenuItems приймає параметр типу TMenuItem, де хочемо створити вищезгаданий список (Зазвичай це пункт головного меню головної форми). Причому паралельно автоматично заповнюється властивість об'єкта DocManager ActionList типу TActionList. Його можна використовувати для заповнення «вручну» (програмістом) альтернативний засіб вибору вікон не міняючи код TDocManager.

При реєстрації класу вікна (DocManager.RegisterFormClass) необхідно вказати додатковий параметр – це тип вікна. Є три типи «Вікно», «Документ» і «Звіт». При виклику CreateMenuItems все, що зареєстровано як «Документ» не входить до меню, а те, що позначено як «Звіт», потрапляє в кінець меню після роздільника. Передбачається, що «Документ» викликається з інших вікон (наприклад вікно «Накладна»), а кількість і порядок «Звітів» можуть часто змінюватися, тому в кінці. В якості пункту меню вибору доступних вікон зручно використовувати пункт головного меню головної форми.

DocManager створювати вручну не треба, створюється і знищується автоматично при додаванні в проект посилання на модуль Doc.

Деякі рекомендації щодо використання Params: array of const. Рекомендується першим елементом масиву використовувати ціле число – номер команди (зв'язку), досить зробити унікальним в межах класу форми на рівні схожих проектів і нижче. Т.ч. при виклику ShowDoc і ProcessDocParams, щоб потрапити в потрібне місце, вказуємо номер класу (TypeId: Integer), номер команди (Наприклад перший елемент Params: array of const). У потрібній формі в ProcessParams аналізуємо перший елемент масиву Value: Variant, в DocInit аналізуємо перший елемент масиву FParams: Variant (поле даних TBaseMDIChildForm). В інших елементах Params: array of const передаємо всі, що необхідно для зв'язку з іншою формою.

Розглянемо один окремий випадок застосування вищезазначеного принципу. Припустимо, що ми хочемо з декількох місць програми («Список документів» «Список товарів») відкривати вікно «Накладна», в якому знаходиться вміст відповідного документа. В якості параметра при організації зв'язку використовуємо унікальний номер накладної в рамках БД. Все б добре. Але є одне «але». Реальна ситуація – від загального батька «Абстрактний документ» наслідувало кілька конкретних: «Прихід», «Видаток», «Акт переоцінки». Це різні класи, що мають різні номери при реєстрації. Т.ч. безпосередньо викликати ShowDoc можемо але це не зручно, нам треба ще знати тип документа: «Прихід», «Видаток», «Акт переоцінки». Це щоб вибрати необхідний номер класу. Рішення у мене таке. Викликаємо вікно «Список документів» за допомогою ProcessDocParams, з передачею номера документа. У вікні «Список документів» в ProcessParams організуємо механізм запиту з БД типу документа за його номером. Далі викликаємо ShowDoc із зазначенням номера класу, який відповідає типу даного документа, і транслюємо туди ж номер документа (інший елемент масиву Params), отриманий від іншої форми через ProcessDocParams. Що у нас вийшло. Припустимо, користувач з «Списку товарів »хоче відкрити останній документ, що містить товар під курсором. Ним може виявитися як «Прихід», так і «Акт переоцінки». Після натискання наприклад він відразу побачить потрібне вікно, а як організований механізм його відкриття він може навіть і не здогадуватися. Ну а в «Списку документів» відкрити потрібний документ можна викликавши напряму «свій» ProcessParams або теж через DocManager (для одноманітності). Витончено, чи не так?

Додається робочий код рівня платформи [ZIP; 158 Кб], демонстраційний код рівня схожих проектів і конкретного застосування. Див коментарі у вихідному коді. Необхідно: Delphi 7, BDE. Після розпакування запустити Proj1Firm1.dpr, скомпілювати.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*