Введение в Классы и Объекты

Материал из wikiru.visual-prolog.com

Оригинал Introduction to Classes and Objects

Перевод выполнен участником форума ProgZ c ником Krys

Редакция Victor Yukhtenko

Целью этого руководства является введение в основы объектно-ориентированного программирования в VIP, и демонстрация примеров синтаксиса.

Объектная модель

Основными элементами объектной модели VIP являются объекты, объектные типы и классы.

Основными средствами при работе с ними являются интерфейсы (interfaces), объявления классов(class declarations) и исполнения (implementations).

Интерфейсом называется именованный набор объявлений предикатов и оформленный в виде раздела interface. Интерфейсы описывают доступ к объектам извне объекта. Интерфейсы представляют собой объектные типы.

Рассмотрим следующее определение интерфейса:

interface person
predicates
  getName : () -> string Name.
  setName : (string Name).
end interface person

Это определение интерфейса с именем person. В этом примере объекты типа person имеют два предиката getName и setName, которые объявляются в этом разделе.

Интерфейсы только определяют типы объектов, классы порождают объекты. Класс имеет раздел class объявления (declaration) и раздел implement исполнения. Класс, который порождает объект person, можно объявить так:

class person : person
constructors
  new : (string Name).
end class person

Это объявление класса с именем person, который создает объект типа person. Класс имеет конструктор (contructor) с именем new, который при вызове и порождает объект типа person, передавая при этом новому объекту строку с именем Name. У класса должен быть раздел исполнения (implement), который может выглядеть так:

implement person
facts
  name : string.
clauses
  new(Name) :- 
    name := Name.
clauses
  getName() = name.
clauses
  setName(Name) :- 
    name := Name.
end implement person

Как видно, в этом разделе должны присутствовать определения для каждого из видимых извне (public) предикатов и конструкторов, т.е. для new, getName и setName. В разделе implement можно, кроме того, объявить и определить дополнительные локальные элементы, доступ к которым возможен только из самого раздела implement. Так, класс содержит объявление факта-переменной name, который будет использоваться для хранения имени персоны.

Каждый из объектов при создании получает свою собственную реализацию факта-переменной. Выше приведенные клозы будут ссылаться только на конкретную реализацию факта-переменной внутри объекта. Мы говорим, что этот предикат является объектным предикатом (object predicate), а факт является объектным фактом (object fact).

Элементы класса

Класс может иметь элементы, общие для всех объектов этого класса. Расширим пример, добавив в него код, который позволяет подсчитывать количество созданных объектов класса person. Счетчик будет увеличиваться каждый раз, когда создается новый объект, и никогда не будет уменьшаться. Расширим объявление класса, добавив предикат getCreatedCount (получить значение счетчика созданий), который сможет возвращать текущее значение счетчика:

class person : person
constructors
  new : (string Name).
predicates
  getCreatedCount : () -> unsigned Count.
end class person

Отметим, что видимый извне предикат класса, объявлен в объявлении класса, тогда как видимый извне объектный предикат объявлен в интерфейсе. Из этого правила нет исключений; невозможно объявить объектный предикат в объявлении класса, и невозможно объявить предикат класса в разделе интерфейса.

После объявления, каждый предикат должен быть определен в разделе implement (имплементация) класса. То же самое нужно сделать для факта, который будет хранить счетчик. Этот факт должен быть фактом класса, т.е. быть общим для всех объектов. В разделе implement класса можно объявить локальные (private) как элементы объекта, так и элементы класса. Для объявления элементов класса нужно вставить ключевое слово class перед соответствующим объявлением. Например, имплементация класса person может быть представлена таким образом:

implement person
class facts
  createdCount : unsigned := 0.
 
clauses
  getCreatedCount() = createdCount.
 
facts
  name : string.
 
clauses
  new(Name) :-
    name := Name,
    createdCount := createdCount+1.
 
clauses
  getName() = name.
 
clauses
  setName(Name) :-
    name := Name.
end implement person

Здесь мы добавили факт класса createdCount и инициализировали его нулевым значением. Мы также добавил клоз для предиката getCreatedCount, который возвращает текущее значение переменной createdCount. И, наконец, мы добавили в текст клоза-конструктора строку, которая увеличивает createdCount на единицу.

Отметим, что в конструкторе два присваивания имеют похожий вид, но одно присваивание createdCount := createdCount+1, оперирующее с фактом класса, меняет состояние класса, а другое - name := Name, оперирующее с фактом объекта, меняет состояние объекта.

Модули

Это особые варианты классов, которые вообще не порождают объектов, поэтому они действуют, как модули, а не как классы. Класс, не конструирующий объекты (т.е., модуль), объявляется без указания объектного типа:

class io % тип не указан!
predicates
  write : (string ToWrite).
  write : (unsigned ToWrite).
end class io

Такой класс не может порождать объекты.

Создание и доступ

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

goal
  P = person::new("John"),
  Name = P:getName(),
  io::write(Name).

В первой строке происходит вызов конструктора (constructor) new класса person. Созданный объект запоминается в переменной P.

Во второй строке, переменной Name присваивается результат исполнения объектного предиката getName объекта P.

В последней строке происходит вызов предикат write класса io с передачей ему имени Name.

Отметим, что на имена классов ссылаются с помощью символа ‘::’, например, person::new(...). А объектные предикаты ссылаются на объекты с использованием символа ‘:’, напрмер, P:getName. Наконец, отметим, что конструкторы являются функциями, которые возвращают объекты, даже если эти конструкторы не объявлены, как функции. Тип возвращаемого значения определяется исходя из объявления класса, к которому применяется конструктор.

Интерфейсы как объектные типы

Как уже говорилось, интерфейсы являются объектными типами. Это следует понимать буквально, вы можете использовать интерфейсы везде, где используются обычные необъектные типы. Например, в объявлении предиката:

class mail
predicates
    sendMessage : (person Recipient, string Message).
end class mail

Предикат mail::sendMessage имеет типы аргументов: person (т.е., имя интерфейса) и string.

Разнообразие имплементаций

Можно создать несколько различных классов, которые могут создавать одинаковые объекты person. Вы просто объявляете классы с разделами implement, которые содержат свои конструкторы объектов person. Разделы implement могут быть различными. Например, можно создать класс, который сохраняет персоны в базе данных, а не в факт-переменной. Вот, объявление такого класса:

class personInDB : person
constructors
  new : (string DatabaseName, string Name).
end class personInDB

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

implement personInDB
 
facts
  db : myDatabase.
  personID : unsigned.
clauses
  new(DatabaseName, Name) :-
    db := myDatabase::getDB(DatabaseName),
    personID := db:storePerson(Name).
clauses
  getName() = db:getPersonName(personID).
clauses
  setName(Name) :- 
    db:setPersonName(personID, Name).
end implement personInDB

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

Категорированный полиморфизм

Объекты одного типа могут использоваться в одинаковых контекстах, независимо от различия в разделах implement. Можно, например, послать сообщение некоей персоне, используя почтовый класс, объявленный заранее, не обращая внимание, на то, каким конструктором объект person был создан: person или personInDB:

goal
  P1 = person::new("John"),
  mail::sendMessage(P1, "Hi John, ..."),
  P2 = personInDB::new("Paul"),
  mail::sendMessage(P2, "Hi Paul, ...").

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

Отметим, что предикат mail::sendMessage принимает объекты любого класса person, поэтому этот предикат является, в определенном смысле, полиморфным.

Supports (поддерживает) - как расширение типов

Пусть наша программа имеет дело со специфическими персонами, а именно, - с пользователями программ. Пользователи являются персонами с именами и, кроме того, они имеют пароли. Создадим новый интерфейс (тип) объекта для пользователей, который бы устанавливал, что пользователь является персоной с паролем. Для этого используем квалификацию supports:

interface user supports person
predicates
  trySetPassword : (string Old, string New, string Confirm) determ.
  validatePassword : (string Password) determ.
end interface user

Интерфейс user поддерживает интерфейс person. Это означает, что:

  • Объекты типа user будут исполнять также предикаты, объявленные в интерфейсе person, а именно, getName и setName.
  • Объекты типа user являются одновременно объектами типа person и, поэтому, могут использоваться в тех же контекстах, что и объекты person.

Допустим, что имеется некоторый класс user.

class user : user
constructors
    new : (string Name, string Password).
end class user

Тогда объекты этого класса могут быть использованы предикатом mail::sendMessage:

goal
  P = user::new("Benny", "MyCatBobby"),
  mail::sendMessage(P, "Hi Benny, ...").

Поддержка интерфесом других интерфейсов означает, что:

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

Квалификация supports порождает, таким образом, иерархию подтипов. Мы говорим, что user является подтипом person.

object - первичный тип

Интерфейс object неявно поддерживает все интерфейсы. Интерфейс object является интерфейсом, не имеющим содержания, т.е. в нем нет объявленных предикатов. Объектный интерфейс object поддерживает, прямо или непрямо, любой другой интерфейс, поэтому все объекты имеют тип объектного интерфейса object. Объектный интерфейс object является супертипом для любого объекта.

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

Для применения класса user, можно использовать свойства одного из наших классов person. Класс user похож на класс person, за исключением того, что user имеет дело еще и с паролями. Хотелось бы, чтобы наш класс user наследовал раздел implement из класса person. Это можно сделать с помощью квалификации inherits, как показано ниже:

implement user
    inherits person
 
facts
    password : string.
clauses
    new(Name, Password) :-
        person::new(Name),
        password := Password.
clauses
    trySetPassword(Old, New, Confirm) :-
        validatePassword(Old),
        New = Confirm,
        password := New.
clauses
    validatePassword(Password) :-
        password = Password.
end implement user

Раздел implement подтверждает, что этот класс наследует (inherits) свойства класса person. Отсюда следует, что:

  • Объект person встраивается в каждый созданный объект user.
  • Все предикаты, объявленные в интерфейсе person, могут быть наследованы из класса person в класс user.

Когда наследуется какой-нибудь предикат, определять его в разделе implement не нужно, т.к. наследуется определение предиката из раздела implement наследуемого класса.

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

implement user
facts
  person : person.
  password : string.
clauses
  new(Name, Password) :-
    person := person::new(Name),
    password := Password.
clauses 
  getName() = person:getName().
clauses
  setName(Name) :- 
    person:setName(Name).
end implement user

Действие этого кода похоже на действие предыдущего фрагмента, но размер текста увеличился. В этом фрагменте наследования из класса person не происходит. Вместо этого создан объект person и он сохранен в одноименном факте-переменной. Вместо наследования кода getName и setName они просто переопределены и теперь обращаются за решением к объекту из факт-переменной. Такие обращения за решением можно сократить с помощью ключевого слова delegate. Код в этом случае сократится.

implement user
  delegate interface person to person
facts
  person : person:=erroneous.
  password : string.
clauses
  new(Name, Password) :-
    person := person::new(Name),
    password := Password.
end implement user_class

Функционирование то же, но:

  • Вызовы предикатов, относящихся к объекту person транзитом передаются в него
  • Mожно динамически изменить значение в факт-переменной на другой объект, например, на объект класса personInDB простым присваиванием нового объекта факту-переменной.

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

VIP дает возможность многократного наследования, т.е., вы можете одновременно наследовать многие классы.

Заключение

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

  • Объекты могут поддерживать добавочные интерфейсы в разделе implement.
  • Предикаты завершения, освобождающие занятую память.
  • и т.д.

Ссылки