Русский
Русский
English
Статистика
Реклама

Metaide

Композиция вместо наследования в языке программирования Delight

22.04.2021 18:21:21 | Автор: admin

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

Сразу к делу

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

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

class BaseBehavior  unitPos: UnitPos [shared]  fn DoTurn [virtual]class PathBuilder  unitPos: UnitPos [shared]  fn Moving:boolean [virtual]    ...  fn BuildPath(x:int, y:int) [virtual]    ...  // ... and some more helper functions ...

BaseBehavior - отвечает за базовое поведение существа, сам класс не имеет логики, только необходимые декларации.

PathBuilder - класс который отвечает за поиск пути по земле (включая обход препятствий).

Модификатор [shared] означает что это поле будет общим для всех подклассов финального класса.

Дальше пропишем классы, в которых уже есть непосредственно логика поведения:

class SimpleBehavior  base: BaseBehavior [shared]  path: PathBuilder [shared]    fne DoTurn // override of BaseBehavior.DoTurn    if path.Moving = false      path.BuildRandomPathclass AgressiveBehavior  open SimpleBehavior [shared]  fne DoTurn // override of SimpleBehavior.DoTurn    d: float = path.GetDistance(player.x, player.y) // get distance from this unit to player    if d < 30      path.BuildPath(player.x, player.y) // run to player    else      nextFn // inherited call to next DoTurnclass ScaredBehavior  open SimpleBehavior [shared]  fne DoTurn // override of SimpleBehavior.DoTurn    d: float = path.GetDistance(player.x, player.y) // get distance from this unit to player    if d < 50      path.BuildPathAwayFrom(player.x, player.y) // run away from player    else      nextFn // inherited call to next DoTurn

Здесь все просто:

SimpleBehavior - существо будет перемещаться по карте по случайным координатам.

AgressiveBehavior - если игрок находиться близко, то существо бежит к нему. Иначе управление передаеться в SimpleBehavior.

ScaredBehavior - если игрок находиться недалеко, то существо отбегает от него или двигаеться согласно SimpleBehavior.

open - означает открытое поле класса заданного типа но без имени.

fne - перегрузка (override) виртуальной функции.

nextFn - виртуальный вызов следующей в цепочке функции.

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

class UncertainBehavior  open AgressiveBehavior [shared]  open ScaredBehavior [shared]

Здесь уже включается "магия" композиции. В этом простом коде, при вызове DoTurn, управление сначала передаётся в AgressiveBehavior.DoTurn. Если игрок близко, то существо побежит к нему. Если нет, то управление переходит к ScaredBehavior.DoTurn - если игрок недалеко, то существо убегает от него. Если нет, то дальше вызывается SimpleBehavior.DoTurn и существо просто бродит по карте.

На этом коде уже можно создать существ Волка (AgressiveBehavior), Зайца (ScaredBehavior) и Кошку (UncertainBehavior). Но что делать для других видов существ? Летающих или плавающих? Или комбинированных? В ООП подобная иерархия уже не сработает. Зато очень помогает композиция. Сначала создадим новые классы для поиска пути в разных средах:

class PathBuilder_air // поиск пути по воздуху  path: PathBuilder [shared]  fne BuildPath(x:int, y:int)    ...class PathBuilder_water // поиск пути в воде  path: PathBuilder [shared]  fne BuildPath(x:int, y:int)    ...

А дальше просто подменим этими классами уже существующий код поведения:

class Shark  open PathBuilder_water [shared]  open AgressiveBehavior [shared]

В этом классе "Акулы", сначала создаётся класс поиска пути по воде, дальше используется код с AgressiveBehavior, только учитывая, что класс PathBuilder общий (shared), то в AgressiveBehavior (как и в SimpleBehavior) будет использоваться PathBuilder_water (так как он был объявлен ранее чем обычный PathBuilder). Соответственно вся логика AgressiveBehavior сохранилась, но поиск пути будет работать уже по воде. Таким же способом, просто перебирая классы-компоненты и используя минимум кода, можно создать существ с разным поведением в разных средах обитания:

class Fish  open PathBuilder_water [shared]  open ScaredBehavior [shared]class Eagle  open PathBuilder_air [shared]  open UncertainBehavior [shared]class Pigeon  open PathBuilder_air [shared]  open ScaredBehavior [shared]class Wolf  open AgressiveBehavior [shared]

Как видим, суть компонентно-ориентированного программирования состоит в создании небольших классов-компонентов и правильной комбинации этих классов в финальном объекте-сущности.

Основы композиции в Delight

Объявление простого класа в Delight выглядит следующим образом:

class NonVirtualClass  val: OtherClass  fn SomeFn    Trace('Hello world')

Здесь val является обычным членом класса, с типом OtherClass.

Функции, как и в ООП языках могут быть виртуальными, для этого используется модификатор [virtual]

fn SomeFn [virtual]  Trace('Hello virtual world')

и перегружаться/дополняться с помощью ключевого слова fne (вместо fn)

fne SomeFn  Trace('Hello overrided world')

А вот синтаксис наследования (вернее композиции) сильно отличается от классических языков. В случае, если класс хочет перегрузить функцию своего базового класса, он должен объявить базовый класс с модификатором [shared] (общий), и использовать fne для перегрузки функции:

class BaseClass  fn SomeFn [virtual]    Trace('Hello virtual world')class NewClass  base: BaseClass [shared]  fne SomeFn    Trace('Hello overrided world')    nextFn

Ключевое слово nextFn вызовет следующую функцию в цепочке виртуальных вызовов.

Для примера похожий (но не эквивалентный) код на С++

class BaseClass{public:  virtual void SomeFn()  {    Trace('Hello virtual world');  }};class NewClass : public virtual BaseClass{  virtual void SomeFn() override  {    Trace('Hello overrided world');    BaseClass::SomeFn();  }};

В классе может быть множество полей с модификатором [shared], что соответствует концепции множественного наследования. Более того, shared поля одного типа могут повторяться в любом месте иерархии класса, но при этом в финальном объекте, независимо от количества [shared] деклараций одного типа, создастся только один общий объект этого типа, а все [shared] поля соответствующего типа во всех общих классах будут содержать только ссылки на этот объект (вернее запись в vtable).

Таким образом в коде:

class Base  val: intclass ClsA  base: Base [shared]class ClsB  base: Base [shared]class ClsC  a: ClsA [shared]  b: ClsB [shared]

при создании класса ClsC, этот объект будет содержать три базовых объекта в единичном экземпляре (Base, ClsA, ClsB) и значение val будет всегда одинаковым (общим) для всех этих объектов. Подобный подход соответствует виртуальному наследованию классов, которое используется в С++.

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

class ClsA  open Base [shared]  fne Constructor    val = 10

Обход классов и функций

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

  • Класс проходиться сверху вниз, если находиться новый общий (shared) класс, то происходит строительство этого класса;

  • Если в поле общий (shared) класс такого типа уже был построен, то используется указатель на уже построенный класс.

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

  • В цепочку вызовов сначала попадают функции из тела текущего класса, и только после них функции из общих полей класса;

  • Декларация функции всегда будет стоять в конце цепочки вызовов.

Для вызова следующей в цепочке виртуальной функции, используется оператор nextFn. Важно понимать, что этот оператор по сути является виртуальным вызовом (virtual call), в отличие от статического вызова перегруженной функции в классическом ООП (inherited call).

Например, такой код:

class Base  fn SomeVirtFn [virtual]    Trace('Base')class ClsA  open Base [shared]  fne SomeVirtFn    Trace('ClsA')class ClsB  open Base [shared]  fne SomeVirtFn    Trace('ClsB')class ClsC  open ClsA [shared]  open ClsB [shared]  fne SomeVirtFn    Trace('ClsC')....  fn Main    c: ClsC    c.SomeVirtFn

выдаст:

  ClsC  ClsA  ClsB  Base

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru