Читая литературу о том, как создаются игры, мы обнаруживаем, что общим шаблоном для многих игр является шаблон Сущность-Компонент-Система (ECS), впервые использованный в одной из наших любимых игр Thief: The Dark Project. Tau Station использует ECS для предметов, которые могут найти персонажи, и она оказалась очень гибкой, и, поскольку мы не являемся традиционной графической игрой, некоторые из известных недостатков ECS к нам не относятся. Однако мы также используем традиционное объектно-ориентированное программирование (ООП), и именно здесь мы хотим избежать распространенной ловушки, в которую попадают многие разработчики программного обеспечения: множественного наследования.

Чтобы понять проблему, стоит рассмотреть проблему, которую пытается решить наследование. «Современное» объектно-ориентированное программирование фактически было создано 50 лет назад с выходом Simula-67. У него были классы, полиморфизм, инкапсуляция и наследование. Для основанного на классах ООП достоинства классов, полиморфизма и инкапсуляции обычно согласованы. Однако споры о правильном использовании наследования ведутся уже пять десятилетий. Теоретически наследование используется для создания более специализированной версии «вещи». Например, если у вас есть класс с именем Mammal, у вас может быть дочерний класс с именем Cat, который «наследует» все поведение от родительского класса. Это означает, что вы пишете меньше кода, что, как правило, хорошо. Обратите внимание, что эти классы обычно не предназначены для моделирования реального мира; они моделируют потребности бизнеса в программном обеспечении.

Но для чего используется множественное наследование? Что ж, утконос — это млекопитающее, но, помимо многих особенностей, он откладывает яйца. Как бы вы справились с этим? Что ж, вы могли скопировать код откладывания яиц из класса Bird, но вы хороший программист и знаете, что копирование и вставка — это плохо. Или вы можете использовать множественное наследование и наследовать класс Platypus как от Mammal, так и от Bird в надежде получить различные варианты поведения, которые вы хотите, но это рискует получить дополнительное нежелательное поведение от класса Bird. Вы можете извлечь код кладки яиц в класс EggLaying и наследовать от него Bird и Platypus, но это любопытно, потому что классы обычно являются существительными, а «яйцекладка» — это прилагательное, описывающее поведение. Вы знаете, как выглядит птица или утконос, но как выглядит «яйцекладка»? Это не так.

Существует множество способов решения проблемы (включая композицию и делегирование), но в Tau Station мы используем черты (известные как роли в семействе языков Perl). Черта — это, по сути, набор поведений, которые может использовать класс. Это не полноценный класс и не может использоваться как таковой. В нашем примере выше у нас может быть черта EggLaying, и классы Bird и Platypus могут потреблять эту черту и знать, как откладывать яйца!

К этому моменту некоторые из вас могут задаться вопросом, чем трейты отличаются от mixins, инструмента программирования, созданного на Lisp, но популяризированного Ruby. Mixin — это также набор поведений, которые может использовать класс. Хотя они лучше, чем множественное наследование, у них есть общая проблема с множественным наследованием: порядок, в котором вы используете примеси, имеет значение.

Представьте, например, что вы пишете игру и хотите пошутить в игре. Персонаж входит в комнату, и розыгрыш должен взорваться (без смертельного исхода) после того, как сгорит предохранитель (в течение точного времени). У вас есть класс Boss, который имеет explode() (несмертельный) и fuse() метод (случайный). У вас также есть класс Bomb, который имеет метод explode() (смертельный) и метод fuse(), который сжигает точное количество времени. Итак, вам нужен метод explode() для Boss и метод fuse() для Bomb. Но если вы наследуете от них, то вы получите оба метода из первого класса, от которого вы наследуетесь. Но как насчет миксинов Ruby?

module Bomb
  def explode
    puts "Bomb explode"
  end

  def fuse
    puts "Bomb fuse"
  end
end

module Boss
  def explode
    puts "Boss explode"
  end

  def fuse
    puts "Boss fuse"
  end
end

class PracticalJoke
  include Boss
  include Bomb
end

joke = PracticalJoke.new()
joke.explode
joke.fuse

Это распечатывает:

Bomb explode
Bomb fuse

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

Однако эта «проблема упорядочения» не существует для трейтов. В приведенном выше примере, если бы мы переписали это на Perl, используя трейты, код даже не скомпилировался бы, потому что он не знает, какие методы вам нужны. Вместо этого вы можете написать это так:

package PracticalJoke;
use Moose;
with 'Bomb' => { excludes => 'explode' },
     'Boss' => { excludes => 'fuse'    };

С вышесказанным мы просто исключаем любое поведение, которое нам не нужно, и порядок, в котором потребляются черты, не важен.

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

Трейты — наша основная единица повторного использования кода, а не наследования. Учитывая наш предыдущий опыт упрощения сложных систем с помощью ролей, это был вполне естественный выбор для Tau Station, и с нашим кодом стало намного проще и безопаснее работать. Если вы программист, узнайте, доступны ли трейты для вашего языка. Вполне возможно, что они являются самым большим достижением в объектно-ориентированном программировании за пятьдесят лет.

Если вы хотите узнать больше о чертах, статья Черты: составные единицы поведения — хорошее начало. Мы также настоятельно рекомендуем Черты характера: формальная модель.