ООП…рулит

Cерия статей простым и доступным языком рассказывает о использовании ООП применительно к базам данных.

По мере надобности можно дописать отчеты, в которых появилась необходимость. В некоторой точке вы начинаете писать код, который уже встречался в другом отчете. Код не одинаков «один в один», но имеет похожую логику. И однажды, вы понимаете, что имеете три разных отчета представляющих одни и те же данные в разных видах. Это является проблемой. Например, при изменении данных в таблице, вы будете вынуждены модифицировать три процедуры отчета и изменить три секции кода. При большом количестве процедур это настоящая проблема, можно что-то легко забыть или пропустить.

Сейчас есть лучшее решение такой задачи: ООП.

Кларион имеет достаточно средств для ООП. Для многих ООП — синоним слова «ABC», процедурное программирование — «Clarion (Legacy)». Многие избегают выбора ABC-шаблонов при создании нового приложения из-за ужаса «ручного кодирования». Но ты же программер, а не пользователь шаблонов.

Поддержка ООП появилась в 4-ой версии клариона (имеются ввиду ABC-шаблоны, возможность писать классы была еще и в CW20). Достаточно трудно понять принципы и преимущества ООП. Но когда ты поймешь, это будет как озарение.
Я начал писать свои классы, не используя ABC, и стало намного легче программировать и выполнять требования пользователей. Первый классы были написаны на Legacy и никак не были связаны с ABC.

ООП программирование мало распространено среди кларион-программистов. Не потому, что Кларион плохо поддерживает ООП, а потому что программисты не могут использовать ООП продуктивно.

Преимущества ООП:
— код более детализирован и более разбит на части. Маленькие кусочки кода выполняют большую работу. Они работают так, что уменьшается риск, что одна секция кода повторит работу другой
— код можно использовать несколько раз. Не в смысле «копировать» и «вставить» и даже речь не о использовании шаблонов. Речь о единожды написанном коде, который вызывается из разных мест в разных ситуациях. Правильно разработанный ООП код позволяет расширить свою функциональность изменив часть кода только в одном месте, т.е. нет необходимости просматривать всю программу целиком. Это также означает, что если вы сделали ошибку, то ее нужно будет исправить всего в одном месте.
— данные можно использовать несколько раз. Имеется ввиду, что объект может содержать данные, когда данные изменяются одной частью программы, другие части видят эти изменения, не прилагая к этому больших усилий
— разработка приложений командой разработчиков становится намного легче. Объекты позволяют работать только со своей задачей, не вникая в работу других программистов
— индивидуальная разработка облегчается. Отдельно взятый программист не очень любит работать в команде. Вы погружаетесь в задачу настолько глубоко, что можете забыть что было написано ранее, тем самым исправляя одну часть, вы можете задеть другую часть программы. ООП позволяет избежать этого
— после некоторого времени вы не знаете каким образом ваша программа работает. Т.е. вы начинаете с мелкой программы, содержащей необходимый код. Постепенно вы добавляете функциональность. Программа становится очень большой. Когда обнаруживается ошибка, вы исправляете маленький кусочек кода. Но если этот ваш кусочек кода взаимодействует с другим кусочком кода, т.е. два кусочка работают вместе, вы можете задуматься, а не надо ли вносить изменения во второй кусок кода. ООП позволяет избежать этого. Например, у вас есть объект, получающий список покупателей, вы просто используете его везде, и вам не нужно каждый раз беспокоится как получить эти данные
— вы начинаете думать совсем по-другому. Когда каждый объект уже имеет код, вы думаете об объектах, а не о коде. Например, вы написали отчет для печати списка клиентов, вы можете представить список как объект, содержащий набор данных о клиентах. Если вы один раз уже использовали объект для создания отчета, то в дальнейшем вам не нужно повторять процесс написания кода для получения данных о клиентах. И когда вы станете мыслить таким образом, то вы поймете, что объект, содержащий набор данных о клиентах, можно использовать для генерации страницы (Excel), для операции merge в Word, для создания html документа и т.д.

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

Комментарий переводчика

меня лично слова, сказанные выше, не убедили в большом преимуществе ООП;

я тоже написал свой первый класс на Legacy шаблонах;

хотелось бы, чтобы вы поняли, что ООП — это просто такой метод программирования и он удобнее, чем процедурное программирование, а написать можно все и тем и тем методом;

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

Для начала работы вы должны понять, что такое «объект» в Кларионе. Кларион объекты имеют две описания. Наиболее общее описание — это CLASS. Менее общее и менее понятное, это INTERFACE, который обычно не что иное, как другой путь использования класса. Остановимся для начала на CLASS-ах. Вначале вы должны понять, что такое классы, а уже потом, что такое интерфейсы. А вообще, работать можно только с классами, и забыть про интерфейсы.
Класс можно представить как мини-программу. В идеале, класс содержит всю функциональность и данные, в которых он нуждается для решения своих задач, и находится в изоляции от остальных ваших программ. Это в идеале, т.е. это пожелание, а не правило, так как в кларион программах вы зачастую нуждаетесь в доступе к глобальным данным и описаниям файлов, которые тоже являются глобальными для вашей программы.
Подумайте о классе как о мини-программе. Из чего состоит программа? Она имеет данные, map-стуктуру и процедуры. То же и с классом. Класс определяется следующим образом:

CustomerOrderClassType CLASS, TYPE                ! описание класса
Q                         &CustomerOrderQueueType ! ссылка на очередь
CustomerID                LONG                    ! номер покупателя
GetData                   PROCEDURE()             ! метод для получения данных о заказах покупателя
Construct                 PROCEDURE()             ! констуктор
Destruct                  PROCEDURE()             ! десктруктор
                       END

CustomerOrderQueueType QUEUE, TYPE                ! описание очереди, содержащей данные о заказах
CustomerID                LONG
CustomerName              LONG
OrderID                   LONG
OrderDate                 LONG
OrderAmount               DECIMAL(20,2)
OrderBalance              DECIMAL(20,2)
                       END

Класс имеет данные (принято называть их свойствами): Q и CustomerID. Эти данные принадлежат классу. Эти данные не принадлежат другой части программы, хотя другие части программы имеют доступ к классу, и имеют доступ к данным класса.
Класс имеет три процедуры.
Таким образом, класс имеет данные и прототипы процедур (MAP) — мини-программа.
Если убрать из рассмотрения, то, что классу необходимы данные о файлах приложения, то класс содержит всю необходимую информацию, которая нужна ему самому, в независимости от других частей программы. Это не означает, что класс не может использовать другие части программы, класс даже может использовать другие классы. Это также не означает, что класс разработан таким образом, что он не может быть использован из другой части программы и не может быть изменен для специфических нужд.
Обратите внимание, что Q — свойство класса и эта очередь имеет атрибут TYPE и определена ниже описания класса. Для того чтобы использовать очередь в качестве данных класса, она должна быть определена как ссылка на очередь с атрибутом TYPE (примечание: на самом деле атрибут TYPE не обязателен, но желателен, если вы не будете использовать CustomerOrderQueueType, то зачем вам засорять некоторое количество памяти). Очередь, которая является свойством класса, обычно создается в методе Construct и уничтожается в методе Destruct. Пример:

CustomerOrderClassType.Construct procedure()
  code
  self.q &= new CustomerOrderQueueType

CustomerOrderClassType.Destruct procedure()
  code
  dispose(self.q)

Методы с именами Construct и Destruct вызываются автоматически соответственно при создании класса и его уничтожении. Создание очередей (и классов) с использованием оператора NEW — это обычный метод Клариона.
Метод GetData будет получать информацию о заказах покупателя. Ниже пример возможного использования метода:

cc CustomerOrderClassType        ! определяется экземпляр класса (объект)
  code
  cc.CustomerID = loc:CustomerID ! инициализация свойства - номер покупателя
  cc.GetData()                   ! вызов метода

В этой точке (после выполнения метода) экземпляр класса «сс» будет содержать данные, которые вы запросили для покупателя с указанным номером loc:CustomerID. Вы можете прочитать полученные данные из очереди Q. Например, для того чтобы посмотреть имя покупателя:

get(cc.q, 1)
message('Customer Name: ' & cc.q.CustomerName)

Также обратите внимание, что, используя класс, вы не обращаете внимания, каким образом выполняется метод. Вы просто присвоили номер покупателя cc.CustomerID и выполнили метод cc.GetData(), вернувший данные. Каким образом данные были получены, каким способом, полностью скрыто от части программы, которая использует этот класс для своих целей. (Код метода GetData() пропущен, можете придумать его сами)
Каким образом может быть использован данный класс? Вы можете показать заказы покупателя в ListBox-е, напечатать их в отчете, создать html-файл, или даже преобразовать в другой формат. Если вы написали метод GetData() правильно, то во всех местах его использования он вернет одинаковую и верную информацию. Вот это означает, что код можно использовать много раз. Никакого «копировать» и «вставить».

Существует несколько механизмов написания многоразового использования классов. О них в дальнейшем.

Комментарий переводчика

Описание класса (его свойств и методов) и скрытие информации называется инкапсуляцией. IMHO — это простое определение и в тоже время это главная проблема в написании классов. Если у вас возникнет желание заглянуть и узнать, как же все-таки работает какой-либо метод класса, это может означать, что класс написан неверно (я не принимаю во внимание любопытность и желание самообразования). В правильно написанном классе никогда не должно возникнуть такого желания. Даже когда вы сами написали свой класс, а через некоторое время решили его использовать и решили посмотреть код — воспринимайте это как предупреждение, что что-то не так.

Высший уровень многоразового использования объектов (CLASS-ов) достигается нахождением классов в DLL. Ниже описаны основы: каким образом сказать компилятору и линковщику выполнить свою работу правильно в случае нахождения кода класса в DLL и для случая вызова класса, находящегося в DLL из exe-файла или другого DLL-файла.
Допустим, что имеется следующая ситуация. Вы создаете класс, код которого расположен в DLL-файле и этот класс может ссылаться на глобальные данные или процедуры, которые также расположены в этом dll-приложении.
Допустим, что dll-приложение имеет имя MyApp. Для того, чтобы описать класс, необходимо воспользоваться некоторыми дополнительными атрибутами:

MyClassType CLASS,TYPE,MODULE('myclasses1.clw'), |
                  LINK('myclasses1.clw', myapp_linkmode),DLL(myapp_dllmode)

Атрибут module — говорит, что в нем будет находиться код класса.
Атрибут link — указывает скомпилировать myclasses1.clw, если значение myapp_linkmode=TRUE и прилинковать модуль к текущему exe или dll файлу проекта. Если myapp_linkmode=FALSE — не компилировать, и даже не смотрим в этот модуль, и не линкуем его.
Атрибут dll говорит, что код класса находится в какой то внешней dll, а не в MyApp.
Если ваше приложение содержит код класса, то вы должны описать в глобальной секции embeds:

myapp_linkmode equate(1)
myapp_dllmode  equate(0)
include('myclasses1.inc'), once

В других приложениях, dll или exe, которые будут использовать этот класс, также в глобальных embeds-ах:

myapp_linkmode equate(0)
myapp_dllmode  equate(1)
include('myclasses1.inc'), once

а в свойствах проекта, необходимо будет добавить вашу dll в external library секцию (или через меню Application->Insert Module — > External Lib).
Также, присвоить значения вы можете в свойствах проекта (закладка Defines):

myapp_linkmode=>1
myapp_dllmode=>0

Но это обычно используется в hand code проектах, а так как обычно вы используете app, то рекомендуется описывать глобальные переменные.
Также, описание класса должно иметь строку принадлежности модуля — MEMBER. Так как мы решили, что наш класс будет использовать глобальные данные приложения, т.е. описание класса должно содержать строку:

  MEMBER('MyApp')

Если бы не было условия использования глобальных данных, то можно было бы написать просто MEMBER, т.е. не указывать приложение, данные которого мы хотим использовать.
Теперь плохие новости: так как вам необходимо написать один inc-файл и один clw-файл для описания и для кода класса соответственно, генератор приложений не окажет вам большую помощь для этого (Здесь имеется ввиду, видимо, что кларионовский редактор текста не особо удобен). Вы должны будете использовать файлы, которые не будут включены в дерево процедур. Также вы должны будете вносить правильную информацию в exp-файлы, чтобы линковщик правильно экспортировал описание класса для использование его в других приложениях.
Есть и хорошая новость: я написал шаблон, который позволяет вам видеть описание классов в дереве приложений и создавать inc и clw файл. Шаблон также обрабатывает inc-файл и вставляет необходимую информацию в exp-файл. Т.е. вы сможете работать с классами, также как ранее вы работали с процедурами.
Ограничение, которое не удалось победить: если у вас возникнет ошибка в коде класса, то вы не сможете сразу же ее исправить, необходимо будет закрыть окно с указанной ошибкой и зайти в код класса и там уже исправлять, иначе при повторной генерации эти же ошибки будут показаны вновь.
Скачать шаблон и пример его использования можно с OOP Sample (753). Архив содержит oop.app, в котором приведен пример использования шаблона. Минимум требуется C55 версия Клариона. Приложение не содержит словаря (последняя редакция примера содержит словарь) и использует baseball.mdb (MS Access), который я использую в моей серии Better SQL, используя одну из техник описанных здесь. Подробности работы примера, несколько позже.

Если вы ранее не скачивали пример OOP Sample (753),
то скачайте сейчас. Прочтение этой статьи без примера не имеет смысла.
Примеры в app демонстрируют несколько вещей.
Во-первых, показывается использование шаблона для создания «процедуры», в которой вы можете набрать свой код и по итогу получить INC и CLW файлы. Посмотрите на свойства процедуры Classes:1.


Вы можете ввести имена для генерации INC,CLW и INT файлов. В разделе «Embeds» есть точки вставки, в которых вы можете описать классы «Class Declarations» и написать код для них «Class Definitions». Также существует check box ExportClasses, при установке которого, происходит обработка inc-файла и описания экспортируются в exp-файл.
Посмотрите на описание класса TeamClassType. Вы видите описание очереди и переменной (они называются «свойствами»; вообще-то, там есть еще и другие описания переменных, о них будет рассказано позже). Также есть несколько процедур (их называют «методами»).
Ниже описания класса, находится описание очереди, имеющей атрибут TYPE. Класс содержит очередь с именем Q, которая является ссылкой на эту очередь с атрибутом TYPE. Так написано потому, что нельзя описывать в классе очередь напрямую — разрешено описывать только ссылку, а потом, в некоторой точке, использовать оператор NEW для создания очереди.
Примечание: Существуют «сложные» и «простые» типы данных. Например: LONG, STRING, BYTE, GROUP — простые типы, QUEUE, CLASS — сложные типы. В Кларионе принято, что внутри типов не могут быть использованы сложные типы данных напрямую, их можно использовать только через ссылки, а этого вполне достаточно. Примеры:

FirstQueue  QUEUE
MyField        LONG
            END
MyGroup     GROUP
Q              &FirstQueue
            END
SecondQueue QUEUE
Q              &FirstQueue
            END
MyClass     CLASS
Q              &FirstQueue
            END
 code
 MyClass.Q &= new FirstQueue
 MyClass.Q.MyField = 1
 add(MyClass.Q)

Ниже методов конструктора и деструктора, есть методы GetData() и ExportToCSV(). Метод GetData заполняет очередь данных. ExportToCSV метод экспортирует данные из очереди в CSV-файл (цель этого метода — показать содержимое файла в Notepad-е). Перед просмотром кода для этих методов я объясню общую суть для пониманию целой картины.

Процедура ShowTeams
Процедура ShowTeams — это «browse», хотя он и сделан при использовании шаблона «Source». Заглянем внутрь.


Обратите внимание, что в структуре окна в контроле list box не указан атрибут FROM. Когда вы используете очередь, которая содержится в классе, вы не должны использовать FROM атрибут, вы должны установить свойство Prop:From после открытия окна, как и сделано в коде.
В процедуре определен экземпляр объекта TeamClassType, названный «tc».
Данные для list box-а получаются одной командой:

 tc.getdata()

После идет обычный цикл ACCEPT для обработки событий окна.
Если вы нажмете кнопку «PRINT», то посредством кода, вызовется процедура PrintTeams, получающая класс «tc» как параметр. Это позволяет использовать класс и его данные, которые уже были получены, в процедуре, без повторного получения данных.
При нажатии на кнопку «Export» вызовется метод ExportToCSV, примерно так:

 tc.exporttocsv('myteams.csv')

Здесь обратите внимание, что процедура ShowTeams не знает о том каким образом будут получены данные, и она не знает о том, как эти данные будут экспортированы. Об этом знает класс, а процедура только говорит классу выполнить свою работу. Т.е. для использования функциональности класса вы только должны знать как взаимодействовать с ним, а не знать как он работает. Это ключ к пониманию возможности повторного использования класса.

Процедура PrintTeams
Обратите внимание, каким образом происходит вызов процедуры PrintTeams из процедуры ShowTeams. Процедура PrintTeams имеет следующий прототип:

PrintTeams           PROCEDURE (*TeamClassType pClass)

Помните, что вы можете передать экземпляр класса внутри вашей программы как обычный параметр. Это и сделано здесь. Получая класс в качестве параметра, процедура PrintTeams имеет доступ ко всем данным класса. Это упрощает написание кода для отчета, потому что данные уже были получены.
Кстати, в процедуре PrintTeams показывается, как вы можете легко и просто использовать класс ABC PrintPreviewClass, применяя его в ручном кодировании. Также вы можете увидеть каким образом можно напечатать в отчете очередь «+» техника, которую я предпочитаю использовать: использование наследуемой группы, которая служит источником для данных отчета, позволяя вам задать свой собственный префикс.
Так же как и ShowTeams, процедура PrintTeams не заботится о том, каким образом были получены данные. Она знает только о данные, потому что они содержатся в переданном классе, но не знает ничего о механизме получения этих данных и даже об источнике этих данных.

Процедура PrintTeamsEntry
А что если вы захотите вызвать процедуру PrintTeams в отдельном потоке?
Это проблема. Прототип процедуры PrintTeams имеет параметр — указатель на класс. Кларион не позволит вам использовать команду START для вызова такой процедуры.
Существует техника с использованием переменной типа STRING как параметра для передачи ссылки на класс для запуска процедуры в другом потоке. Правда, это чревато проблемами с длительностью существования экземпляра класса и возможными GPF, т.е. уничтожая экземпляр класса в одном потоке, вы можете продолжать использовать его в другом потоке, что вызовет GPF. Для упрощения я создал еще одну процедуру PRINTTEAMSENTRY, которая решает все проблемы. В этой процедуре описан новый экземпляр класса TeamClassType, который получает данные (с использованием заданной пользователем сортировки), и затем вызывает процедуру PrintTeams, точно также как процедура ShowTeams вызывает PrintTeams.

Класс CsvExportClassType
Вернемся к процедуре Classes:1. Найдите embed, в котором описан класс CsvExportClassType. Задача этого класса экспортировать в csv-файл любую очередь. Также этот класс может быть использован для отладки (например, вы можете загрузить полученный файл в MsExcel посредством API ShellExecute).
Посмотрите код метода TeamClassType.ExportToCSV в точке «Class definitions». В секции данных для этого метода объявлен локальный экземпляр класса CsvExportClassType. Используя этот экземпляр, и происходит вывод данных в csv-файл. Вспомните, что процедура ShowTeams вызывает метод класса TeamClassType для экспорта, т.е. процедура ничего не знает о классе CsvExportClassType. Таким образом, мы получили простой пример взаимодействия классов (как я говорил ранее, мы имеем несколько маленьких объектов, которые в сумме получаются большими, которые в свою очередь могут стать даже громадными).

Подводя итоги: функциональность этого примера основана на классах, расположенных в различных местах. Классы используются несколько раз, что уменьшает количество кода, облегчает сопровождение кода и поиск ошибок, классы имеют понятную структуру, которую можно расширить в дальнейшем. При расширении программы, вновь появившиеся ее части, которые будут нуждаться в функциональности класса TeamClassType, не будут заботиться о том, каким образом они получат данные — они будут использовать метод, который был уже написан ранее для этих целей. Используя класс TeamClassType, программа легко может показать данные в list box-е, напечатать отчет и экспортировать данные в csv-файл.
Этот тип гибкости и повторного использования кода — это и есть технология ООП, которая очень поможет вам, если вы приложите некоторые небольшие усилия для ее изучения. Также, обратите внимание, что большинство функциональности изолированно в классе. Описание класса — это то, что в будущем придется описывать в документации.
И я не говорил о лошадях, утках, машинах и самолетах 🙂

В следующей части речь пойдет о таком важном моменте, как использование очередей в классах.

Комментарий:

Пример претерпел большие изменения со времени выхода первой статьи, так что не удивляйтесь тому, что в «процедуре» Classes:1 несколько больше классов, чем описано в этой части.

Если вы ранее не скачивали пример OOP Sample (753),
то скачайте сейчас. Прочтение этой статьи без примера не имеет смысла.
Мы используем Кларион для написания программ, работающих с базами данных (как минимум я это делаю). Таким образом, возникает такое чувство, что мы должны использовать объекты для работы с данными. Т.е. это может быть набор данных (RecordSet), одна запись и т.п. Идея создания объектов для работы с данными очень хороша, но еще лучше идея создавать объекты, которые содержат в себе данные. Например, TeamClass может содержать данные из файла Team.

Тип данных QUEUE (очередь), используемый в Кларионе, дает возможность использовать его для хранения данных, потому что его структура аналогична структуре таблицы. Очередь имеет поля и записи, так же как и файл данных. Это позволяет использовать очередь в классах для хранения данных.

В примере, классы TeamClassType и PlayersClassType выполняют рутинные операции по извлечению данных из файлов базы данных. Вы не работаете с одной записью, когда вы используете классы. Единожды сказав классу «GetData()», вы получаете набор данных, который хранится внутри самого класса. Как было показано ранее на примере класса TeamClassType — это обеспечивает максимальную гибкость, так как класс может быть использован в любом месте вашей программы. Также, это обеспечивает максимальную точность — единожды получив данные правильно, вы можете работать с ними в любой части вашей программы, которая нуждается в них. Это QUEUE дает вам такую возможность, а класс может содержать в себе очередь.

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

&#149 Класс напрямую не может содержать описание очереди. Он должен содержать только ссылку на очередь, а затем создавать ее при помощи оператора NEW в методе «конструкторе». Также очередь должна быть уничтожена при помощи оператора DISPOSE в методе «деструкторе». Это достигается несколькими строками кода, смотрите код методов Construct и Destruct для класса TeamClassType.

&#149 Очередь класса может быть использована для различных целей. Очередь имеет фиксированную структуру и порядок полей в ней. Этот момент может потребовать специальной обработки очереди. Посмотрите класс PlayersClassType и процедуру ShowPlayers(Показать игроков) в примере. Поля для отображения данных о игроках в list box-е не совпадают с последовательностью определения полей в очереди. Но если вы посмотрите код процедуры ShowPlayers, вы увидите, как это легко можно обойти путем присвоения свойства Prop:FieldNo после открытия окна.

Преимущество использования очереди (и уход от модели SET/NEXT) в том, что ваш класс содержит все данные, они доступны в любом месте программы, без дополнительного описания структуру VIEW для различных list box-ов, отчетов или процессов (шаблон process — обработка данных).
Также, используя очередь, содержащую набор данных, вы уменьшаете риск повреждения буфера записи, когда вы редактируете одну запись. Обратите внимание на процедуру ShowPlayers, которая вызывается из процедуры ShowTeams. В этой процедуре есть код для получения записи из файла Team (tc.GetSingleRecord). Но так как в процедуре ShowPlayers используется другой экземпляр класса TeamsClassType, то нет риска, что буфер записи сотрется при возвращении в процедуру ShowTeams.

Примечание: IMHO последний абзац несколько сумбурно написан. Объясню о чем идет речь.

&#149 Во-первых, у вас есть класс, у которого есть очередь с какими то данными. Вы можете использовать эти данные в любом месте программы. Если вы заведете второй экземпляр этого класса — то у него тоже есть очередь и эта очередь также может иметь данные. Вы можете проводить любые операции над этими двумя очередями, в независимости друг от друга. Т.е. данные не пересекаются.

&#149 Во-вторых, в одном классе вы встали на третью запись в очереди, потом запустили процедуру с другим экземпляром класса и там взяли пятую запись из файла. Вернувшись в первую процедуру, вы по-прежнему будете находиться на третьей записи в очереди. Т.е. данные в разных экземплярах классах не пересекаются. Проводя аналогии с файлом, если вы в одной процедуре пробегаетесь по файлу при помощи SET/NEXT, потом в середине цикла вызываете другую процедуру, в которой также пробегаете по этому файлу при помощи SET/NEXT, то при возвращении в первую процедуру вам придется восстанавливать позицию файла для продолжения цикла, так как это положение будет «сбито» второй процедурой.

И это далеко не все преимущества использования очереди. Посмотрите код класса CsvExporterClassType. Этот класс экспортирует любую очередь в csv-файл. И посмотрите код метода GenericQueueViewer класса GeneralClassType. Этот метод может отобразить любую указанную очередь в list box-е (и обратите внимание, что этот метод может экспортировать отображаемую очередь в csv-файл, примечание: указанный метод основан на классе QueueViewerClassType, поэтому собственно сам код находится QueueViewerClassType).

Когда вы будете просматривать код, обратите внимание на то, как мало требуется кода для browse-ов и отчетов. И весь этот код единожды написан, а в экземплярах вы только вызываете несколько методов. Многие Кларион-программисты думают, что написание классов и «ручное кодирование» равнозначны проделыванию очень «большой работы», а это далеко не так. Если вы используете классы в своей программе, вы увеличиваете функциональность программы, улучшаете масштабируемость, уменьшаете количество ошибок и уменьшаете количество строк кода. И это рулит.

Дополнение
Меня спросили: возможно ли для очереди в классе наследовать структуру файла. Конечно. Это очень легко сделать.

MyClass     CLASS,TYPE ! описание класса
q              &MyQueueType
            END

MyQueueType QUEUE(FILE:record),TYPE,PRE(MyQueueType) ! описание очереди
            END

где FILE:record — структура файла, PRE(MyQueueType) — префикс очереди, совпадающий с именем очереди.
При использовании очереди к полю нужно обращаться как:

 MyClass.q.MyField1

INC-файл с описанием класса должен быть подключен в embeds после описания файлов, для того чтобы класс знал о структуре файлов. Также, вы должны понимать, что приложение (app), в которое вы подключаете INC-файл должно знать о структуре файлов (для случая если файлы объявляются в другом app).

Если вы ранее не скачивали пример OOP Sample (753),
то скачайте сейчас. Прочтение этой статьи без примера не имеет смысла.

В предоставленном примере, я использую базу данных MS Access. В методах классов, которые получают данные используется техника, которую я описал в статьях про SQL (на www.toolwares.com отсутствуют эти статьи, их можно увидеть на сайте www.clarionpost.com, оригинальное название статей Better SQL). Такая техника получения данных может выглядеть несколько странной для Кларион программ.

Если вы предпочитаете использовать такую структуру как VIEW для доступа к файлам, то посмотрите процедуру ShowTeamsWithView. Эта процедура выглядит практически также как и процедура ShowTeams. Ни в одной из процедур не видно каким образом извлекаются данные.

Процедура ShowTeamsWithView использует экземпляр класса TeamsClass2Type. Как вы можете видеть в «процедуре» Classes:1 этот класс использует VIEW и ABC класс RelationManager.

В методе TeamsClass2Type.Getdata определен VIEW, ABC класс RelationManager открывает файл, устанавливается буфер для VIEW, определяется сортировка и данные загружаются в очередь. Далее VIEW и файл закрываются. Также есть дополнительный метод, который рассчитывает итоги, как сумму всех ID. Этот метод служит примером обработки данных после выполнения запроса (т.е. проведение дополнительных вычислений, таких как подсчет сумм по строке и подсчет итогов).

Когда вы используете ABC классы (или операторы Кларион, такие как share, next) для получения данных из файла, всегда думайте о том, что вы можете повредить буфер файла, на который опирается другая часть вашей программы. Если вы повреждаете буфер — задокументируйте этот момент, чтобы другие программисты или вы сами знали и помнили об этом, и, соответственно, приняли какие то шаги. Так как обычно описание класса и есть маленькая документация, то достаточно будет несколько слов комментария (посмотрите например описание класса TeamsClass2Type).

Для сохранения буфера файла посмотрите документацию к методу FileManager.SaveFile и к методу FileManager.RestoreFile. Вы можете использовать эти методы соответственно до и после получения данных.

Комментарий:

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

Если вы ранее не скачивали пример OOP Sample (753), то скачайте сейчас.

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

Теперь пример содержит еще одно приложение states.app, использующее states.dct. И это приложение используется oop.app. Эти два приложения используют разные словари (используется драйвер TOPSPEED).

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

Для начала скомпилируйте приложение states.app. Потом oop.app. Запустите oop.exe и выберите пункт меню «Select states». Вы увидите список штатов. В этой точке запускается код, находящийся в dll. После того, как вы выберете штат (или нажмете Cancel), будут показаны данные штата из класса.

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

Теперь о том, каким образом осуществлялась сборка dll

1. Для начала было сделано обычное приложение со словарем. Естественно, это было dll-приложение. Я создал необходимые классы для этого приложения (см. процедуру Classes:1). Не устанавливайте галочку Generate template globals and ABC’s EXTERNAL, но установите Generate all file declarations.

2. В глобальных embeds есть следующий код:

states_linkmode        equate(1) ! линковать
states_dllmode         equate(0) ! описание находится не в dll
include('states1.inc'),once

Это говорит компилятору как правильно прилинковать код классов.

3. В глобальных шаблонах (global extensions) добавьте шаблон «Limit exports». Этот шаблон предотвращает генерацию всего, что попадает в exp файл, за исключением процедур, имеющих установленный атрибут «Export» и классов, находящихся внутри шаблона DP_Class_Source с атрибутом «Export» (посмотрите на свойства процедуры Classes:1 и обратите внимание на галку «Export Classes»).

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

5. Скомпилируйте приложение — теперь у вас есть приложение, которое может быть использовано другим приложением, даже если их словари разные.

Теперь подготовим приложение oop.app для использования states.app:

1. В Global embeds вставим следующий код:

states_linkmode        equate(0) ! не линковать
states_dllmode         equate(1) ! класс находится в dll
include('states1.inc'),once

2. Добавьте states.lib в настройках проекта.

3. Скомпилируйте приложение.

Неправда ли круто?

Наследование класса происходит тогда, когда класс является копией другого класса, но может иметь дополнительную или измененную функциональность некоторых методов.

Посмотрите на эти классы:

ClassOne    CLASS
Run            PROCEDURE
PrintReport    PROCEDURE
            END

ClassTwo    CLASS(ClassOne)
            END

Эти классы одинаковы, и они будут работать одинаково. А теперь другая ситуация:

ClassOne    CLASS
Run            PROCEDURE
PrintReport    PROCEDURE, VIRTUAL
            END

ClassTwo    CLASS(ClassOne)
PrintReport    PROCEDURE, DERIVED
            END

Теперь эти классы имеют разное поведение, так как ClassTwo наследует виртуальный метод.
Что же такое виртуальный метод для этого случая? Допустим, вы написали код для отчета. Потом вы сделали копию этого отчета и несколько изменили код. Т.е. у теперь у вас есть отчета, которые отличаются друг от друга своим поведением. Т.е. виртуальные методы позволяют создавать вам новые классы, которые работают также как и первоначальный (родительский) класс, но добавляют или изменяют, или замещают часть его функциональности. Наследуемые классы такие же как и родительские + дополнительная функциональность.

Если вы ранее не загружали пример, загрузите сейчас.

Посмотрите на процедуру PrintPlayers в приложении oop.app. Эта процедура разработана для печати команд и их игроков в одном из двух форматов. Вы можете увидеть описание локального класса rClass созданного для печати отчета в одном формате. Также есть описание класса rClass_ByPlayer, который печатает отчет в другом формате. Разница форматов обеспечивается за счет виртуального метода PrintReport. Вся остальная функциональность класса rClass_ByPlayer наследуется (остается такой же) как и у родительского класса rClass.

Написание отчетов достаточно беспорядочно и просто плачет по ООП.

В классе для печати игроков существуют общие методы, такие как: открытие отчета, получение данных для отчета, предпросмотр отчета, подсчет итогов и т.д. Эти общая функциональность содержится в родительском (базовом) классе. Общие методы доступны в обоих классах — и в родительском и в дочернем. Метод с отличным (разным) поведением отделен в наследуемый метод PrintReport. Это выглядит как операция copy/paste, с тем преимуществом, что если вы измените поведение общего метода в родительском, это отразится и на дочерние классы, которые наследуют эти методы.

Метод Run() одинаков для обоих классов. Разница в том, какой метод вызывается внутри метода Run(), для разных классов вызываются свои оригинальные методы PrintReport.

Когда вы пишете код в какой либо точке вставки для ABC классов (например на ValidateRecord), вы, по сути, наследуете виртуальный метод и изменяете его под свои нужды.

Обратите внимание на метод GetNext(). Те из вас, кто программировал на CPD 2.0 (DOS) возможно узнают эту технику. Она несколько изменена, но принцип тот же и она работает в windows-отчетах также как и в dos-отчетах. Я только недавно вспомнил этот метод для управления break-ами (в форматтере отчетов смотри меню Bands/Break group). И это великолепно работает и этот метод более гибкий по сравнению с другими методами. Иногда приходится и нового пса учить старым трюкам.

Прочитайте помощь о классах (F1) и еще раз прочитайте снова и снова, пока не поймете. Применяйте полученные знания, создавая свои классы и наследуя их.

Мой опыт показывает, что программирование баз данных достаточно специфическая задача и наследование зачастую не является обычным делом как при написании других задач. Не окажитесь за бортом. Также, когда вы думаете о наследовании, вначале спросите себя — действительно ли это нужно, т.е. не стоит поддаваться эмоциям типо «о, да это круто, посмотрите как я могу».

Если вы посмотрите код процедуры PrintPlayers в приложении oop.app, то можете увидеть, что в секции локальных данных процедуры описаны два класса. Эти классы локальны, т.е. другие части программы не могут использовать эти классы. Эти классы используются только этой процедурой. А объявлены они локально, потому что нет смысла использовать их в другом месте.

Может возникнуть ощущение, что если классы локальны, то нет смысла использовать именно классы, ведь невозможно повторно использовать эти классы. Но преимущество классов не только в возможности многократного использования кода. Эти определенный стиль написания кода, разбиение кода на более мелкие и более функциональные части. Поэтому локальные классы более предпочтительны, чем локальные процедуры или тем более рутины (routine).

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

Еще одно «за» использование классов вместо рутин: если однажды вы захотите вынести ваш класс в отдельный inc и clw-файлы, то это будет несложно сделать, вынести же рутину в класс (для использования всей программой) будет несколько сложнее.

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

MyClass.MyMethod  PROCEDURE
LocalClass  CLASS
SomeMethod     PROCEDURE
            END
  code

Компилятор не пропустит такую конструкцию, потому что возникает неоднозначность использования слова SELF. Можно избежать этой двусмысленности, передав класс в метод в качестве параметра. Пример:

MySpecialReport(*MyClassType pClass)

Соответственно, когда вы запустите метод MySpecialReport, вы сможете использовать класс MyClassType обращаясь к нему через pClass, например так:

pClass.CalculateAverages()

Также вы можете использовать локальные классы в процедуре MySpecialReport. Иногда это идеальный метод — использовать классы для печати отчетов. У вас просто есть класс, вызывающий процедуру, которая получает класс в качестве параметра. А если ваш класс вызовет процедуру передав ей в качестве параметра самого себя (SELF), то вы получите полную дальнейшую изоляцию кода. Процедура — будем подразумевать, что процедура отчета — не будет знать о остальной части программы, она будет знать только о классе. Также вы освободите себя от экспорта этой процедуры в другую dll или exe, так как другие dll или exe будут печатать отчет, используя метод класса, вообще не зная о том, что вызывается процедура печати.

Другое преимущество передачи класса в процедуру — это то что, добавив новые процедуры, которые вызываются таким образом, необходимо сгенерировать только приложение, в котором они находятся. А другие приложения не нуждаются в регенерации, так как вы не добавляете в них эти процедуры (как external). Если вы измените описание класса, вам придется выполнить глобальную компиляцию (global compile), а это намного быстрее, чем глобальная регенерация (re-generate) и компиляция.