Алгоритмическая и объектно-ориентированная декомпозиция

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

·  алгоритмическая или структурная;

·  объектно-ориентированная (далее ОО).

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

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

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

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

ОО программирование (ООП) вносит в разработку ПО:

1.  Естественность

2.  Надежность

3.  Повторное использование

НЕ нашли? Не то? Что вы ищете?

4.  Удобство сопровождения

5.  Способность к расширению

6.  Упрощение выпуска новых версий

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

В ООП при написании классов выделяют две стороны:

·  интерфейс;

·  реализация.

Интерфейс – декларативная часть класса, описывает «внешнему миру» абстракцию класса, его облик, в определенном смысле контракт – «обязуюсь делать то-то и то-то». В большинстве ОО языков программирования выделяют как минимум следующие три уровня интерфейса:

·  Public (общедоступный, открытый) – элементы класса, доступные всем, кто использует класс и/или его объекты

·  Protected (защищенный) – элементы класса, доступные в классах производных (наследующих) от данного.

·  Private (закрытый) – элементы класса, доступные только изнутри класса, а так же из некоторых особо выделенных классов и функций (в разных ОО языках существуют разные способы указания таких классов и функций).

Реализация – императивная (процедурная) часть класса, не видна (в идеале, и не существенна) для остального мира, «то-то и то-то делаю так-то и так-то»

Описание класса

Перейдем к классу, как синтаксическому элементу конкретного ОО-языка – Delphi. Итак, прежде всего класс – это тип данных. Так же как и другие типы, классы описываются в разделах type программ и модулей, причем могут быть как интерфейсной (interface), так и реализационной (implementation) части модуля. Однако, описание классов в реализационной части модуля – явление малораспространенное.

Полное описание (интерфейсная часть) класса выглядит следующим образом:

<имя класса> = class [abstract | sealed] [(<имя класса-предка>)]

<список элементов (членов) класса>

end;

где <имя класса> – правильный идентификатор, а <имя класса-предка> – имя класса, описание которого доступно в данной точке программы (то есть он описан или предварительно описан выше по тексту модуля или находится в интерфейсной части используемого модуля). Если указан класс-предок, но не описано ни одного элемента в классе, то ключевое слово end можно опустить. Такое описание называется кратким описанием класса. Необязательное ключевое слово abstract говорит о том, что класс абстрактный, то есть не предназначен для того, чтобы его объекты создавались и использовались в программе. Необязательное ключевое слово sealed говорит о том, что класс опечатан, то есть не может иметь наследников. Класс не может быть одновременно абстрактным и опечатанным.

Интерфейс класса состоит из произвольного количества произвольно чередующихся, в том числе с повторениями, секций (областей доступности или видимости):

public, protected, private, strict protected, strict private, published и automated

Значение первых трех областей видимости совпадает с указанным выше. Для секции private способ указания функций и классов, имеющих доступ к этим элементам — размещение в одном модуле с классом. Последние две по области видимости приравнены к public, но имеют некоторую дополнительную чисто техническую нагрузку. Кроме того, применение ключевого слова automated признано устаревшим и оставлено только для обратной совместимости с предыдущими версиями, оно рассматриваться в курсе не будет. Ключевое слово published будет рассмотрено в теме «VCL». Действие атрибута распространяется на элементы класса, описанные по тексту после него до момента появления другого атрибута или конца объявления класса. В начале описания класса может присутствовать секция, не имеющая явно указанного атрибута видимости, она в различных случаях расценивается или как public, или как published (этот нюанс будет рассмотрен в теме «VCL»). В отношении атрибутов strict protected, strict private в справочной документации сказано, что они ведут себя соответственно так же как protected и private, но игнорируют фактор размещения в одном модуле и допускают обращение методов экземпляра только к элементам этого же экземпляра, однако на практике их поведение оказывается сложнее (см. пример далее по тексту).

Существует понятие предварительного описания класса, которое необходимо, если два класса в одном модуле так или иначе ссылаются друг на друга. Так как язык Pascal, а значит и Delphi, последовательно исповедует принцип «прежде чем что-то использовать, нужно это описать», возникает невозможность дать настоящее (полное или краткое) корректное определение этим классам. В таком случае один из классов (или оба) предварительно (до того как на них ссылаются) описываются следующим образом:

<имя класса> = class;

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

Различают следующие виды элементов класса:

    поле

·  обычное поле (поле экземпляра)

·  поле класса

    метод

·  обычный метод (метод экземпляра)

·  метод класса

·  обычные методы класса

·  статические методы класса

    свойство

·  обычное свойство (свойство экземпляра)

·  свойство класса

Обычные поля, обычные методы и обычные свойства будем совокупно называть обычными элементами класса. Поля класса, методы класса и свойства класса будем совокупно называть классовыми элементами класса.

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

Поля

Обычное поле – это физически хранимый в экземплярах класса элемент данных. Набор полей и их значений отражает состояние объекта. Поля полностью относятся к декларативной части класса и аналогичны полям типа записи (record). Поля описываются следующим образом:

[var] <имя поля>:<тип поля>;

где:

<имя поля> – правильный уникальный в пределах класса идентификатор,

<тип поля> – доступный в данной точке файла тип (то есть. тип, описанный ранее по тексту или описанный в интерфейсной части используемого модуля или встроенный тип Delphi), кроме файловых типов.

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

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

class var <имя поля>:<тип поля>;

Слова class var можно пропускать, если предшествующий элемент в текущей секции видимости также является полем класса.

Методы

Обычный метод – описанная внутри класса и связанная с экземплярами класса (то есть имеющая доступ ко всем элементам класса) процедура или функция. Совокупность методов описанных в классе (а так же и унаследованных) отражает поведение экземпляров класса.

Методы классифицируются на:

    Конструкторы (создают в памяти и/или инициализируют состояние объекта); Деструкторы (освобождают память и/или состояние объекта); Модификаторы (изменяют состояние объекта); Селекторы (возвращают некоторую характеристику состояния); Итераторы (организуют доступ к частям объекта в строго определенном порядке).

Методы – единственный вид элементов класса, у которых присутствует как декларативная (объявление), так и императивная (определение, реализация) часть. Декларативная часть находится внутри объявления класса.

Заголовки объявления методов последних трех типов по структуре не отличаются от заголовков объявлений обычных процедур и функций (за исключением специфического набора директив). Объявление конструкторов начинается с ключевого слова constructor, а объявление деструкторов с ключевого слова destructor, после которых следует имя и список параметров. Несмотря на то, что при объявлении конструктора не указывается тип возвращаемого значения, конструктор является функцией. Возвращаемое значение – экземпляр того класса, в котором описан конструктор. Деструктор и по форме описания и по использованию является процедурой.

Существует два способа вызова конструктора – через имя класса или через имя объекта. Между этими способами есть существенная разница: при вызове через имя класса происходит выделение памяти под объект и инициализация объекта (в том числе и тот код, что находится в реализации конструктора). При вызове через имя объекта подразумевается, что под объект уже была ранее выделена память и производится только инициализация объекта (в том числе и тот код, что находится в реализации конструктора). Хорошая практика программирования: все ресурсы, захваченные конструктором, должны быть освобождены деструктором.

Директивы должны следовать именно в указанном порядке, разделяются «;» и присутствуют только в описании метода (но не в его реализации):

reintroduce; overload; binding; calling convention; abstract; warning;

где binding – одно из: virtual, dynamic, или override; calling convention – одно из: register, pascal, cdecl, stdcall, или safecall; и warning – любое подмножество из: platform, deprecated и library.

Все они применяются в особых случаях и могут отсутствовать. Некоторые из них будут рассмотрены позднее в темах «Полиморфизм. Статические, динамические и виртуальные методы. Перегрузка методов».

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

Реализация методов всегда располагается в разделе implementation модуля, вне зависимости от того, где находится его объявление. От заголовка реализации обычной процедуры или функции заголовок реализации метода отличается наличием имени класса перед именем метода, отделенным точкой.

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

В противоположность обычным методам, методы класса – методы, не оперирующие с обычными элементами класса. Их объявление начинается с ключевого слова class (заголовок реализационной части – тоже). Методы класса бывают обычными и статическими. В обычных методах класса неявный параметр Self присутствует, но носит другой смысл нежели в обычных методах экземпляра – это класс, для которого вызван метод класса (не тоже самое что класс, где метод описан — описан он может быть и в предке). Через этот параметр можно обращаться к другим классовым элементам класса, а так же к конструкторам (так как они занимают особое, промежуточное положение между обычными методами и методами класса). Статические методы класса не имеют неявного параметра Self, но имеют доступ к классовым элементам того же класса, где они объявлены. Для описания статического метода класса нужно указать директиву static в объявлении метода.

Свойства

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

Описание свойства:

property <имя свойства> [[<список индексов>]]: <тип свойства> [index <целая константа>] <спецификаторы>; [default;]

где:

<имя свойства> – корректный уникальный в пределах класса идентификатор,

<тип свойства> – произвольный тип (кроме файловых),

<список индексов> – список, перечисленных через точку с запятой, пар
<имя индексирующей переменной>: <тип индексирующей переменной>

Перед именем индексирующей переменной допускается модификатор const. В справочной документации по Delphi сказано, что в качестве индексирующего типа может выступать любой тип, это, вообще говоря, неверно (файловые типы не могут), однако, например, строки или записи могут быть индексирующими переменными. После объявления индексированного свойства может быть указана через точку с запятой директива default (не путать со спецификатором default). Директива default позволяет индексировать сам экземпляр класса для доступа к свойству (то есть свойство используется «по умолчанию» при попытке индексации объекта). В классе может быть несколько свойств по умолчанию, если у них существенно различается список индексов.

Модификатор index позволяет нескольким свойствам разделять один метод доступа. Такие разделяемые методы должны принимать дополнительный параметр типа Integer (как если бы это был индекс свойства). При обращении к конкретному свойству в качестве этого параметра будет присылаться константа, обозначенная после ключевого слова index. Для методов read это должен быть последний параметр, для методов write – предпоследний (последний – устанавливаемое значение свойства).

<спецификаторы> – последовательность из спецификаторов read, write, stored, default или nodefault и implements, где хотя бы один из спецификаторов read или write обязан присутствовать. Спецификаторы stored, default или nodefault будут рассмотрены в теме «VCL». Спецификатор implements относится к теме «Интерфейсы» и его рассмотрение выходит за рамки курса.

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

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

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

Свойства, для которых указан только спецификатор read, можно только читать, а в свойства, для которых указан только спецификатор write, можно только записывать значения.

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

Объявление объекта

Объект или экземпляр класса (в терминах Delphi) – это переменная типа класс. Соответственно, объявление объекта (а если быть точным, ссылки на объект) может располагаться в разделе var (любом – глобальном или локальном) или в пределах объявления другого класса или записи. Очень важно помнить, переменная-объект – это всего лишь адрес памяти. Мало объявить переменную. Прежде чем использовать объект, его нужно создать (то есть выделить память и инициализировать) при помощи конструктора. Из этого логически вытекает, что операция присваивания не копирует объект, а лишь адрес, то есть после ее выполнения две переменные указывают на один и тот же объект.

Ex.

var

O1, O2: TSomeClass;

O1 := TSomeClass. Create;

O2 := O1; // обе переменные указывают на один и тот же объект в памяти

Доступ к элементам класса извне класса осуществляется через оператор «точка» (.).

Для всех элементов:

<имя объекта>.<имя элемента>

Для классовых элементов также возможна (и является желательной) формой обращения:

<имя класса>.<имя элемента>

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

Ex.

Доступ к элементам класса:

program Project1;

{$APPTYPE CONSOLE}

uses

SysUtils;

type

T1 = class

private

a: Integer;

public

procedure SomeMethod;

end;

{ T1 }

procedure T1.SomeMethod;

begin

Writeln(a); // доступ к элементам класса из его же метода

end;

var

O1: T1;

begin

O1 := T1.Create;

O1.SomeMethod; // доступ к элементам класса извне

end.

Наследование

Существует два способа многократного использования кода класса – собственно использование и наследование. К использованию можно отнести:

1.  Объект другого класса (используемого) присутствует как поле данного класса;

2.  Объект используемого класса присылается методу данного класса в качестве параметра или доступен глобально;

3.  Объект используемого класса создается в рамках работы методов данного класса.

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

Перейдем к наследованию. В Delphi класс может иметь только одного предка. Синтаксис наследования уже был описан. Что же именно наследуется? Наследуются все элементы родительского класса. Однако элементы с некоторыми атрибутами видимости становятся недоступными для использования. Однако физически они присутствуют и унаследованные видимые методы и свойства, ссылающиеся на невидимые элементы, будут работать. В отношении дальнейшего наследования атрибуты сохраняют свою область видимости, если ее специально не изменить, еще раз описав их с другим атрибутом видимости. Для полей и методов фактически это будет означать добавление нового поля или метода (соответственно) и сокрытие старого. В отношении свойств есть два механизма – перекрытие (override) и переопределение (redeclaration). Первый позволяет изменить видимость и/или спецификаторы свойства. Оно может не включать спецификаторов вообще. Второй – полностью изменить свойство, вплоть до типа (фактически создать новое). В первом случае не указывается тип свойства, во втором – указывается, и так как это фактически новое свойство, оно должно удовлетворять всем требованиям к описанию нового свойства. Для методов описанных с помощью особых директив virtual и dynamic тоже есть возможность перекрытия (то есть новый метод именно заменяет старый, а не прячет его, подробнее это будет рассмотрено в теме «Полиморфизм. Статические, динамические и виртуальные методы. Перегрузка методов»).

Ex.

Области видимости элементов класса, описание элементов класса (закомментированные строки признаны компилятором некорректными):

unit Unit1;

interface

type

TBaseClass = class

strict private

a: Integer;

private

b: Double;

strict protected

c: String;

protected

d: Boolean;

public

procedure e(Obj: TBaseClass);

end;

TDerivedClass = class(TBaseClass)

procedure f(Obj: TBaseClass);

procedure g(Obj: TDerivedClass);

end;

implementation

{ TBaseClass }

procedure TBaseClass. e(Obj: TBaseClass);

begin

a := obj. a;

b := 0.01;

c := 'abcd';

d := True;

Obj. a := 2;

Obj. b := 0.02;

Obj. c := 'dcba';

Obj. d := True;

end;

{ TDerivedClass }

procedure TDerivedClass. f(Obj: TBaseClass);

begin

//a := 1;

b := 0.01;

c := 'abcd';

d := True;

//Obj. a := 2;

Obj. b := 0.02;

//Obj. c := 'dcba';

Obj. d := True;

end;

procedure TDerivedClass. g(Obj: TDerivedClass);

begin

//a := 1;

b := 0.01;

c := 'abcd';

d := True;

//Obj. a := 2;

Obj. b := 0.02;

Obj. c := 'dcba';

Obj. d := True;

end;

var

Obj1: TBaseClass;

Obj2: TDerivedClass;

begin

//Obj1.a := 2;

Obj1.b := 0.02;

//Obj1.c := 'dcba';

Obj1.d := True;

//Obj2.a := 2;

Obj2.b := 0.02;

//Obj2.c := 'dcba';

Obj2.d := True;

end.

unit Unit2;

interface

uses Unit1;

type

TOtherDerivedClass = class(TBaseClass)

procedure f(Obj: TBaseClass);

procedure g(Obj: TOtherDerivedClass);

end;

implementation

{ TOtherDerivedClass }

procedure TOtherDerivedClass. f(Obj: TBaseClass);

begin

//a := 1;

//b := 0.01;

c := 'abcd';

d := True;

//Obj. a := 2;

//Obj. b := 0.02;

//Obj. c := 'dcba';

//Obj. d := True;

end;

procedure TOtherDerivedClass. g(Obj: TOtherDerivedClass);

begin

//a := 1;

//b := 0.01;

c := 'abcd';

d := True;

//Obj. a := 2;

//Obj. b := 0.02;

Obj. c := 'dcba';

Obj. d := True;

end;

var

Obj1: TBaseClass;

Obj2: TOtherDerivedClass;

begin

//Obj1.a := 2;

//Obj1.b := 0.02;

//Obj1.c := 'dcba';

//Obj1.d := True;

//Obj2.a := 2;

//Obj2.b := 0.02;

//Obj2.c := 'dcba';

Obj2.d := True;

end.

Совместимость по присваиванию. Преобразование и приведение объектов.

В отношении совместимости по присваиванию переменные-объекты проявляют следующие свойства:

1.  Переменной-объекту типа класса-предка можно присвоить объект типа класса-потомка. В связи с этим важно заметить, что формальный тип переменной-объекта может не совпадать с фактическим.

2.  Переменной-объекту типа класса-потомка нельзя непосредственно присвоить объект, формально имеющий тип класса-предка, однако, если фактический тип соответствует пункту 1, то присваивание возможно, для этого необходимо применить один из двух механизмов:

1)  Преобразование типов (конструкция <тип>(<объект>)). Операция не проверяет совместимость типов (в рамках переменных-объектов), использовать ее следует с особой осторожностью, так как это может повлечь трудно обнаружимые ошибки в программе.

2)  Приведение типов (операция as в конструкции <объект> as <класс>). Операция более безопасна, ибо в случае несовместимости вызывает исключение.

Проверить фактический тип объекта позволяет операция is:

<объект> is <класс>

возвращает истину, если объект фактически принадлежит указанному классу или какому-либо из его наследников.

Ex.

Приведение и преобразование типов, операция is

type

T1 = class

public

a: Integer;

end;

T2=class(T1)

public

b: Integer;

end;

T3 = class(T2)

public

c: Integer;

end;

var

O1: T1;

O2: T2;

begin

O1 := T2.Create; // допустимо, см. п. 1

O2 := O1; // ошибка на этапе компиляции, см. п. 2

O2 := T2(O1); // верное преобразование, см. п. 2.1

O2 := O1 as T2; // верное приведение, см. п. 2.2

O1.a := 1; // верно

T2(O1).b := 2; // верно

T3(O1).c := 3; { не вызывает ошибок компиляции и исключений, но неверно, см. п. 2.1}

(O1 as T2).b := 4; // верно, см. п. 2.2

(O1 as T3).c := 5; // вызывает исключение, см. п. 2.2

If (O1 is T1)

then Writeln('Ok')

else Writeln('Wrong!'); // Вывод «Ok»

If (O1 is T2)

then Writeln('Ok')

else Writeln('Wrong!'); // Вывод «Ok»

If (O1 is T3)

then Writeln('Ok')

else Writeln('Wrong!'); // Вывод «Wrong!»

end.