Обновить
0

Пользователь

1
Подписчики
Отправить сообщение

Про оружие
Вы предложили использовать композицию без наследования (если добавляем наследование, то вернемся к проблеме HumanWithWeaponWithWings...).
Возможно пример с оружием не лучший. Вы говорите, что Humanoid содержит все нужные данные.
Humanoid может быть и с крыльями, и с костылями, и со шляпой, и на самокате и т.д. Как я понимаю, есть класс Humanoid у него есть поля weaponId, hairID, hatId и т.д. Сколько таких id должен иметь human? Так как Humanoid не базовый класс (отказ от наследования), то он должен учесть все возможные варианты. Аналогично для orcoid, goblinoid и т.д. (хотя нет, Вы сказали, что humanoid может быть orc, значит, добавим к нему еще и raceId. А лучше массив, если смесь рас допустима)
В такой ситуации захочется использовать что то похожее на ecs, про это статья сравнивающая разные подходы к созданию объекта игрового персонажа.


Про ООП
«этот код вообще не ООП ни разу»
Если финальный вариант кода рекомендованный в статье про фильтрацию транзакций не является ООП, то что это?


OOP про иерархии обьектов, не так ли?


DOD про данные без иерархии (в частности, композиция). Данные могут быть как последовательными в памяти, так и нет. DOD не только про cache miss.


Да, вместо Data driven progamming (data driven development) мне нужно было написать data-oriented design.


Рассмотрим код из комментария чтобы понять что я не считаю ООП иерархией https://m.habr.com/en/company/piter/blog/524882/comments/#comment_22271322


Данные отделены от логики, это раз.


И фильтры, и транзакции можно рассматривать как отдельные компоненты, это два. Компоненты итерируют, компоненты перебирают в visit. (Схоже с ecs)


Я вполне могу называть такой код не OOP, а DOD.


За неимением примеров кода, предполагаю, что вариант с Humanoid подразумевал схожую архитектуру т.е. DOD, а не ООП.


И мы опять начинаем сравнивать композицию с ecs (вместо сравнения кода из статьи с do_casewise и кода с ecs)

Если "вы вынуждены строить модели взаимодействия сущностей не имея четкого представления о том, что из себя будут эти сущности представлять"


  • разве ecs плохо работает в такой ситуации? Например, транзакцией можно считать все, что имеет компонент transaction. А какие компоненты дополнительно будут эти сущности предоставлять не важно.

«Вопрос о расширении набора реализаций сущностей, вероятно, даже и не стоит» — ecs лучше ООП если нужно добавить новые сущности или изменить сущ-ие, серия статей и про это. Аналогично и с «добавлять новые реализации уже определенных ранее интерфейсов не меняя ничего в уже существующем коде»

Если человек/дварф/эльф/всадник человек/всадник дварф и т.д. может носить тысячи видов вооружения (в разных руках), то сколько понадобится обьектов без runtime полиморфизма чтобы реализовать логику композицией? Примените шаблоны (не забываем о времени компиляции) или придете к схожему с ecs подходу?


Про ООП — я изначально сравнивал ecs с ООП из статьи (полиморфизм), а не с «ООП путем композиции», не с data driven development. Так и ecs можно назвать ООП, ведь это тоже data driven development. Например, комментарий https://m.habr.com/en/company/piter/blog/524882/comments/#comment_22271322 содержит замечательный код с композицией без runtime полиморфизма, но всем ли привычное ли это ООП или ближе к ecs, data driven development? (посмотрите на код с visitor или на код с do_casewise, неужели он по Вашему лучше? — изначально обсуждалось это)


Сравнивать ecs и data driven development не вижу смысла.

Пример "было вот так с использованием ООП, а затем стало вот так с использованием ECS"


https://leatherbee.org/index.php/2019/09/12/ecs-1-inheritance-vs-composition-and-ecs-background/


OOP — "derive Hammerdwarf and Axedwarf from Dwarf and Swordgoblin from Goblin."
ECS — "The bottom six components on both Axedwarf and Axegoblin represent the components that all humanoid NPCs share.


Now it’s easy to reuse all the common code between Axedwarf and Axegoblin. If I want to change an entity while the game is running, it’s a simple as removing and adding components. I can now remove the Axe component from my Axedwarf and give him a Spear component, and everything runs fine."


Конечно Axedwarf плохое ООП и можно иначе. Но ecs защитит от подобного плохого ООП, даже если Вы лично пишете хорошее ООП (что важно для командной работы).


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


Я однажды решил, что ecs достаточно полезный подход для того, чтобы поискать и изучить статьи, видео и книги на эту тему. Результат — сейчас я считаю ecs самым красивым решением многих проблем разработки ПО (из известных мне решений).


Постепенно простота ecs начинает нравится и уже не хочешь думать о «допустим ли тут pattern visitor» или «виртуальные методы с шаблонами, которые не поймет очередной джуниор в команде», а хочешь сразу использовать ecs.


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

https://habr.com/ru/post/441174/


В той статье пришли к struct RegularObject, там нет ни наследования, ни указателей. Также сравнение производительности идет с ужасной реализацией ecs, в которой каждый компонент уже имел виртуальный метод update (что даже смешно).


Я бы лично назвал получившийся в той статье код не ООП, а data driven development. Это что то между ECS и ООП.


Статья просто говорит, что композицией можно добиться результатов схожих с ecs (в одном конкретном примере при неизменных компонентах). Впрочем, если так сделать — не будет гибкости при разработке, в комментариях к статье говорят про минусы такого подхода по сравнению с более «правильной» реализацией ecs.


Почему "правильный" ecs может быть лучше ООП и data driven development из той статьи говорит статья от автора entt, ее русскоязычная версия https://m.habr.com/en/post/490500/


Замечу, что я не представляю как переписать проблему с транзакциями и фильтрацией в data driven development стиле, но легко могу реализовать с использованием готовой ecs библиотеки, вроде entt (мой код выше)

Что вы понимаете под «обо всех вариантах сущности»? Компонент обычно представлен в одном варианте, без наследования. Также напоминаю, что обсуждается влияние этого на время компиляции, буду рад примеру кода.


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


Про std::move — исходить из предположения, что программист опечатается всегда — не всегда верный подход.

«время компиляции» — Логика не обязана обрабатывать всегда все возможные виды поступаемых данных и включать все заголовочные файлы со всеми компонентами всюду. Более того, если не предоставить заголовочный файл с компонентами пользователям, то они и не смогут обработать некоторые виды поступаемых данных.


Обычно «ecs система» — небольшой cpp файл (обычно нет в hpp логики системы), который включает только нужные компоненты. В нашем случае отдельный тип фильтра можно считать отдельной системой. Так как система максимально изолирована, то ее можно вызвать методом вида «void runMySystem(entt::registry&)». На практике такая изоляция частей логики друг от друга должна ускорить компиляцию.


Возможно я не так понял проблему с временем компиляции, буду рад примерам кода для сравнения.


Про производительность — прирост от использования кеша вместо RAM примерно 25-100x (цифра из статьи «How to Write Faster Code Than 90% of Programmers» от Jackson Dunstan). Использование указателей и вирт. методов сводит использование кеша на нет. Также на сайте gameprogrammingpatterns (бесплатная онлайн книга «Game Programming Patterns») в статье «Data Locality» написано про это.

Да, при добавлении или изменении компонентов нужно быть внимательным. Теперь объект проще изменить через компоненты (композиция), но это порождает новые проблемы (которые, очевидно, можно решить по разному).


Замечу, что и в случае ООП возможны схожие проблемы, особенно если смотреть на пример с dynamic_cast или do_casewise (добавляется или меняется тип транзакции — код меняется).


Да, view с ошибочными компонентами даст пустой результат. Нужно писать тесты и т.д.

«пример кода имеет… другой оттенок. » — вполне логично, что я прошу более конструктивную критику на такие фразы. Также я сказал свое личное мнение, чтобы было понятно почему я не могу понять такую критику.


«у пары контор то» — это целая индустрия разработки ПО активно использующая ECS (и схожие с ним подходы). Я привел в пример только самые узнаваемые компании.


«в ECS логика должна знать всё множество возможных типов входных данных» — по-моему это ошибочное суждение. Я просто для примера все типы поместил в одну функцию с кучей проверок в if (и написал об этом комментарий в коде). Можно вынести проверки в отдельные функции и использовать только нужные типы компонентов или отдельные view с наборами нужных компонентов.


«так вы замеряли или нет» — вычитал из книг вроде Game engine architecture про оптимизацию цикла ААА игр и лекций GDC. На практике мне важнее оказалось удобство изменения архитектуры, чем производительность.

Да, больше проверок будет в run-time или нужно создать несколько view, тогда каждая view будет предоставлять entity с гарантированным набором компонент.


Если же говорить про возможную опечатку с двумя entt::entity — можно решить через strong alias (создать отдельные два типа оборачивающие entt::entity, можно к ним даже добавить проверки на допустимый набор компонент при желании).

« Но вот взглянешь на приведенный вами пример кода и складывается ощущение, что у "отлично" здесь какой-то другой оттенок. »


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

« фильтр всё еще должен знать о транзакциях. В то время как классический ООП предлагает полную инкапсуляцию. » — фильтр должен знать о транзакциях по условию задачи, например, чтобы обработать depositTxt отдельно. В ООП решении метод do_casewise знает о нужной транзакции.


«приживется или нет» — в игрострое уже прижилось


время компиляции затрудняюсь сравнивать с ООП. Считаете ли Вы, что у ECS почему-то хуже с этим?


«так то она и с виртуальными методами как правило достаточна» — я верно сказал, в игровом цикле слишком накладно постоянно вызывать виртуальный метод, поэтому используют ECS или другие подходы.


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


«ничто не мешает разработчику опечататься» — от опечаток не спасет и ООП. Конечно, можно создать отдельный объект как обертку над набором компонентов или сделать функцию с проверкой на допустимые компоненты объекта. Проблему не понимаю, то же возможно и с ООП.


"отлично подходит" не значит идеальное решение. У обоих подходов есть куча минусов.

Я про код из комментария с Visitor, там «Транзакция также теперь зачем-то знает о фильтрации» habr.com/en/company/piter/blog/524882/#comment_22227028 Например, объект «пирог» не должен знать о том, что его могут «выбирать» покупатели (мое личное мнение).

«старый, зарекомендовавший себя и хорошо изученный подход к проектированию ПО» — это не означает, что данный подход хорошо подходит для решения проблемы из статьи. ECS, к слову, также давно существует и зарекомендовал себя в индустрии разработки игр (Unity, Unreal Engine, Dava Engine от Wargaming).

Про производительность — зависит от решаемых задач. Как правило производительность достаточна для критического к производительности ПО, например, игровых серверов см. «Overwatch Gameplay Architecture and Netcode» — лекция с GDC. Но ECS зачастую выбирают из-за того, что он во многих случаях не позволит создать ужасную архитектуру (не будет многих проблем, которые на практике не редкость с ООП подходом).

Про ЦПУ или RAM — на сайте gameprogrammingpatterns (бесплатная онлайн книга «Game Programming Patterns») в статье «Data Locality» написано про проблему применимости «Moore’s Law» к RAM. ECS на практике применяют там, где под запретом наследование, виртуальные методы из-за требований производительности (игровые движки). Но вопрос производительности — отдельная и сложная тема.

Замечу также, что я не говорю про идеальность ECS (к сожалению, идеальное решение мне неизвестно). Я говорю, что ECS еще один способ решить проблему из статьи, может кому-то подойдет больше ООП.

Пример кода:

#include <entt/entt.hpp>
#include <entt/entity/registry.hpp>
#include <entt/entity/helper.hpp>
#include <entt/entity/entity.hpp>
#include <entt/core/type_info.hpp>
#include <entt/core/type_traits.hpp>
#include <entt/entity/group.hpp>

#include <boost/algorithm/string.hpp>

#include <cstddef>
#include <cstdint>
#include <sstream>
#include <string>
#include <type_traits>

// на практике проверят валидность данных иначе
const std::string kInvalidStr{"INVALID..."};

struct DebugName
{
  std::string debugName{kInvalidStr};
};

// ECS компонент - данные без логики
struct AccountNumber
{
  int accountId{-1};
};

// ECS компонент - данные без логики
struct AccountName
{
  std::string accountName{kInvalidStr};
};

// ECS компонент - данные без логики
struct MoneyAmount
{
  int moneyAmount{-1};
};

// Метка для любого перевода
using TransactionTag
  = entt::tag<"TransactionTag"_hs>;

// Метка для входящего перевода
using DepositTag
  = entt::tag<"DepositTag"_hs>;

// Метка для исходящего перевода
using TransferTag
  = entt::tag<"TransferTag"_hs>;

// Метка для любого фильтра
using FilterTag
  = entt::tag<"FilterTag"_hs>;

enum class CaseSensitivity
{
  CaseSensitive,
  IgnoreCase
};

enum class ComparisonType
{
  Greater,
  Less
};


// Метка для фильтра Money
struct MoneyAmountFilter
{
  ComparisonType comparisonType;
};

// Метка для фильтра соотв. AccountName
struct ExactAccountNameFilter
{
  CaseSensitivity caseSensitivity;
};

// входные транзакции для примера,
// на практике могут поступать в любой момент
void createTestTransactions(entt::registry& registry)
{
  {
    entt::entity entity
      = registry.create();
    registry.emplace<DebugName>(entity, "Transaction1");
    registry.emplace<TransactionTag>(entity);
    registry.emplace<DepositTag>(entity);
    registry.emplace<MoneyAmount>(entity, MoneyAmount{130});
    registry.emplace<AccountNumber>(entity, AccountNumber{1});
    registry.emplace<AccountName>(entity, AccountName{"Peter"});
  }

  {
    entt::entity entity
      = registry.create();
    registry.emplace<DebugName>(entity, "Transaction2");
    registry.emplace<TransactionTag>(entity);
    registry.emplace<TransferTag>(entity);
    registry.emplace<MoneyAmount>(entity, MoneyAmount{101});
    registry.emplace<AccountNumber>(entity, AccountNumber{2});
    registry.emplace<AccountName>(entity, AccountName{"habrahabr"});
  }
}

// включенные фильтры для примера,
// на практике могут включаться или отключаться в любой момент
void createTestFilters(entt::registry& registry)
{
  {
    entt::entity entity
      = registry.create();
    registry.emplace<DebugName>(entity, "Filter1");
    registry.emplace<FilterTag>(entity);
    MoneyAmountFilter filter{};
    filter.comparisonType = ComparisonType::Greater;
    registry.emplace<MoneyAmountFilter>(entity, std::move(filter));
  }

  {
    entt::entity entity
      = registry.create();
    registry.emplace<DebugName>(entity, "Filter2");
    registry.emplace<FilterTag>(entity);
    ExactAccountNameFilter filter{};
    filter.caseSensitivity = CaseSensitivity::IgnoreCase;
    registry.emplace<ExactAccountNameFilter>(entity, std::move(filter));
  }
}

// Проверка входящей транзакции определенным фильтром
bool filterTransaction(
  entt::registry& registry
  , entt::entity filterEntity
  , entt::entity transactionEntity)
{
  // проверки входных данных...
  {
    CHECK(registry.valid(filterEntity));
    CHECK(registry.has<FilterTag>(filterEntity));
    CHECK(registry.has<DebugName>(filterEntity));
    CHECK(registry.get<DebugName>(filterEntity).debugName != kInvalidStr);

    CHECK(registry.valid(transactionEntity));
    CHECK(registry.has<TransactionTag>(transactionEntity));
    CHECK(registry.has<DebugName>(transactionEntity));
    CHECK(registry.get<DebugName>(transactionEntity).debugName != kInvalidStr);
  }

  MoneyAmount* moneyAmount
    = registry.try_get<MoneyAmount>(transactionEntity);

  AccountNumber* accountNumber
    = registry.try_get<AccountNumber>(transactionEntity);

  AccountName* accountName
    = registry.try_get<AccountName>(transactionEntity);

  const bool isTransfer
    = registry.has<TransferTag>(transactionEntity);

  const bool isDeposit
    = registry.has<DepositTag>(transactionEntity);

  ExactAccountNameFilter* exactAccountNameFilter
    = registry.try_get<ExactAccountNameFilter>(filterEntity);

  MoneyAmountFilter* moneyAmountFilter
    = registry.try_get<MoneyAmountFilter>(filterEntity);

  // Снимать деньги со счета может только владелец этого счета.
  if(exactAccountNameFilter && accountName && isDeposit)
  {
    // тут могут быть доп. проверки входных данных...
    if(accountName->accountName == kInvalidStr) {
      return false;
    }

    const std::string kTestName = "habrahabr";

    if(exactAccountNameFilter->caseSensitivity == CaseSensitivity::IgnoreCase
       // compared case insensitively
       && !boost::iequals(accountName->accountName, kTestName))
    {
      return false;
    }

    if(exactAccountNameFilter->caseSensitivity == CaseSensitivity::CaseSensitive
       // compared case sensitively
       && !boost::equals(accountName->accountName, kTestName))
    {
      return false;
    }
  }

  if(moneyAmountFilter && moneyAmount)
  {
    // тут могут быть доп. проверки входных данных...

    const int kTestAmount = 42;

    if(moneyAmountFilter->comparisonType == ComparisonType::Less
       && moneyAmount->moneyAmount >= kTestAmount)
    {
      return false;
    }

    if(moneyAmountFilter->comparisonType == ComparisonType::Greater
       && moneyAmount->moneyAmount <= kTestAmount)
    {
      return false;
    }
  }

  // Еще проверки...
  // Конечно на практике кучу if в одной функции не пишут,
  // а регистрируют набор обработчиков (например, через плагины).

  return true;
}

// проверка входящих транзакций всеми включенными фильтрами
bool runFiltersOnTransaction(
  entt::registry& registry
  , entt::entity transactionEntity)
{
  auto view
    = registry.view<FilterTag>();

  // если хоть одна из включенных проверок не прошла,
  // то фильтрация вернет false
  for(const entt::entity& filterEntity : view)
  {
    const bool filterOk
      = filterTransaction(
          std::ref(registry)
          , filterEntity
          , transactionEntity);

    LOG(INFO)
      << "==================="
      << "\n"
      << "transactionEntity: "
      << registry.get<DebugName>(transactionEntity).debugName
      << "\n"
      << "filterEntity: "
      << registry.get<DebugName>(filterEntity).debugName
      << "\n"
      << "filterOk: "
      << std::to_string(filterOk)
      << "\n"
      << "===================";

    if(!filterOk) {
      return false;
    }
  }

  return true;
}

// обработка входящих транзакций
void processTransactions(entt::registry& registry)
{
  auto view
    = registry.view<TransactionTag>();

  for(const entt::entity& transactionEntity : view)
  {
    const bool filterOk
      = runFiltersOnTransaction(
          std::ref(registry)
          , transactionEntity);

    if(filterOk)
    {
      // далее логика сохранения транзакций в БД и т.д.
      // ...
    }
  }
}

int main(int argc, char* argv[])
{
  // предметная область — “банковские транзакции.”
  // необходимо фильтровать подозрительные транзакции

  // хранилище данных (ECS)
  entt::registry registry;

  createTestTransactions(std::ref(registry));

  createTestFilters(std::ref(registry));

  // на практике `processTransactions` можно вызывать периодически
  // по мере поступления новых данных
  processTransactions(registry);
  
  return
    EXIT_SUCCESS;
}

Примеры проблем с ООП:
Если в транзакцию добавляем метод filter чтоб решить проблему из статьи, то со временем DepositTxn, WithdrawalTxn и т.д. станет иметь много методов (по аналогии с filter добавится новая логика). Транзакция также теперь зачем-то знает о фильтрации. На практике это приводит к большим классам с кучей логики внутри.


Также одной из проблем наследования является сложность изменения под новые требования. Про эту проблему ООП можно найти во многих вводных статьях про ECS.


Пример решения на ECS библиотеке entt:
Определяем для каждого типа транзакции свой набор компонентов. Т.е. сущность представляем путем композиции компонентов (компонент — только данные без логики обработки) или тегов (entt::tag). Все делается без наследования, следовательно не будет возникать проблем решаемых в статье.
Фильтрация транзакции — обработка определенного набора компонентов.


В документации entt описаны примеры обработки (в нашем случае обрабатывается событие ыильтрации транзакций), нам интересны примеры с использованием entt::view или entt::group. Замечу, что можно обработку делать не только периодически в системе (как во многих примерах использования ECS), но и вызвать ее в нужный момент единожды.


Про производительность.
Если использовать библиотеку entt, то в ее документации можно прочитать о оптимизациях производительности через cache locality (данные упакованы последовательно, что ускоряет итерацию). На практике производительность зачастую ограничена не ЦПУ, а скоростью доступа к RAM (скорость доступа к памяти отстает от прогресса ЦПУ). Поэтому где производительность лучше — отдельный вопрос, зависит от входных данных.


Минусы ECS:
Зависимости между сущностями скорее всего придется описывать связным списком. Блог автора entt содержит пример решения компонентом relationship.


Минусы entt:
Нужно хорошо изучить документацию для избежания ошибок многопоточности и UB. Т.е. программистам нужно выделить время на изучение нового подхода к разработке. При этом ООП обычно все понимают интуитивно.


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

Уход от ООП позволяет избавиться от проблем с наследованием, которые решают в этой статье. На хабр есть статьи про ECS, пример: https://habr.com/en/post/441174/

Для решения этой проблемы отлично подходит Entity–component–system (ECS) pattern.

Я не имею никакого отношения к yandex. Но теоретически за такой заголовок они могут подать в суд (что маловероятно, но я бы поменял заголовок если еще можно)

Я не опровергаю миф 4. Я говорю, что все зависит от use case. Говорить, что что-то быстрее без конкретики — странно.

Рассмотрим, например, пункт опровержения «Миф №4. Rust медленнее C++.». Антон Полухин приводит пример без unique_ptr и говорит, что код на C++ быстрее (что логично, если не нужно управлять временем жизни объектов и т.д. — то C++ быстрее. Фактически возможности Rust для такого use case не нужны). Если участок кода критичен к производительности, то C++ позволяет написать более быстрый код без контроля за жизнью объектов и т.д., а в Rust придется оборачивать unsafe и/или подключать C или C++ код. В опровержении вдруг уходят от темы и начинают сравнивать код с unique_ptr. Они в чем то правы — сравнивают совершенно разные инструменты, которые решают задачи совершенно по разному, но почему-то use case не был понят и выводы были сделаны неправильные (что Полухин не прав, хотя он рассматривает совершенно другой use case). Делают неверный вывод, что «Миф №4. Rust медленнее C++.» — это миф. В изначальном примере «подтасовали» ситуации из совершенно разных примеров и обвиняют Полухина в ошибках сравнения. Также нахожу странным, что в пункте «Миф №4. Rust медленнее C++.» пытаются сказать, что «больше асма → медленнее язык» — не применимо к Rust в рассмотренных примерах (не привели примеры, опровержения, просто сделали предположение и пытаются на нем делать выводы).
2

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность