Объектно-ориентированный анализ и проектирование
Чтобы использовать всю мощь языков ООП, особенно в сложных проектах, необходимо, анализировать и проектировать программу, руководствуясь принципами ООП. Имеется несколько методологий объектно-ориентированного анализа и много подходов объектно-ориентированного проектирования [5,13]. Процесс разработки программного обеспечения с использованием ООП включает все обычные этапы циклической схемы интегрального прототипирования (рис. 2.2).
Процесс циклической прототипной реализации начинается с создания основной программы или проекта будущего программного продукта. Классы подключаются к проекту таким образом, чтобы создать грубый, но работающий прототип будущей системы, который сразу же тестируется и отлаживается при участии конечного пользователя. Затем к системе подключается следующая | руппа классов, например, связанная с реализацией некоторого пункта меню. Полученный вариант также тестируется и отлаживается, и так далее, до реализации всех возможностей системы.
Особенностью ООП является то, что объект или группа объектов могут разрабатываться отдельно, и, следовательно, их проектирование может находиться на различных этапах. Например, интерфейсные классы уже реализованы, а структура классов предметной области еще только уточняется.
На этапе формирования требований к информационной системе выполняется исследование предметной области задачи, объектная декомпозиция разрабатываемой системы и определяются важнейшие особенности поведения объектов (описание абстракций). По результатам анализа разрабатывается структурная схема программного продукта, показывающая основные объекты и сообщения, передаваемые между ними.
На этапе разработки эскизного и технического проектов сначала осуществляется логическое проектирование, не учитывающее деталей технической реализации, зависящих -от платформы, языка программирования и пр., а затем окончательное физическое проектирование. Логическое проектирование в ООП заключается в разработке структуры классов: определяют поля для хранения составляющих состояния объектов и алгоритмы методов, реализующих аспекты поведения объектов. Результатом является иерархия или диаграмма классов, отражающая взаимосвязь классов и описание классов. Физическое проектирование включает объединение описаний классов в модули, ныбор механизма их подключения (статическая или динамическая компоновка), определение способов взаимодействия с оборудованием, с операционной системой и/или другим программным обеспечением (например, базами данных, сетевыми программами), обеспечение синхронизации и т.д. [13].
Процесс модификации заключается в добавлении новых функциональных позможностей или изменении существующих свойств системы. Как правило, изменения затрагивают реализацию класса, оставляя без изменения его интерфейс, что благодаря ООП обычно не вызывает проблем, так как процесс изменений затрагивает локальную область. Изменение интерфейса - более сложная задача, так как ее решение может повлечь за собой необходимость согласования процессов взаимодействия объектов, что потребует изменений в других классах программы. Однако сокращение количества параметров в интерфейсной части по сравнению с модульным программированием существенно облегчает и этот процесс. Простота модификации позволяет сравнительно легко адаптировать программные системы к изменяющимся условиям эксплуатации, что увеличивает время жизни систем, на разработку которых затрачиваются огромные временные и материальные ресурсы.
Глава 3. РЕАЛИЗАЦИЯ ООП В ЯЗЫКЕ OBJECT PASCAL
§3.1. Классы. Объявление классоё
Как уже было указано выше, в современных языках программирования основным носителем базовых принципов ООП является конструкция языка класс. В Object Pascal класс - это пользовательский тип данных, который может иметь в своем составе поля, методы, свойства и события. Класс должен быть объявлен до того как будет хотя бы одна переменная этого класса. Переменная типа класс называется объектом. Объекты, в отличие от классов, реальны, во время выполнения программы они хранятся в памяти. Пример 3.1. Синтаксис объявления класса.
Туре
<имя класса> = Class (<имя класса-родитедя>) private {только в модуле}
<поля, методы, свойства, события» protected {доступ только для потомков}
<поля, методы, свойства, события» public {доступно всем)
Споля, методы, свойства, события» published {видны в Инспекторе Объектов и изменяемы}
<поля, свойства» end;
При описании классов следует учитывать следующее.
1. Класс описывается в разделе описания типов, обычно расположенном в разделе модуля interface. Если класс используется только внутри данного модуля, его описание может располагаться в разделе описания типов раздела модуля implementation. Класс не может быть описан внутри процедуры и функции.
2. Имя класса - это любой доступный идентификатор, однако принято идентификаторы классов начинать с символа "Г.
3. Синтаксис объявления полей класса схож с объявлением переменных или полей в записях.
4. Синтаксис объявления методов в простейшем случае не отличается от объявления процедур и функций.
5. Доступ к объявляемым элементам класса определяется тем, в каком разделе они объявлены (public, published, protected, private), что является одним из механизмов реализации принципа инкапсуляции.
6. Класс наследует поля, методы, свойства и события от своих предков. Имя класса родителя указывается в скобках после ключевого слова class, если оно явно не указывается, то предполагается, что данный класс является непосредственным наследником класса TOb jecfc.
7. В первом приближении можно определить свойство как поле, доступное для чтения и записи не напрямую, а через соответствующие методы, а событие как особый вид свойства, имеющего тип указателя на метод.
8. В случае если классы взаимно ссылаются друг на друга, разрешено использовать опережающее объявление классов. Пример 3.2. Опережающее описание класса, type
TFirstClass = class; // Опережающее описание класса TSecondClass = class
FirstObject: TFirstClass; end;
TFirstClass = class
SecondObject : TSecondClass; end;
Если в классе описаны методы, то в тексте модуля, как правило, в разделе implementation, обязательно должна быть реализация этих методов. Пример 3.3. Объявления класса в модуле, unit MyUnit; interface type
TMyButton = class (TButton) private
FClickCounter: Integer;
procedure SetClickCounter(const Value: Integer); published
property ClickCounter: Integer read FClickCounter
write SetClickCounter;
end; var
MyButton: TMyButton; implementation
procedure TMyButton.SetClickCounter(const Value; Integer); begin
FClickCounter Value; •nd;
Обращение к полям, методам, свойствам и событиям объектов класса синтаксически оформляется как обращение к полям записей, то есть сначала указывается идентификатор объекта, а затем через точку имя соответствующего элемента. Например, чтобы считать значение свойства ClickCounter объекта MyButton, необходимо записать MyButton. ClickCounter. Оператор with применительно к любым элементам класса действует аналогично полям записи, то есть открывает область действия, содержащую имена элементов указанной переменной типа класс, так что теперь эти имена могут использоваться как имена простых переменных, функций, процедур.
Разделы описания класса private, protected и public
В примере 3.3 описан класс TMyButton, содержащий поле FClickCounter, метод SetClickCounter и свойство ClickCounter. Поле и метод находятся it разделе private, а свойство - в разделе published. Рассмотрим, что "шачает размещение компонентов класса в том или ином разделе.
Хороший стиль программирования требует, чтобы данные в классах были скрыты или инкапсулированы (encapsulate). Концепция инкапсуляции очень проста: нужно просто думать о классе как о «черном ящике» с очень маленькой видимой частью. Видимая часть, которая называется интерфейсом класса (class interface), позволяет остальным частям программы получать доступ к объектам данного класса. Однако реализация объекта должна быть скрыта от пользователя. Этого требует объектно-ориентированный подход к концепции классического программирования, называемый разграничением информации (information hiding).
Object Pascal заимствовал из классического ООП три спецификатора доступа: в private - определяет поля и методы класса, которые недоступны вне модуля (файла с исходным кодом), в котором определен класс. В С++, например, поля и методы этого раздела недоступны даже из методов любого другого класса, описанного в этом же модуле;
• public - определяет поля и методы, к которым может обращаться любая часть программы;
• protected - определяет частично доступные поля и методы. Доступ к ним имеют методы данного класса и всех классов, являющихся его наследниками. Хороший стиль ООП требует, чтобы все поля кЛасса относились к категории
private, методы же, как правило, имеют спецификатор public или protected.
Есть еще два спецификатора доступа - published и automated, которые определяют такие же права доступа к элементам класса, как и public. Их отличие заключается в том, что элементы класса, размещенные в разделе published, доступны для изменения из Инспектора объектов Delphi, а для элементов, расположенных в разделе automated, генерируется специальная информация, требуемая для работы серверов автоматизации OLE. Следует обратить внимание, что при реализации названия методов предваряются именем класса, например, в рассматриваемом случае: procedure TMyButton.SetClickCounter.
Fcnn элементы класса объявляются вне разделов доступа, то в случае включенной опции компилятора {$М+} (по умолчанию) они относятся к разделу published, иначе {$М-} - к разделу public.
§3.2. Объекты. Экземпляры объектов
Объекты можно представлять несколькими способами. Так, во многих языках программирования объявление переменной, имеющей тип класса, автоматически создает объект (например, Visual Basic). Object Pascal вместо этого использует ссылочную модель объектов (object reference model). Ее суть заключается в том, что каждая переменная типа класс называется объектом, но реально содержит лишь указатель на область памяти, в которой содержится значение реального объекта. Эта область оперативной памяти называется экземпляром объекта (рис. 3.1). Несмотря на кажущуюся сложность, такая модель является очень
мощной и удобной. Единственная ее проблема состоит в том, что программист должен вручную отводить память под так называемый экземпляр объекта.
Объект (переменная) | Экземпляр объекта (динамическая переменная) | |
Рис. 3.1. Схема связи объекта и экземпляра объекта
Таким образом, можно выделить три основных языковых понятия в Object Pascal, реализующих принципы ООП:
• класс - пользовательский тип данных, описывающий поля, свойства, методы и события, которые будут у всех объектов этого класса (переменных этого типа).
• объект - это переменная типа класс, фактически является типизированным указателем.
• экземпляр объекта - место в оперативной памяти, где собственно хранятся значения полей, свойств и адреса методов объекта (по сути, является динамической переменной).
Однако есть определенное отличие динамической переменной от экземпляра объекта. Если для получения значения динамической переменной используется операция разыменования (А), то при работе с экземплярами объекта эту операцию проводить не нужно.
Текст фрагмента программы, где используется объект объявленного класса, может выглядеть следующим образом. Пример 3.4. Создание и уничтожение объекта класса. // Основная программа v»r
AButton : TMyButton; begin
// Создаем об-ьвкт AButton:= TMyButton.Create; // Записываем значение в его свойство AButton.ClickCountar := 0;
// Уничтожаем об-ьагст AButton.Free; end;
В приведенном примере в разделе var описана переменная AButton - объект класса TMyButton. В тексте программы происходит создание объекта, модификация его свойства и в конце - его удаление. Для того чтобы понять как создается и удаляется объект, необходимо более детально рассмотреть реализацию этого понятия в языке Object Pascal.
Чтобы создать экземпляр объекта, необходимо вызвать метод Create, который является конструктором объекта, а результат его выполнения присвоить переменной-объекту. При создании нового экземпляра объекта класса конструктор применяется к классу, а не к объекту, например. TMyButton. Create. Такой конструктор есть у всех классов, поскольку он
описан как конструктор класса ТОЬ ject, от которого наследуются все остальные классы. Если создается экземпляр объекта (отводится место в оперативной памяти), то впоследствии его необходимо уничтожить (освободить память). Эта операция выполняется с использованием деструктора. В классе ТОЬ ject определен деструктор Destroy, который, таким образом, есть у всех классов Object Pascal. Тем не менее, для уничтожения экземпляра объекта рекомендуется использовать метод Free, который также унаследован от класса TObject. Этот метод надежней, поскольку он сначала проверяет не равен ли указатель на экземпляр объекта nil и только затем вызывает деструктор Destroy.
§3.3. Свойства
Одной из ключевых целей инкапсуляции является уменьшение числа глобальных переменных, поскольку использование глобальных переменных крайне затрудняет отладку и снижает надежность программы.
Пример. Необходимо хранить количество щелчков мыши на форме. Эти данные должны быть доступны для чтения из других модулей. Примечание: код для подсчета щелчков на форме не рассматривается.
Решение:
1 вариант. Можно описать глобальную переменную в разделе interface
модуля формы, она будет доступна из любых модулей, использующих (uses)
данный модуль.
interface
type
TForml = class (TForm)
end; var
Forml: TForml; nClicks: Integer; Главный недостаток данного варианта в том, что данные не связаны с экземпляром объекта-формы: если будут созданы две формы (два объекта данного класса), то они будут использовать эти данные совместно. Для того чтобы каждая копия формы имела свой экземпляр общедоступных данных, их надо описывать как поле класса формы в разделе public.
2 вариант. Данные о количестве щелчков мыши хранятся в поле класса, описывающего форму.
interface type
TForml = class (TForm)
public
nClicks: Integer; end; var
Forml: TForml;
Теперь у каждой формы будет свой счетчик, так как каждый объект имеет свое поле nClicks. В то же время данные доступны из других модулей, только ссылка идет через имя объекта: Forml. nClicks. Однако у этого способа также есть недостатки. Они связаны с тем, что, описав данные как public-поле, нельзя в будущем изменить их воплощение, не затронув кода, который использует эти данные, и, во-вторых, в public-поле могут быть записаны любые данные, в том числе некорректные, например, отрицательное число. Чтобы избежать этого, данные класса следует описывать как поле private, а для получения их значения предоставлять соответствующий метод класса.
3 вариант. Данные описаны как поле класса в разделе private, а для получения их значения определен соответствующий метод класса в разделе public, interface type
TForml = class (TForm) public
function GetClicks: Integer; private
nClicks: Integer; end; var
Forml: TForml; implementation
function TForml.GetClicks: Integer; begin
Result := nClicks; end;
Теперь для того, чтобы получить из внешних модулей данные о количестве щелчков на форме, можно вызвать метод Forml. GetClicks. Еще более грамотным решением было бы хранить данные как свойство класса.
Рассматривая понятие свойства необходимо сразу отметить, что само свойство не хранит данных, свойство класса - это специальная конструкция языка Object Pascal, которая определяет, каким образом будет осуществляться доступ к защищенным данным класса. Данные, в конечном счете, всегда хранятся только в полях класса. Свойство класса объявляется оператором вида: property <имя свойства»: <тип>
read <иия поля или метода чтения> write <имя поля или метода записи> <директивы эапоминания>;
В разделе read задается способ для чтения данных; если имя поля, то данные напрямую берутся из соответствующего поля класса, тип которого должен совпадать с типом свойства, иначе для чтения данных используется указанный метод чтения. Метод чтения должен быть объявлен как функция без параметров, возвращающая значение того же типа, что и тип свойства. Принято имя функции чтения начинать с префикса Get, после которого следует имя свойства.
В разделе write задается способ для записи данных, и если это - имя поля, то динные напрямую берутся из соответствующего поля класса, тип которого
должен совпадать с типом свойства, иначе для чтения данных используется указанный метод записи. Метод записи должен быть объявлен как процедура с одним параметром, имеющим тот же тип, что и свойство. Имя процедуры записи лучше начинать с префикса Set, после которого следует имя свойства. Если раздела write нет - значит свойство определено только для чтения.
Директивы запоминания определяют как надо сохранять значения свойств при сохранении пользователем файла формы *.dfrn. Чаще всего используется директива default <значение по умолчанию>, которая не задает начальные условия (это делает конструктор), а лишь указывает, что если пользователь в процессе проектирования не изменил значение свойства по умолчанию, то сохранять значение свойства не надо.
Пример использования свойства для решения поставленной ранее задачи (<4 вариант).
interface type
TForml = class (TForm) public
property nClicks: integer read GetClicks ; private
FnClicks: Integer; function GetClicks: Integer; end; var
Forml: TForml; implementation
function TForml.GetClicks: Integer; begin
Result : = FnClicks; end;
Теперь для того чтобы узнать количество щелчков сделанных на форме, необходимо обратиться к свойству соответствующего объекта: Forml. nClicks. После такого обращения реально вызывается метод Forml. GetClicks, который возвращает значение, хранимое во внутреннем поле FnClicks. Традиционно идентификаторы полей совпадают с именами соответствующих свойств но с добавлением в качестве префикса символа 'F'. То есть использование свойства класса позволило сохранить простой синтаксис обращения к данным класса (как в первом примере), но в то же время сохранить все преимущества принципа инкапсуляции, присущие третьему варианту.
Единственным недостатком такого подхода можно было бы считать достаточно большой объем дополнительного программного кода, который необходимо включать в описание класса. Однако Delphi умеет автоматизировать процесс описания свойств с использованием механизма автоматической генерации кода класса, который был рассмотрен в §1.3. Можно внести в класс описание свойства, например: property nClicks: integer read GetClicks;
после этого установить курсор на эту строку и нажать Ctrl+Shift+C и Delphi сгенерирует описание внутреннего поля данных, методов чтения (записи) и даже реализацию этих методов в разделе implementation.
Свойство может быть векторным. В этом случае синтаксис его объявления: property AButtons [Index: Integer]: TButton read GetButton write SetButton;
Для векторного свойства необходимо описать не только тип элементов массива, но также имя и тип индекса. После ключевых слов read и write в этом случае должны стоять имена методов - использование здесь полей типа массив недопустимо. Метод, читающий значение векторного свойства, должен быть описан как функция, возвращающая значение того же типа, что и элементы свойства, и имеющая единственный параметр того же типа и с тем же именем, что и индекс свойства:
function GetButton (Index: Integer):TButton;
Аналогично, метод, помещающий значения в такое свойство, должен быть описан как процедура, первый параметр которой определяет индекс, а второй - переменную типа свойства:
procedure SetButton (Index: Integer; NewButton: TButton);
Некоторые классы «построены» вокруг своего основного векторного свойства (например, такие предопределенные классы Delphi как TList, TStrings). Для облегчения доступа к такому свойству оно может быть описано с ключевым словом default. При этом смысл и синтаксис его применения для векторных свойств совершенно иные, чем для обычных свойств, type
TMyObject и class
property AButtons [Index: Integer]: TButton
read GetButton write SetButton; default;
end;
Если у объекта есть векторное свойство с директивой default, то при обращении к его элементам идентификатор свойства можно не упоминать, а ставить индекс в квадратных скобках сразу после имени объекта. Например, если описан объект класса с таким векторным свойством: var
AMyObject: TMyObject;
то к элементам этого векторного свойства можно обращаться двумя способами:
AMyObject.AButtons[1] := Buttonl; (первый способ> AMyObject[2] :— Button2; {второй способ (короткий)}
Таким образом, исходя из принципа инкапсуляции ООП поля данных классов должны быть защищены от несанкционированного доступа. Поэтому поля целесообразно объявлять в разделе private, в редких случаях их можно помещать в раздел protected - защищенном разделе класса, чтобы возможные потомки класса имели к ним доступ. Доступ должен осуществляться через свойства, определяющие методы чтения и, при необходимости, записи полей. Так, нее предопределенные классы, описанные в Delphi, определяют доступ к данным только через свойства.
§3.4. Ключевое слово self
В Главе 2 было указано, что отличие методов от процедур и функций состоит в том, что у методов есть неявный параметр, который является ссылкой на тот объект класса, для которого вызван метод. Внутри метода можно явно ссылаться на этот параметр при помощи ключевого слова self. Этот дополнительный скрытый параметр необходим в тех случаях, когда создаются несколько объектов одного класса, так что каждый раз, когда применяется метод к одному из объектов, он должен оперировать со своими данными и не влиять на остальные объекты. Например, при рассмотрении свойств была описана реализация метода класса GetClicks.