Электронный учебник

§ 50. Иерархия классов

Классификации

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

Например, есть много видов фруктов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 (связали выход элемен­та И с первым входом элемента НЕ). Далее в теле цикла обраще­ния к элементу НЕ нет, потому что элемент И автоматически со­общит ему об изменении своего выхода.

Вопросы и задания

  1. Что такое классификация? Зачем она нужна? Приведите примеры.
  2. В каком случае можно сказать: «Класс Б — наследник класса А*,
    а когда:   «Объект класса А содержит объект класса Б»? Приведите
    пример
  3. Что такое иерархия классов.
  4. На примере класса TLogElement (пример из параграфа) покажите,как выполнена инкапсуляция.
  5. Что такое виртуальный метод?
  6. Что такое полиморфизм?
  7. Что такое абстрактный класс? Почему нельзя создать объект этого класса?
  8. Как транслятор определяет, что тот или иной класс — абстрактный?
  9. Что нужно сделать, чтобы класс-наследник абстрактного класса не был абстрактным?
  10. Зачем нужен описатель protectedЧем он отличается от private и public?
  11. Что означает описатель override?
  12. Какие преимущества даёт применение модулей в программе?
  13. Из каких частей состоит каждый модуль? Что включают в каждую
  14. Можно ли всё содержимое модуля включить в секцию interface?
  15. Можно     ли     всё     содержимое     модуля     включить     в     секцию implementationЧем это плохо?
  16. Объясните, как объекты могут передавать сообщения друг другу.

Подготовьте сообщение

а) «Иерархия классов в языке Си»

б) «Иерархия классов в языке Python»


 

Возрат в главу

 

 

Block title

Вход на сайт

Поиск

Календарь

«  Май 2024  »
ПнВтСрЧтПтСбВс
  12345
6789101112
13141516171819
20212223242526
2728293031

Архив записей

Статистика


Онлайн всего: 1
Гостей: 1
Пользователей: 0