Лекция 4 из серии DDD: Спецификации проектирования доменного уровня

Архитектура
Лекция 4 из серии DDD: Спецификации проектирования доменного уровня

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

Первый взгляд на мировую архитектуру драконов и магии

Предыстория и правила

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

Базовая конфигурация выглядит следующим образом:

  • Игроки могут быть бойцами, магами, драгунами.

  • Монстрами могут быть орки, эльфы, драконы, а у монстров есть HP.

  • Оружие может быть мечом, посохом и оружием с силой атаки.

  • Игрок может экипировать оружие, атака оружия может быть физического типа (0), огня (1), льда (2) и т. д. Тип оружия определяет тип урона.

Правила атаки следующие:

  • Урон орков от физических атак уменьшен вдвое

  • Урон спрайтов от магических атак уменьшен вдвое.

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

Реализация ООП

Для студентов, знакомых с объектно-ориентированным программированием, относительно простой реализацией является отношение наследования классов (некоторые неосновные коды здесь опущены):

public abstract class Player {
      Weapon weapon
}
public class Fighter extends Player {}
public class Mage extends Player {}
public class Dragoon extends Player {}

public abstract class Monster {
    Long health;
}
public Orc extends Monster {}
public Elf extends Monster {}
public Dragoon extends Monster {}

public abstract class Weapon {
    int damage;
    int damageType; // 0 - physical, 1 - fire, 2 - ice etc.
}
public Sword extends Weapon {}
public Staff extends Weapon {}

Код для реализации правила выглядит следующим образом:

public class Player {
    public void attack(Monster monster) {
        monster.receiveDamageBy(weapon, this);
    }
}

public class Monster {
    public void receiveDamageBy(Weapon weapon, Player player) {
        this.health -= weapon.getDamage(); // 基础规则
    }
}

public class Orc extends Monster {
    @Override
    public void receiveDamageBy(Weapon weapon, Player player) {
        if (weapon.getDamageType() == 0) {
            this.setHealth(this.getHealth() - weapon.getDamage() / 2); // Orc的物理防御规则
        } else {
            super.receiveDamageBy(weapon, player);
        }
    }
}

public class Dragon extends Monster {
    @Override
    public void receiveDamageBy(Weapon weapon, Player player) {
        if (player instanceof Dragoon) {
            this.setHealth(this.getHealth() - weapon.getDamage() * 2); // 龙骑伤害规则
        }
        // else no damage, 龙免疫力规则
    }
}

Затем запустите несколько одиночных тестов:

public class BattleTest {

    @Test
    @DisplayName("Dragon is immune to attacks")
    public void testDragonImmunity() {
        // Given
        Fighter fighter = new Fighter("Hero");
        Sword sword = new Sword("Excalibur", 10);
        fighter.setWeapon(sword);
        Dragon dragon = new Dragon("Dragon", 100L);

        // When
        fighter.attack(dragon);

        // Then
        assertThat(dragon.getHealth()).isEqualTo(100);
    }

    @Test
    @DisplayName("Dragoon attack dragon doubles damage")
    public void testDragoonSpecial() {
        // Given
        Dragoon dragoon = new Dragoon("Dragoon");
        Sword sword = new Sword("Excalibur", 10);
        dragoon.setWeapon(sword);
        Dragon dragon = new Dragon("Dragon", 100L);

        // When
        dragoon.attack(dragon);

        // Then
        assertThat(dragon.getHealth()).isEqualTo(100 - 10 * 2);
    }

    @Test
    @DisplayName("Orc should receive half damage from physical weapons")
    public void testFighterOrc() {
        // Given
        Fighter fighter = new Fighter("Hero");
        Sword sword = new Sword("Excalibur", 10);
        fighter.setWeapon(sword);
        Orc orc = new Orc("Orc", 100L);

        // When
        fighter.attack(orc);

        // Then
        assertThat(orc.getHealth()).isEqualTo(100 - 10 / 2);
    }

    @Test
    @DisplayName("Orc receive full damage from magic attacks")
    public void testMageOrc() {
        // Given
        Mage mage = new Mage("Mage");
        Staff staff = new Staff("Fire Staff", 10);
        mage.setWeapon(staff);
        Orc orc = new Orc("Orc", 100L);

        // When
        mage.attack(orc);

        // Then
        assertThat(orc.getHealth()).isEqualTo(100 - 10);
    }
}

Вышеприведенный код и одиночный тест относительно просты и не требуют лишних объяснений.

Анализировать недостатки дизайна в ООП-коде

Строгая типизация языков программирования не может нести бизнес-правила

Приведенный выше код ООП работает нормально, пока мы не добавим ограничение:

  • Воины могут экипировать только мечи

  • Маг может экипировать только посох

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

@Data
public class Fighter extends Player {
    private Sword weapon;
}

@Test
public void testEquip() {
    Fighter fighter = new Fighter("Hero");

    Sword sword = new Sword("Sword", 10);
    fighter.setWeapon(sword);

    Staff staff = new Staff("Staff", 10);
    fighter.setWeapon(staff);

    assertThat(fighter.getWeapon()).isInstanceOf(Staff.class); // 错误了
}

В конце концов, хотя код выглядит как setWeapon(Staff), на самом деле он изменяет только переменные родительского класса и не изменяет переменные подкласса, поэтому он не вступает в силу и не генерирует исключение, а результат неправильно.

Конечно, можно ограничить сеттер защищенным в родительском классе, но это ограничивает API родительского класса, сильно снижает гибкость, а также нарушает принцип подстановки Лискова, то есть родительский класс должен быть приведен к дочерний класс, прежде чем его можно будет использовать:

@Data
public abstract class Player {
    @Setter(AccessLevel.PROTECTED)
    private Weapon weapon;
}

@Test
public void testCastEquip() {
    Fighter fighter = new Fighter("Hero");

    Sword sword = new Sword("Sword", 10);
    fighter.setWeapon(sword);

    Player player = fighter;
    Staff staff = new Staff("Staff", 10);
    player.setWeapon(staff); // 编译不过,但从API层面上应该开放可用
}

Наконец, если правило добавляет один:

  • И воины, и волшебники могут быть экипированы кинжалами.

BOOM, строго типизированный код, написанный ранее, полностью выброшен и нуждается в рефакторинге.

Наследование объектов приводит к тому, что код сильно зависит от логики родительского класса, что нарушает принцип открытого-закрытого (OCP)

Принцип открытого-закрытого (OCP) гласит, что «объекты должны быть открыты для расширения и закрыты для модификации». Хотя наследование может распространять новое поведение через подклассы, поскольку подклассы могут напрямую зависеть от реализации суперкласса, изменение может затронуть все объекты. . В этом примере добавление любого типа игрока, монстра или оружия или добавление правила может потребовать изменения всех методов от родительского к дочернему.

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

  • Weapon

  • Игрок и все подклассы (решение о том, можно ли экипировать оружие)

  • Монстр и все подклассы (логика расчета урона)

public class Monster {
    public void receiveDamageBy(Weapon weapon, Player player) {
        this.health -= weapon.getDamage(); // 老的基础规则
        if (Weapon instanceof Gun) { // 新的逻辑
            this.setHealth(0);
        }
    }
}

public class Dragon extends Monster {
    public void receiveDamageBy(Weapon weapon, Player player) {
        if (Weapon instanceof Gun) { // 新的逻辑
                      super.receiveDamageBy(weapon, player);
        }
        // 老的逻辑省略
    }
}

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

Хотя наследование может быть открытым для расширения, трудно быть закрытым для модификации. Так что основной способ решения OCP сегодня — через Composition-over-inheritance, то есть через композицию для достижения расширяемости, а не через наследование.

Player.attack(монстр) или Monster.receiveDamage(Оружие, Игрок)?

В этом примере на самом деле есть возражение по поводу того, где должна быть написана логика бизнес-правил: когда мы смотрим на взаимодействие между одним объектом и другим объектом, Игрок атакует Монстра, или Монстр атакуется Игроком? Текущий код в основном прописывает логику в классе Монстр.Основное соображение в том, что Монстр будет ранен и уменьшит Здоровье, но что, если Игрок держит палку о двух концах и при этом поранит себя? Вы обнаружили, что в классе Monster тоже есть проблема с письмом? Каковы принципы написания кода?

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

Когда у нас есть разные объекты, но одинаковое или похожее поведение, ООП неизбежно приводит к дублированию кода. В этом примере, если бы мы хотели добавить «подвижное» поведение, нам нужно было бы добавить аналогичную логику как к классам Player, так и к классам Monster:

public abstract class Player {
    int x;
    int y;
    void move(int targetX, int targetY) {
        // logic
    }
}

public abstract class Monster {
    int x;
    int y;
    void move(int targetX, int targetY) {
        // logic
    }
}

Возможное решение состоит в том, чтобы иметь общий суперкласс:

public abstract class Movable {
    int x;
    int y;
    void move(int targetX, int targetY) {
        // logic
    }
}

public abstract class Player extends Movable;
public abstract class Monster extends Movable;

Но что, если вы добавите еще одну способность к прыжкам, Jumpable? А как насчет беговой способности Runnable? Если Игрок может Двигаться и Прыгать, а Монстр может Двигаться и Бегать, как быть с отношением наследования? Имейте в виду, что Java (и большинство языков) не поддерживает множественное наследование родительских классов, поэтому этого можно добиться только путем повторения кода.

краткое изложение проблемы

В этом случае, хотя логика ООП интуитивно проста, но если ваш бизнес сложный и в будущем будет большое количество изменений в бизнес-правилах, простой код ООП на более позднем этапе превратится в сложную кашу. в разных местах, без глобальной перспективы, наложение различных правил вызовет ошибки. Это кажется знакомым? Да, скидки, транзакции и другие ссылки в системе электронной коммерции часто сталкиваются с подобными ямами. Суть таких проблем заключается в следующем:

  • Является ли присвоение бизнес-правил «поведением» объекта или независимым «объектом правила»?

  • Как быть с отношениями между бизнес-правилами?

  • Как следует повторно использовать и поддерживать общее «поведение»?

Прежде чем говорить о решении DDD, давайте взглянем на набор популярных в последнее время архитектурных решений в игре, как реализована Entity-Component-System (ECS).

Введение в архитектуру Entity-Component-System (ECS)

Введение в ЭКС

Архитектурная модель ECS на самом деле является очень старым дизайном игровой архитектуры.Его следует проследить до компонентного дизайна «Dungeon Siege», но она стала популярной в последнее время с добавлением Unity (например, «Overwatch» использует ECS). Чтобы быстро понять ценность архитектуры ECS, нам нужно понять основную проблему игрового кода:

  • Производительность: игра должна достигать высокой скорости рендеринга (60 кадров в секунду), что означает, что весь игровой мир должен быть полностью обновлен в течение 1/60 с (около 16 мс) (включая физический движок, состояние игры, рендеринг, ИИ и т. д.). В игре обычно имеется большое количество (10 000, 100 000) игровых объектов, которым необходимо обновить свое состояние.За исключением рендеринга, который может зависеть от графического процессора, другая логика должна выполняться процессором, и даже большая часть они могут быть выполнены только одним потоком.Завершенный ЦП (в основном пропускная способность от памяти к ЦП) станет узким местом в сложных сценариях большую часть времени. В эпоху, когда скорость одного ядра ЦП почти не увеличивается, повышение эффективности обработки ЦП является основой повышения производительности игры.

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

  • Масштабируемость: похожа на предыдущую, но больше обусловлена ​​характеристиками игры: ее нужно быстро обновлять, чтобы добавлять новые элементы. Архитектура игры должна иметь возможность добавлять игровые элементы с помощью low-code или даже zero-code, чтобы удерживать пользователей за счет быстрых обновлений. Если каждое изменение требует разработки нового кода, тестирования, а затем повторной загрузки пользователем клиента, можно предположить, что такого рода игры трудно выжить в текущей конкурентной среде.

Архитектура ECS вполне может решить вышеуказанные проблемы.Архитектура ECS в основном делится на:

  • Entity: используется для представления любого игрового объекта, но самое важное в Entity в ECS — это только его EntityID, а Entity содержит несколько компонентов.

  • Компонент: Это реальные данные.Архитектура ECS делит каждый объект объекта на более подробные компоненты, такие как местоположение, материал, статус и т. д., что означает, что объект на самом деле является просто набором компонентов.

  • Система (или ComponentSystem, система компонентов): это реальное поведение, в игре может быть много разных систем компонентов, каждая система компонентов отвечает только за одну вещь и может обрабатывать большое количество одних и тех же компонентов по очереди без необходимости понять конкретную сущность. Следовательно, ComponentSystem теоретически может иметь более эффективную эффективность обработки компонентов и даже может обеспечить параллельную обработку, тем самым улучшая использование ЦП.

Некоторые основные оптимизации производительности ECS включают размещение компонентов одного типа в одном и том же массиве, а затем Entity резервируется только для указателя каждого компонента, что может лучше использовать кэш ЦП, снизить затраты на загрузку данных и оптимизировать SIMD.

Псевдокод для случая ECS выглядит следующим образом:

public class Entity {
  public Vector position; // 此处Vector是一个Component, 指向的是MovementSystem.list里的一个
}

public class MovementSystem {
  List<Vector> list;

  // System的行为
  public void update(float delta) {
    for(Vector pos : list) { // 这个loop直接走了CPU缓存,性能很高,同时可以用SIMD优化
      pos.x = pos.x + delta;
      pos.y = pos.y + delta;
    }
  }
}

@Test
public void test() {
  MovementSystem system = new MovementSystem();
  system.list = new List<>() { new Vector(0, 0) };
  Entity entity = new Entity(list.get(0));
  system.update(0.1);
  assertTrue(entity.position.x == 0.1);
}

Поскольку в этой статье не объясняется архитектура ECS, заинтересованные студенты могут выполнить поиск Entity-Component-System или посмотреть документацию Unity по ECS.

Анализ архитектуры ECS

Вернемся к анализу ECS, на самом деле его происхождение все еще несколько очень старых концепций:

составной

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

поведенческая абстракция

Это имеет относительно очевидное преимущество в игровой системе. Согласно методу ООП, игровой объект может включать в себя код движения, боевой код, код рендеринга, код ИИ и т. д. Если их все поместить в класс, его будет очень долго и сложно поддерживать. Выделив общую логику в отдельный класс System, можно значительно улучшить читабельность кода. Другим преимуществом является то, что извлекаются некоторые зависимости, не связанные с кодом объекта, такие как приведенная выше дельта. система. В первой главе есть вопрос, должен Player.attack(monster) или Monster.receiveDamage(Weapon, Player). В ECS эта задача становится очень простой, и ее можно поместить в CombatSystem.

управляемый данными

То есть поведение объекта не жестко запрограммировано, а определяется его параметрами, благодаря динамической модификации параметров конкретное поведение объекта можно быстро изменить. В игровой архитектуре ECS путем регистрации соответствующего Компонента для Сущности и изменения комбинации конкретных параметров Компонента можно изменить поведение и игровой процесс объекта. Например, создание свойства чайник + взрыв становится " взрывающийся чайник», добавьте к велосипеду магию ветра, и он станет летающим автомобилем и т. д. В некоторых играх Rougelike может быть более 10 000 элементов разных типов и разных функций.Если эти элементы с разными функциями написаны отдельно, они могут никогда не быть написаны.Однако благодаря архитектуре, управляемой данными + компонентной, все элементы конфигурации в конечном итоге таблица, и модификация чрезвычайно проста. Это также проявление принципа композиции над наследованием.

Недостатки ЭКС

В то время как ECS начинает оставлять свой след в игровом мире, я вижу, что архитектура ECS еще не использовалась ни в одном крупном коммерческом приложении. Причин может быть много, в том числе тот факт, что ECS является относительно новым, люди еще не понимают его, не хватает коммерчески зрелых и пригодных для использования фреймворков, а программисты не в состоянии адаптироваться к изменению мышления от написания логических скриптов. к написанию компонентов, но я думаю, что одна из самых больших проблем заключается в том, что ECS для повышения производительности делает упор на разделение данных/состояния (State) и поведения (Behaivor), а для снижения затрат на сборку мусора ушли непосредственно рабочие данные до крайности. В коммерческих приложениях правильность, непротиворечивость и надежность данных должны быть наивысшим приоритетом, а производительность — это лишь вишенка на торте, поэтому ECS сложно принести особые преимущества в коммерческих сценариях. Но это не означает, что мы не можем извлечь уроки из некоторых передовых идей ECS, включая компонентизацию, разделение межобъектного поведения и модель, управляемую данными, которые также можно хорошо использовать в DDD.

Решение, основанное на архитектуре DDD

доменный объект

Возвращаясь к нашей исходной предметной области, давайте разделим различные объекты из доменного слоя:

класс сущности

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

public class Player implements Movable {
    private PlayerId id;
    private String name;
    private PlayerClass playerClass; // enum
    private WeaponId weaponId; // (Note 1)
    private Transform position = Transform.ORIGIN;
    private Vector velocity = Vector.ZERO;
}

public class Monster implements Movable {
    private MonsterId id;
    private MonsterClass monsterClass; // enum
    private Health health;
    private Transform position = Transform.ORIGIN;
    private Vector velocity = Vector.ZERO;
}

public class Weapon {
    private WeaponId id;
    private String name;
    private WeaponType weaponType; // enum
    private int damage;
    private int damageType; // 0 - physical, 1 - fire, 2 - ice
}

В этом простом случае мы можем использовать PlayerClass и MonsterClass перечисления, чтобы заменить отношения наследования, и мы также можем использовать шаблон проектирования Type Object для управления данными в будущем.

Примечание 1. Поскольку Weapon — это класс сущностей, но Weapon может существовать независимо, а Player не является совокупным корнем, поэтому Player может хранить только WeaponId, но не может напрямую указывать на Weapon.

Компонентизация объектов ценности

В предыдущей архитектуре ECS есть концепция MovementSystem, которую можно использовать повторно.Хотя вы не должны напрямую оперировать Component или наследовать общие родительские классы, вы можете разбивать объекты предметной области на компоненты через интерфейсы:

public interface Movable {
    // 相当于组件
    Transform getPosition();
    Vector getVelocity();

    // 行为
    void moveTo(long x, long y);
    void startMove(long velX, long velY);
    void stopMove();
    boolean isMoving();
}

// 具体实现
public class Player implements Movable {
    public void moveTo(long x, long y) {
        this.position = new Transform(x, y);
    }

    public void startMove(long velocityX, long velocityY) {
        this.velocity = new Vector(velocityX, velocityY);
    }

    public void stopMove() {
        this.velocity = Vector.ZERO;
    }

    @Override
    public boolean isMoving() {
        return this.velocity.getX() != 0 || this.velocity.getY() != 0;
    }
}

@Value
public class Transform {
    public static final Transform ORIGIN = new Transform(0, 0);
    long x;
    long y;
}

@Value
public class Vector {
    public static final Vector ZERO = new Vector(0, 0);
    long x;
    long y;
}

Обратите внимание на два момента:

  • Интерфейс Moveable не имеет Setter. Правило Entity заключается в том, что его свойства нельзя изменять напрямую, а внутреннее состояние необходимо менять через методы Entity. Это обеспечивает согласованность данных.

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

поведение оборудования

Поскольку мы не используем подклассы Player, чтобы решить, какое оружие можно экипировать, эту логику следует выделить в отдельный класс. Этот класс называется Domain Service в DDD.

public interface EquipmentService {
    boolean canEquip(Player player, Weapon weapon);
}

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

public class Player {
    @Autowired
    EquipmentService equipmentService; // BAD: 不可以直接依赖

    public void equip(Weapon weapon) {
       // ...
    }
}

Проблема здесь в том, что Entity может сохранять только свое собственное состояние (или объекты, которые не являются совокупными корнями). Любой другой объект, независимо от того, введен он посредством внедрения зависимостей или нет, разрушит Инвариантность Сущности, и его трудно измерить индивидуально.

Правильный способ ссылки - введение параметра метода (Double Dispatch):

public class Player {

    public void equip(Weapon weapon, EquipmentService equipmentService) {
        if (equipmentService.canEquip(this, weapon)) {
            this.weaponId = weapon.getId();
        } else {
            throw new IllegalArgumentException("Cannot Equip: " + weapon);
        }
    }
}

Здесь и Weapon, и EquipmentService передаются через параметры метода, чтобы гарантировать, что собственное состояние игрока не будет загрязнено.

Двойная отправка — это метод, который часто используется при использовании доменной службы, аналогичный реверсированию вызовов.

Затем реализуйте соответствующее логическое суждение в EquipmentService.Здесь мы используем другой часто используемый шаблон проектирования Strategy (или Policy):

public class EquipmentServiceImpl implements EquipmentService {
    private EquipmentManager equipmentManager; 

    @Override
    public boolean canEquip(Player player, Weapon weapon) {
        return equipmentManager.canEquip(player, weapon);
    }
}

// 策略优先级管理
public class EquipmentManager {
    private static final List<EquipmentPolicy> POLICIES = new ArrayList<>();
    static {
        POLICIES.add(new FighterEquipmentPolicy());
        POLICIES.add(new MageEquipmentPolicy());
        POLICIES.add(new DragoonEquipmentPolicy());
        POLICIES.add(new DefaultEquipmentPolicy());
    }

    public boolean canEquip(Player player, Weapon weapon) {
        for (EquipmentPolicy policy : POLICIES) {
            if (!policy.canApply(player, weapon)) {
                continue;
            }
            return policy.canEquip(player, weapon);
        }
        return false;
    }
}

// 策略案例
public class FighterEquipmentPolicy implements EquipmentPolicy {

    @Override
    public boolean canApply(Player player, Weapon weapon) {
        return player.getPlayerClass() == PlayerClass.Fighter;
    }

    /**
     * Fighter能装备Sword和Dagger
     */
    @Override
    public boolean canEquip(Player player, Weapon weapon) {
        return weapon.getWeaponType() == WeaponType.Sword
                || weapon.getWeaponType() == WeaponType.Dagger;
    }
}

// 其他策略省略,见源码

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

агрессивное поведение

Как упоминалось выше, должен ли это быть Player.attack(Monster) или Monster.receiveDamage(Weapon, Player)? В DDD, поскольку это поведение может повлиять на игрока, монстра и оружие, оно относится к бизнес-логике между сущностями. В этом случае это нужно делать через стороннюю доменную службу (Domain Service).

public interface CombatService {
    void performAttack(Player player, Monster monster);
}

public class CombatServiceImpl implements CombatService {
    private WeaponRepository weaponRepository;
    private DamageManager damageManager;

    @Override
    public void performAttack(Player player, Monster monster) {
        Weapon weapon = weaponRepository.find(player.getWeaponId());
        int damage = damageManager.calculateDamage(player, weapon, monster);
        if (damage > 0) {
            monster.takeDamage(damage); // (Note 1)在领域服务里变更Monster
        }
        // 省略掉Player和Weapon可能受到的影响
    }
}

Также и в этом случае задача расчета ущерба может быть решена с помощью шаблона проектирования Стратегия:

// 策略优先级管理
public class DamageManager {
    private static final List<DamagePolicy> POLICIES = new ArrayList<>();
    static {
        POLICIES.add(new DragoonPolicy());
        POLICIES.add(new DragonImmunityPolicy());
        POLICIES.add(new OrcResistancePolicy());
        POLICIES.add(new ElfResistancePolicy());
        POLICIES.add(new PhysicalDamagePolicy());
        POLICIES.add(new DefaultDamagePolicy());
    }

    public int calculateDamage(Player player, Weapon weapon, Monster monster) {
        for (DamagePolicy policy : POLICIES) {
            if (!policy.canApply(player, weapon, monster)) {
                continue;
            }
            return policy.calculateDamage(player, weapon, monster);
        }
        return 0;
    }
}

// 策略案例
public class DragoonPolicy implements DamagePolicy {
    public int calculateDamage(Player player, Weapon weapon, Monster monster) {
        return weapon.getDamage() * 2;
    }
    @Override
    public boolean canApply(Player player, Weapon weapon, Monster monster) {
        return player.getPlayerClass() == PlayerClass.Dragoon &&
                monster.getMonsterClass() == MonsterClass.Dragon;
    }
}

Следует отметить, что доменная служба CombatService здесь и доменная служба EquipmentService из 3.2 являются доменными службами, но они существенно отличаются. Вышеупомянутая служба EquipmentService больше похожа на стратегию только для чтения и влияет только на один объект, поэтому ее можно внедрить через параметры метода Player.equip. Однако CombatService может воздействовать на несколько объектов, поэтому его нельзя вызывать напрямую посредством внедрения параметров.

модульный тест

@Test
@DisplayName("Dragoon attack dragon doubles damage")
public void testDragoonSpecial() {
    // Given
    Player dragoon = playerFactory.createPlayer(PlayerClass.Dragoon, "Dart");
    Weapon sword = weaponFactory.createWeaponFromPrototype(swordProto, "Soul Eater", 60);
    ((WeaponRepositoryMock)weaponRepository).cache(sword);
    dragoon.equip(sword, equipmentService);
    Monster dragon = monsterFactory.createMonster(MonsterClass.Dragon, 100);

    // When
    combatService.performAttack(dragoon, dragon);

    // Then
    assertThat(dragon.getHealth()).isEqualTo(Health.ZERO);
    assertThat(dragon.isAlive()).isFalse();
}

@Test
@DisplayName("Orc should receive half damage from physical weapons")
public void testFighterOrc() {
    // Given
    Player fighter = playerFactory.createPlayer(PlayerClass.Fighter, "MyFighter");
    Weapon sword = weaponFactory.createWeaponFromPrototype(swordProto, "My Sword");
    ((WeaponRepositoryMock)weaponRepository).cache(sword);
    fighter.equip(sword, equipmentService);
    Monster orc = monsterFactory.createMonster(MonsterClass.Orc, 100);

    // When
    combatService.performAttack(fighter, orc);

    // Then
    assertThat(orc.getHealth()).isEqualTo(Health.of(100 - 10 / 2));
}

Конкретный код относительно прост, и объяснение опущено.

мобильная система

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

public class MovementSystem {

    private static final long X_FENCE_MIN = -100;
    private static final long X_FENCE_MAX = 100;
    private static final long Y_FENCE_MIN = -100;
    private static final long Y_FENCE_MAX = 100;

    private List<Movable> entities = new ArrayList<>();

    public void register(Movable movable) {
        entities.add(movable);
    }

    public void update() {
        for (Movable entity : entities) {
            if (!entity.isMoving()) {
                continue;
            }

            Transform old = entity.getPosition();
            Vector vel = entity.getVelocity();
            long newX = Math.max(Math.min(old.getX() + vel.getX(), X_FENCE_MAX), X_FENCE_MIN);
            long newY = Math.max(Math.min(old.getY() + vel.getY(), Y_FENCE_MAX), Y_FENCE_MIN);
            entity.moveTo(newX, newY);
        }
    }
}

Одиночный тест:

@Test
@DisplayName("Moving player and monster at the same time")
public void testMovement() {
    // Given
    Player fighter = playerFactory.createPlayer(PlayerClass.Fighter, "MyFighter");
    fighter.moveTo(2, 5);
    fighter.startMove(1, 0);

    Monster orc = monsterFactory.createMonster(MonsterClass.Orc, 100);
    orc.moveTo(10, 5);
    orc.startMove(-1, 0);

    movementSystem.register(fighter);
    movementSystem.register(orc);

    // When
    movementSystem.update();

    // Then
    assertThat(fighter.getPosition().getX()).isEqualTo(2 + 1);
    assertThat(orc.getPosition().getX()).isEqualTo(10 - 1);
}

Здесь MovementSystem является относительно независимой доменной службой.Посредством компонентизации Movable реализуется централизация схожих кодов и централизация некоторых общих зависимостей/конфигураций (таких как границы X, Y и т. д.).

Некоторые спецификации дизайна уровня домена DDD

Выше я в основном сравнивал три реализации ООП, ECS и DDD для одного и того же примера, сравнение выглядит следующим образом:

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

  • Код ECS на основе компонентов: код ECS обладает высочайшей гибкостью, возможностью повторного использования и производительностью, но сильно ослабляет связность классов сущностей.Вся бизнес-логика написана в сервисе, что приведет к согласованности бизнеса. оказывают большее влияние на бизнес-систему.

  • Архитектура DDD на основе доменных объектов + доменных сервисов: правила DDD на самом деле самые сложные, при этом необходимо учитывать связанность классов сущностей и гарантированных инвариантов, также необходимо учитывать атрибуцию межобъектных кодов правил, и даже конкретные домены надо учитывать.То, как сервис называется, дорого для понимания.

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

Класс сущности

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

Создать последовательно

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

Параметр конструктора должен содержать все необходимые свойства или иметь разумные значения по умолчанию в конструкторе.

Например, создание учетной записи:

public class Account {
    private String accountNumber;
    private Long amount;
}

@Test
public void test() {
    Account account = new Account();
    account.setAmount(100L);
    TransferService.transfer(account); // 报错了,因为Account缺少必要的AccountNumber
}

Без надежного конструктора проверки согласованность созданного объекта не может быть гарантирована. Итак, вам нужно добавить сильный конструктор проверки:

public class Account {
    public Account(String accountNumber, Long amount) {
        assert StringUtils.isNotBlank(accountNumber);
        assert amount >= 0;
        this.accountNumber = accountNumber;
        this.amount = amount;
    }
}

@Test
public void test() {
    Account account = new Account("123", 100L); // 确保对象的有效性
}

Используйте шаблон Factory, чтобы уменьшить сложность вызывающей стороны

Другой метод — создавать объекты с помощью шаблона Factory, уменьшая количество повторяющихся входных параметров. Например:

public class WeaponFactory {
    public Weapon createWeaponFromPrototype(WeaponPrototype proto, String newName) {
        Weapon weapon = new Weapon(null, newName, proto.getWeaponType(), proto.getDamage(), proto.getDamageType());
        return weapon;
    }
}

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

Старайтесь избегать публичных сеттеров

Одной из наиболее вероятных причин несогласованности является то, что объекты предоставляют общедоступные методы установки, особенно когда установка одного параметра может привести к несогласованному состоянию. Например, заказ может содержать подсущности, такие как статус заказа (заказано, оплачено, отправлено, получено), платежное поручение, логистический заказ и т. д. Если вызывающий абонент может установить статус заказа по своему желанию, это может привести к тому, что статус заказа не совпадает с подсущностями, что приводит к тому, что бизнес-процесс не может пройти. Итак, в сущности вам нужно изменить внутреннее состояние с помощью метода поведения:

@Data @Setter(AccessLevel.PRIVATE) // 确保不生成public setter
public class Order {
    private int status; // 0 - 创建,1 - 支付,2 - 发货,3 - 收货
    private Payment payment; // 支付单
    private Shipping shipping; // 物流单

    public void pay(Long userId, Long amount) {
        if (status != 0) {
            throw new IllegalStateException();
        }
        this.status = 1;
        this.payment = new Payment(userId, amount);
    }

    public void ship(String trackingNumber) {
        if (status != 1) {
            throw new IllegalStateException();
        }
        this.status = 2;
        this.shipping = new Shipping(trackingNumber);
    }
}

[Предложение] В некоторых простых сценариях иногда значение может быть установлено произвольно, не вызывая противоречий.Также рекомендуется переписать имя метода на более "поведенческое" имя, что улучшит его семантику. Например, setPosition(x, y) можно назвать moveTo(x, y), setAddress можно назвать assignAddress и т. д.

Обеспечьте согласованность основных и подсущностей через совокупный корень

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

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

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

  • Подобъекты могут независимо изменять свое состояние, но согласованность состояния между несколькими подобъектами должна гарантироваться совокупным корнем.

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

Нельзя сильно полагаться на другие агрегированные корневые объекты или службы домена.

Принцип сущности — высокая связность и низкая связанность, то есть класс сущности не может напрямую зависеть от внешней сущности или службы внутри. Этот принцип имеет серьезные противоречия с большинством фреймворков ORM, поэтому он требует особого внимания в процессе разработки. Необходимые причины для этого принципа включают: зависимость от внешних объектов напрямую приведет к отказу объекта от однократной проверки; и объект не может гарантировать, что изменение внешнего объекта не повлияет на непротиворечивость и правильность объекта. .

Итак, есть два правильных подхода к внешним зависимостям:

1 Сохраняйте только идентификатор внешнего объекта: здесь я снова настоятельно рекомендую использовать строго типизированный объект идентификатора вместо длинного идентификатора. Строго типизированный объект идентификатора может не только содержать код проверки, чтобы гарантировать правильность значения идентификатора, но и гарантировать, что различные входные параметры не вызовут ошибок из-за изменения порядка параметров. Подробнее см. в моей статье «Примитив домена».

2 Для внешних зависимостей без побочных эффектов они передаются с помощью параметров метода. Например, приведенный выше метод equip(Weapon, EquipmentService).

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

Действия любой сущности могут непосредственно влиять только на эту сущность (и ее подсущности)

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

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

Служба домена

Как упоминалось выше, на самом деле существует много типов доменных служб.Здесь мы суммируем три наиболее распространенных на основе вышеизложенного:

стратегия одного объекта

Этот тип объекта домена в основном ориентирован на изменение одного объекта объекта, но включает некоторые правила нескольких объектов домена или внешних зависимостей. В приведенном выше EquipmentService такой:

  • Объектом изменения является параметр Player

  • Он считывает данные игрока и оружия, а также может считывать некоторые данные извне.

В этом типе объект должен передать эту доменную службу в качестве параметра метода, а затем отменить метод вызова доменной службы через Double Dispatch, например:

Player.equip(Weapon, EquipmentService) {
    EquipmentService.canEquip(this, Weapon);
}

Почему вы не можете сначала вызвать доменную службу, а затем вызвать метод объекта сущности в этом случае, чтобы уменьшить зависимость ввода параметров сущности от доменной службы? Например, следующий метод неверен:

boolean canEquip = EquipmentService.canEquip(Player, Weapon);
if (canEquip) {
    Player.equip(Weapon); // ❌,这种方法不可行,因为这个方法有不一致的可能性
}

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

межобъектный транзакционный

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

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

public class Player {
    void attack(Monster, CombatService) {
        CombatService.performAttack(this, Monster); // ❌,不要这么写,会导致副作用
    }
}

И наш реальный вызов должен напрямую вызывать метод CombatService:

public void test() {
    //...
    combatService.performAttack(mage, orc);
}

Этот принцип также отражает принцип 4.1.5, то есть Player.attack напрямую воздействует на Монстра, но Монстр не знает об этом вызове.

Универсальный тип компонента

Этот тип доменной службы больше похож на систему в ECS, обеспечивая компонентное поведение, но он не привязан напрямую к классу сущностей. Для конкретных случаев, пожалуйста, обратитесь к реализации MovementSystem выше.

Объект политики (политика домена)

Шаблон проектирования «Политика» или «Стратегия» — это общий шаблон проектирования, но он часто появляется в архитектуре DDD, и его ядром является инкапсуляция доменных правил.

Политика — это одноэлементный объект без состояния, для которого обычно требуется как минимум два метода: canApply и бизнес-метод. Среди них метод canApply используется для определения того, применима ли политика к текущему контексту, и, если применимо, вызывающая сторона активирует бизнес-метод. Как правило, чтобы уменьшить тестируемость и сложность политики, политика не должна работать с объектом напрямую, а должна управлять объектом в доменной службе, возвращая вычисленное значение.

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

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

Закуски - Обработка побочных эффектов - События предметной области

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

  • Когда HP монстра упадет до 0, наградите игрока очками опыта.

Решений этой проблемы множество, например, написание побочных эффектов прямо в CombatService:

public class CombatService {
    public void performAttack(Player player, Monster monster) {
        // ...
        monster.takeDamage(damage);
        if (!monster.isAlive()) {
            player.receiveExp(10); // 收到经验
        }
    }
}

Но проблема с этим написанием: скоро код CombatService сильно усложнится, например, добавим побочный эффект:

  • Когда опыт игрока достигает 100, повышайте уровень

Тогда наш код станет таким:

public class CombatService {
    public void performAttack(Player player, Monster monster) {
        // ...
        monster.takeDamage(damage);
        if (!monster.isAlive()) {
            player.receiveExp(10); // 收到经验
            if (player.canLevelUp()) {
                player.levelUp(); // 升级
            }
        }
    }
}

Что, если вы добавите «вознаграждение XXX после обновления»? А как насчет «Обновить рейтинг XXX»? И так далее, последующий код не будет поддерживаться. Итак, нам нужно ввести последнюю концепцию уровня предметной области: Событие предметной области.

Выездные мероприятия Введение

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

Поэтому события предметной области являются рекомендуемым механизмом распространения «побочного эффекта» между сущностями в DDD.

Реализация события предметной области

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

// 实现者:瑜进 2019/11/28
public class EventBus {

    // 注册器
    @Getter
    private final EventRegistry invokerRegistry = new EventRegistry(this);

    // 事件分发器
    private final EventDispatcher dispatcher = new EventDispatcher(ExecutorFactory.getDirectExecutor());

    // 异步事件分发器
    private final EventDispatcher asyncDispatcher = new EventDispatcher(ExecutorFactory.getThreadPoolExecutor());

    // 事件分发
    public boolean dispatch(Event event) {
        return dispatch(event, dispatcher);
    }

    // 异步事件分发
    public boolean dispatchAsync(Event event) {
        return dispatch(event, asyncDispatcher);
    }

    // 内部事件分发
    private boolean dispatch(Event event, EventDispatcher dispatcher) {
        checkEvent(event);
        // 1.获取事件数组
        Set<Invoker> invokers = invokerRegistry.getInvokers(event);
        // 2.一个事件可以被监听N次,不关心调用结果
        dispatcher.dispatch(event, invokers);
        return true;
    }

    // 事件总线注册
    public void register(Object listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener can not be null!");
        }
        invokerRegistry.register(listener);
    }

    private void checkEvent(Event event) {
        if (event == null) {
            throw new IllegalArgumentException("event");
        }
        if (!(event instanceof Event)) {
            throw new IllegalArgumentException("Event type must by " + Event.class);
        }
    }
}

Метод вызова:

public class LevelUpEvent implements Event {
    private Player player;
}

public class LevelUpHandler {
    public void handle(Player player);
}

public class Player {
    public void receiveExp(int value) {
        this.exp += value;
        if (this.exp >= 100) {
            LevelUpEvent event = new LevelUpEvent(this);
            EventBus.dispatch(event);
            this.exp = 0;
        }
    }
}
@Test
public void test() {
    EventBus.register(new LevelUpHandler());
    player.setLevel(1);
    player.receiveExp(100);
    assertThat(player.getLevel()).equals(2);
}

Недостатки и перспективы текущих выездных мероприятий

Как видно из приведенного выше кода, хорошая реализация доменных событий зависит от поддержки EventBus, Dispatcher и Invoker, которые относятся к уровню фреймворка. В то же время другая проблема заключается в том, что, поскольку Entity не может напрямую зависеть от внешних объектов, EventBus в настоящее время может быть только глобальным синглтоном, и каждый должен знать, что глобальные объекты синглтона трудно тестировать по отдельности. Это легко приводит к тому, что объект Entity не может быть легко покрыт полным модульным тестом.

Другое решение — вторгнуться в Entity и добавить список в каждую Entity:

public class Player {
  List<Event> events;

  public void receiveExp(int value) {
        this.exp += value;
        if (this.exp >= 100) {
            LevelUpEvent event = new LevelUpEvent(this);
            events.add(event); // 把event加进去
            this.exp = 0;
        }
    }
}

@Test
public void test() {
    EventBus.register(new LevelUpHandler());
    player.setLevel(1);
    player.receiveExp(100);

    for(Event event: player.getEvents()) { // 在这里显性的dispatch事件
        EventBus.dispatch(event);
    }

    assertThat(player.getLevel()).equals(2);
}

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

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

Суммировать

В реальной бизнес-логике наша предметная модель имеет более-менее определенную «специализацию», если она на 100% соответствует спецификации DDD, это может быть утомительно, поэтому самое главное — разобраться с влиянием объекта на объект. поведения. , а затем принимать дизайнерские решения, а именно: затрагивает ли он только один объект или несколько объектов,

  • Будущее расширение и гибкость правил,

  • Требования к производительности,

  • Борьба с побочными эффектами и т.д.

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

Запросите кейс, запросите резюме

Наконец, я прошу читателей внести свой вклад. Я хочу написать о том, как заставить программиста не всегда писать код CRUD. Я надеюсь, что из некоторых очевидных случаев кода CRUD дизайн архитектуры DDD можно использовать для создания продуктов. ● Разработайте масштабируемую и удобную в сопровождении архитектуру. В настоящее время мне все еще не хватает некоторых реальных кейсов, и я надеюсь, что читатели и студенты могут прислать мне несколько кейсов по электронной почте, включая (десенсибилизированные) коды и описания бизнеса. Обещаю ответить на каждый кейс максимально подробно, а классические кейсы я приведу в статью. мой почтовый ящик:guangmiao.lgm@alibaba-inc.com, вы также можете добавить мой номер ногтя: luangm (Инь Хао)

Параллельно продолжается набор в нашу команду. Я отвечаю за отраслевую и торговую команду Департамента Тао. Наша команда отвечает за повседневные потребности бизнеса и инновационный бизнес (3D / AR, сопоставление, настройка, руководство по покупкам по размеру) четырех основных отраслей (одежда, быстро -перемещение потребительских товаров, потребление электроэнергии и обустройство дома) Tmall и Taobao и т. д.), фронт-офиса (iFashion, Global Shopping, Probaby, Beauty Academy, Coldplay Planet и т. д.), а также поле горизонтального шоппинг-гида Hand Taobao (хорошие товары, хорошие магазины, награды, справочники по магазинам и т. д.), общий DAU (среднее ежедневное количество посещающих пользователей) составляет около 3000 Вт. Основная цель нашей команды в этом году — воссоздать опыт руководства по покупкам в отрасли и предложить потребителям руководство по покупкам, отличающееся от других, более человечное, более интерактивное и способное лучше отражать дифференцированные характеристики и культуру каждого сегмента отрасли. Заинтересованные студенты могут присоединиться.