Классификации
Как в науке, так и в быту, важную роль играет классификация — разделение изучаемых объектов на группы (классы), объединённые общими признаками. Прежде всего это нужно для того, чтобы не запутаться в большом количестве данных и не описывать каждый объект заново.
Например, есть много видов фруктов1 (яблоки, груши, бананы, апельсины и т. д.), но все они обладают некоторыми общими свойствами. Если перевести этот пример на язык ООП, класс Яблоко — это подкласс (производный класс, класс-наследник, потомок) класса Фрукт, а класс Фрукт — это базовый класс (суперкласс, класс-предок) для класса Яблоко (а также для классов Груша, Слива, Апельсин и др.).
Стрелка на схеме (рис. 7.9) обозначает наследование. Например, класс Яблоко — это наследник класса Фрукт.
Фруктами называют сочные съедобные плоды деревьев и кустарник
Классический пример научной классификации — классификация животных или растений. Как вы знаете, она представляет собой иерархию (многоуровневую структуру). Например, горный клевер относится к роду Клевер семейства Бобовые класса Двудольные и т. д. Говоря на языке ООП, класс Горный клевер — это наследник класса Клевер, а тот, в свою очередь, — наследник класса Бобовые, который является наследником класса Двудольные и т. д.
Класс Б является наследником класса А, если можно сказать, что Б — это разновидность А.
Например, можно сказать, что яблоко — это фрукт, а горный клевер — одно из растений семейства Двудольные. В то же время мы не можем сказать, что «двигатель — это разновидность машины», поэтому класс Двигатель не является наследником класса Машина. Двигатель — это составная часть машины, поэтому объект класса Машина содержит в себе объект класса Двигатель. Отношения между двигателем и машиной — это отношение * часть — целое».
Иерархия логических элементов
Рассмотрим такую задачу: составить программу для моделирования управляющих схем, построенных на логических элементах (см. главу 3 в учебнике для 10 класса). Нам нужно «собрать» заданную схему и построить её таблицу истинности.
Как вы уже знаете, перед тем, как программировать, нужно выполнить объектно-ориентированный анализ. Все объекты, из которых состоит схема, — это логические элементы, однако они могут быть разными (НЕ, И, ИЛИ и другие). Попробуем выделить общие свойства и методы всех логических элементов.
Ограничимся только элементами, входа. Тогда иерархия классов может в рис. 7.10.
Итак, для того чтобы не описывать несколько раз одно и то же, классы в программе должны быть построены в виде иерархии. Теперь можно дать определение объектно-ориентированного программирования.
Объектно-ориентированное программирование — это такой подход к программированию, при котором программа представляет собой множество взаимодействующих объектов, каждый из которых является экземпляром определённого класса, а классы образуют иерархию наследования.
Построим первый вариант описания класса Логический элемент (TLogElement). Обозначим его входы как Inl и 1п2, а выход назовём Res (от англ. result — результат) (рис. 7.11). Здесь состояние логического элемента определяется тремя величинами (Inl, In2 и Res), это позволяет на основе того же самого базового класса моделировать и элементы с памятью (например, триггеры), а не только статические элементы (как НЕ, И, ИЛИ и т. п.).
In 1 (вход 1)
!п2 (вход 2)
Res [результат)
Любой логический элемент должен уметь вычислять значение выхода по известным входам, для этого введём в класс метод calc: type
TLogElement « class Inl, In2: boolean; Res: boolean; procedure calc; end;
В таком варианте все данные открытые (общедоступные). Чтобы защитить внутреннее устройство объекта, скроем внутренние поля (добавим при этом в их названия первую букву «F») и введём свойства:
type
TLogElement = class private
FInl, FIn2: boolean; FRes: boolean;
procedure setlnl(newlnl: boolean); procedure setln2 (newln2: boolean); procedure calc; public
property Inl: boolean read FInl write setlnl; property In2: boolean read FIn2 write setln2; property Res: boolean read FRes; end;
Обратите внимание, что свойство Res — это свойство только для чтения, и другие объекты не могут его менять. Кроме того, мы поместили процедуру calc в скрытый раздел (private), потому что пересчёт результата должен выполняться автоматически при изменении любого входного сигнала (другие объекты не должны об этом беспокоиться).
Несложно написать процедуру setlnl (и аналогичную ей процедуру setln2), в ней новое входное значение присваивается полю и сразу пересчитывается результат:
procedure TLogElement.setlnl(newlnl: boolean); begin
FInl:-newlnl;
calc; end;
Если внимательно проанализировать построенное описание класса, можно выявить несколько проблем. Во-первых, элемент НЕ имеет только один вход, поэтому не хотелось бы для него открывать доступ к свойству 1п2 (это не нужно и может привести к ошибкам).
Во-вторых, процедуру calc невозможно написать, пока мы не знаем, какой именно логический элемент моделируется. Вместе с тем мы знаем, что такую процедуру имеет любой логический элемент, т. е. она должна принадлежать именно классу TLogElement. Здесь можно написать процедуру-взаглушку» (которая ничего не делает):
procedure TLogElement.calc;
begin
end;
Но нужно как-то дать возможность классам-наследникам изменить этот метод так, чтобы он выполнял нужную операцию. Такой метод называется виртуальным. Более точное определение этого понятия мы дадим несколько позже.
Классы-наследники могут по-разному реализовывать один и тот же метод. Такая возможность называется полиморфизмом.
Полиморфизм (от греч. noXv — много, и цорфп. — форма) — это возможность классов-наследников по-разному реализовывать метод, описанный для класса-предка.
Мы уже говорили о том, что метод calc не нужно делать общедоступным (public). В то же время его нельзя делать закрытым (private), потому что в этом случае он не будет доступен классам-наследникам. В таких случаях в описании класса используется третий блок (кроме private и public), который называется protected (защищенный). Данные и методы в этом блоке доступны для классов-наследников, но недоступны для других классов. В этот же блок protected мы переместим и объявление свойства 1п2 — оно будет скрыто для элемента «НЕ», а элементы с двумя входами его «откроют» (чуть позже).
type
TLogElement = class private
FInl, FIn2: boolean; FRes: boolean;
procedure setlnl(newlnl: boolean);
procedure setln2(newln2: boolean); protected
property In2: boolean read FIn2 write setln2;
procedure calc; virtual; abstract; public
property Inl: boolean read FInl write setlnl;
property Res: boolean read FRes; end;
Обратите внимание на объявление метода calc: после него стоят слова virtual (виртуальный) и abstract (в переводе с англ. — абстрактный). Описатель virtual говорит о том, что метод calc — виртуальный, и классы-наследники могут его переопределять. Как уже отмечалось, мы должны объявить этот метод (ввести его в описание класса), поскольку он должен быть у любого логического элемента. В то же время невозможно написать процедуру calc, пока неизвестен тип логического элемента. Такой метод называется абстрактным и обозначается описателем abstract. Для абстрактного метода не нужно ставить «заглушку».
Абстрактный метод — это метод класса, который объявляется, но не реализуется в классе.
Более того, не существует логического элемента «вообще», как не существует «просто фрукта*, не относящегося к какому-то виду. Такой класс в ООП называется абстрактным. Его отличительная черта — хотя бы один абстрактный (нереализованный)
метод.
Абстрактный класс — это класс, содержащий хотя бы один абстрактный метод.
Итак, полученный класс TLogElement — это абстрактный класс (компилятор определит это автоматически). Его можно использовать только для разработки классов-наследников, создать в программе объект этого класса нельзя.
Чтобы класс-наследник не был абстрактным, он должен переопределить все абстрактные методы предка, в данном случае метод calc. Как это сделать, вы увидите в следующем пункте.
Иерархия классов
Классы-наследники
Теперь займёмся классами-наследниками от TLogElement. Поскольку у нас будет единственный элемент с одним входом (НЕ), сделаем его наследником прямо от TLogElement (не будем вводить специальный класс «элемент с одним входом»).
type
TNot = class(TLogElement)
procedure calc; override; end;
После слова class в скобках указано название базового класса. Все объекты класса TNot обладают всеми свойствами и методами класса TLogElement.
Новый класс переопределяет метод calc, на это указывает слово override (в переводе с англ. — перекрыть). Заметим, что у базового класса TLogElement этот метод не реализован — он абстрактный, поэтому в данном случае мы фактически программируем метод, объявленный в базовом классе. Для элемента «НЕ» он выглядит очень просто:
procedure TNot.calc; begin
FRes:=not FInl; end;
Класс TNot уже не абстрактный, потому что абстрактный метод предка переопределён и теперь известно, что делать при вызове метода calc. Поэтому можно создавать объект этого класса и использовать его:
var n: TNot;
n:=TNot.Create; п.Inl:=False; writeln(п.Res);
Остальные элементы имеют два входа и будут наследниками класса.
TLog2In = class(TLogElement) public
property In2; end;
Единственное, что делает этот класс, — переводит свойство 1п2 в раздел public, т. е. делает его общедоступным. Отметим, что видимость можно только повышать, т. е. нельзя, например, в наследнике сделать общедоступное свойство класса-предка закрытым или защищенным.
Класс TLog2In — это тоже абстрактный класс, потому что он не переопределил метод calc. Это сделают его наследники TAnd (элемент И) и ТОг (элемент ИЛИ), которые определяют конкретные логические элементы:
type
TAnd = class(TLog2In)
procedure calc; override; end; TOr = class(TLog2ln)
procedure calc; override; end;
Реализация переопределённого метода calc для элемента «И» выглядит так:
procedure TAnd.calc; begin
FRes:=FInl and FIn2; end;
Для элемента «ИЛИ» этот метод определяется аналогично.
Обратим внимание на метод setlnl, введённый в базовом классе: procedure TLogElement.setlnl(newlnl: boolean);
begin
Flnl:=newlnl;
calc; end;
В нём вызывается метод calc, который пересчитывает значение на выходе логического элемента при изменении входа. Какой же метод будет вызван, если в базовом классе TLogElement он только объявлен, но не реализован?
Проблема в том, что для вызова любой процедуры нужно знать её адрес в памяти. Для обычных методов транслятор сразу записывает в машинный код нужный адрес, потому что он заранее известен. Это так называемое статическое связывание (связывание на этапе трансляции), при выполнении программы этот адрес не меняется.
В нашем случае адрес метода неизвестен: в классе TLogElement его нет вообще, а у каждого класса-наследника адрес метода calc свой собственный. Чтобы выйти из положения, используется динамическое связывание, т. е. адрес вызываемой процедуры определяется при выполнении программы, когда уже определён тип объекта, с которым мы работаем. Такой метод нужно объявлять виртуальным, что мы и сделали ранее. Это означает не только то, что его могут переопределять наследники, но и то, что будет использоваться динамическое связывание. Теперь можно дать полное определение виртуального метода.
Виртуальный метод — это метод базового класса, который могут переопределить классы-наследники, при этом конкретный адрес вызываемого метода определяется только при выполнении программы.
Теперь мы готовы к тому, чтобы создавать и использовать построенные логические элементы. Например, таблицу истинности для последовательного соединения элементов «И» и «НЕ» можно построить так:
var elNot: TNot; elAnd: TAnd; А, В: boolean; begin
elHot:=TNot.Create; elAnd:=T And.Create; writeln{'| A | В | not(ASB) ');
write In {'--------------------------- ') ;
for A:=False to True do begin elAnd.Inl:=A;
for B:=False to True do begin elAnd.In2:=B; elNot.Inl:=elAnd.res;
writeln('I ', integer(A), ' | ', integer(B), ' | ', integer(elNot.Res))
end.
Сначала создаются два объекта — логические элементы НЕ (класс TNot) и ИЛИ (класс TAnd), Далее в двойном цикле перебираются все возможные комбинации значений логических переменных А к В, они подаются на входы элемента И, а его выход — на вход элемента НЕ. Чтобы при выводе логических значений вместо False и True выводились более компактные обозначения О и 1, значения входов и выхода преобразуются к целому типу (integer).
Модульность
Как вы знаете из главы 6, большие программы обычно разбивают на модули — внутренне связные, но слабо связанные между собой блоки. Такой подход используется как в классическом программировании, так и в ООП.
В нашей программе с логическими элементами в отдельный модуль можно вынести всё, что относится к логическим элементам. Модуль, содержащий классы логических элементов, на объектной версии языка Паскаль можно записать так: unit log_elem; 1$mode objfpc} interface type
TLogElement = class private
FInl, FIn2: boolean; FRes: boolean;
procedure setlnl(newlnl: boolean); procedure setln2 (newln2: boolean); protected
property In2: boolean read FIn2 write setln2; procedure calc; virtual; abstract; public
property Inl: boolean read FInl write setlnl; property Res: boolean read FRes; end; TNot = class(TLogElement)
procedure calc; override; end;
TLog2In - class(TLogElement) public
property In2; end; TAnd = class(TLog2In)
procedure calc; override; end; TOr - class(TLog2In)
procedure calc; override; end;
implementation
procedure TLogElement-setlnl(newlnl: boolean); begin
Flnl:=newlnl; Gale-end;
procedure TLogElement.setln2(newln2: boolean); begin
Fln2:=newln2; calc; end;
procedure TNot.calc; begin
FRes:=not FInl; end;
procedure TAnd.calc; begin
FRes:=FInl and FIn2; end;
procedure TOr.calc; begin
FRes:=FInl or FIn2;
rd;
end.
Чтобы использовать такой модуль, нужно подключить его в основной программе с помощью ключевого слова uses, после которого через запятую перечисляются все используемые модули:
program logic;
{Smode objfpc]
uses log_elera;
var elNot: TNot; elAnd: TAnd;
begin
elNot:=THot.Create; elAnd:=TAnd.Create;
Сообщения между объектами
Когда логические элементы объединяются в сложную схему, желательно, чтобы передача сигналов между ними при изменении входных данных происходила автоматически. Для этого можно немного расширить базовый класс TLogElement, чтобы элементы могли передавать друг другу сообщения об изменении своего выхода.
Для простоты будем считать, что выход любого логического элемента может быть подключён к любому (но только одному!) входу другого логического элемента. Добавим к описанию класса два поля и один метод:
type
TLogElement == class private
FNextEl: TLogElement,-
FNext I n: integer ,-
. . . public
procedure Link(nextEle nextln:
rent: TLogElement; integer);
end;
Поле FNextEl хранит ссылку на следующий логический элемент, а поле FNextln — номер входа этого следующего элемента, к которому подключён выход данного элемента. С помощью общедоступного метода Link можно связать данный элемент со следующим:
integer);
begin
FNextEl:=nextElen FNextIn:=nextIn; end;
procedu begin
Flnl:=newlnl; calc;
if FNextElOnil then
case FNextln of
1: FNextEl.Inl:=res;
2: FNextEl.In2:=res;
end;
Условие FNextElonil означает «если следующий элемент задан». Если он не был установлен, значение поля FNextEl будет равно nil и никакие дополнительные действия не выполняются.
С учётом этих изменений вывод таблицы истинности функции И-НЕ можно записать так (операторы вывода заменены многоточиями >:
elNot:=TNot.Create,■
elAnd:=TAnd.Create,■
elAnd.Link(elNot, 1) ;
for A:=False to True do begin elAnd.Inl:=A;
for B:=False to True do begin elAnd.In2:=B;
end; end;
Обратите внимание, что в самом начале мы установили связь элементов И и НЕ с помощью метода Link (связали выход элемента И с первым входом элемента НЕ). Далее в теле цикла обращения к элементу НЕ нет, потому что элемент И автоматически сообщит ему об изменении своего выхода.
Вопросы и задания
- Что такое классификация? Зачем она нужна? Приведите примеры.
- В каком случае можно сказать: «Класс Б — наследник класса А*,
а когда: «Объект класса А содержит объект класса Б»? Приведите
пример - Что такое иерархия классов.
- На примере класса TLogElement (пример из параграфа) покажите,как выполнена инкапсуляция.
- Что такое виртуальный метод?
- Что такое полиморфизм?
- Что такое абстрактный класс? Почему нельзя создать объект этого класса?
- Как транслятор определяет, что тот или иной класс — абстрактный?
- Что нужно сделать, чтобы класс-наследник абстрактного класса не был абстрактным?
- Зачем нужен описатель protected? Чем он отличается от private и public?
- Что означает описатель override?
- Какие преимущества даёт применение модулей в программе?
- Из каких частей состоит каждый модуль? Что включают в каждую
- Можно ли всё содержимое модуля включить в секцию interface?
- Можно ли всё содержимое модуля включить в секцию implementation? Чем это плохо?
- Объясните, как объекты могут передавать сообщения друг другу.
Подготовьте сообщение
а) «Иерархия классов в языке Си»
б) «Иерархия классов в языке Python»