DDDがピッツェリアの新しいリビゞョンの䜜成にどのように圹立ったか

ピッツェリアでは、圚庫管理ず圚庫システムを構築するこずが重芁です。このシステムは、補品を玛倱したり、䞍芁な償华を実行したり、翌月の賌入を正しく予枬したりするために必芁です。改蚂の説明における重芁な圹割。圌らはあなたが食べ物の残りをチェックし、実際の量ずシステムに䜕があるかを確認するのに圹立ちたす。







Dodoの監査は玙ベヌスではありたせん。監査人はタブレットを持っおおり、監査人はすべおの補品に泚意しおレポヌトを䜜成したす。しかし、2020幎たで、ピッツェリアの改蚂は正確に玙片で行われおいたした。それは、その方が簡単だったからです。もちろん、これは䞍正確なデヌタ、゚ラヌ、損倱に぀ながりたした。人々は間違いを犯し、玙片が倱われ、さらに倚くのこずがありたす。この問題を修正し、タブレットの方法を改善するこずにしたした。実装はDDDを䜿甚するこずを決定したした。どのようにそれをしたか、私たちはさらにあなたに話したす。



たず、コンテキストを理解するために、ビゞネスプロセスに぀いお簡単に説明したす。補品のフロヌチャヌトずその改蚂点を怜蚎しおから、技術的な詳现に移りたす。これは倚くのこずになるでしょう。



補品の移動のスキヌムず監査が必芁な理由



私たちのネットワヌクには600以䞊のピッツェリアがありたすそしおこの数は増え続けるでしょう。補品の準備ず販売、有効期限たでの材料の償华から、チェヌンの他のピッツェリアぞの原材料の移動たで、それぞれに原材料の移動が毎日ありたす。ピッツェリアのバランスには、補品の補造に必芁な玄120のアむテムに加えお、ピッツェリアを枅朔に保぀ための倚くの消耗品、家庭甚品、化孊薬品が垞に含たれおいたす。これには、どの原材料が豊富でどれが䞍足しおいるかを知るために「䌚蚈凊理」が必芁です。 



「䌚蚈」ずは、ピッツェリアでの原材料の動きを指したす。玍期はバランスシヌト䞊でプラスであり、償华はマむナスです。たずえば、ピザを泚文するず、キャッシャヌは泚文を受け入れお凊理のために送信したす。その埌、生地を広げ、チヌズ、トマト゜ヌス、ペペロヌニなどの材料を詰めたす。これらの補品はすべお生産に入る-償华されたす。たた、有効期限が終了するず償华が発生する堎合がありたす。



玍品ず償华の結果、「倉庫残高」が圢成されたす。これは、情報システムの運甚に基づいお、バランスシヌトに含たれる原材料の量を反映したレポヌトです。これがすべお「決枈バランス」です。しかし、「実際の䟡倀」がありたす。぀たり、珟圚実際に圚庫されおいる原材料の量です。



改蚂



実際の倀を蚈算するために、「リビゞョン」が䜿甚されたすこれらは「むンベントリ」ずも呌ばれたす。 



監査は、賌入する原材料の量を正確に蚈算するのに圹立ちたす。賌入が倚すぎるず、䜜業資本が凍結され、過剰な補品を償华するリスクが高たり、損倱にも぀ながりたす。原材料の䜙剰は危険であるだけでなく、䞍足も危険です。これにより、䞀郚の補品の生産が停止し、収益が枛少する可胜性がありたす。監査は、蚘録された未蚈䞊の原材料の損倱によっお䌁業がどれだけの利益を倱っおいるのかを確認し、コストの削枛に取り組むのに圹立ちたす。



リビゞョンは、レポヌトの䜜成など、さらなる凊理を十分に考慮しおデヌタを共有したす。



改蚂プロセスの問題、たたは叀い改蚂の仕組み



改蚂は骚の折れるプロセスです。時間がかかり、原材料の残骞のカりントず修正、保管堎所ごずの原材料の結果の芁玄、DodoIS情報システムぞの結果の入力などのいく぀かの段階で構成されたす。



以前は、原材料のリストが蚘茉されたペンず玙のフォヌムを䜿甚しお監査が実行されおいたした。結果を手動で芁玄、調敎、およびDodo ISに転送する堎合、間違いを犯す可胜性がありたす。完党監査では、100皮類以䞊の原材料がカりントされ、蚈算自䜓が深倜や早朝に行われるこずが倚く、集䞭力が䜎䞋する可胜性がありたす。



問題を解決する方法



Game of Threadsチヌムは、ピッツェリアで䌚蚈を開発しおいたす。ピッツェリアの監査を簡玠化する「監査人甚タブレット」ずいうプロゞェクトを立ち䞊げるこずにしたした。アカりンティングの䞻芁コンポヌネントが実装されおいる独自の情報システムDodoISですべおを行うこずにしたため、サヌドパヌティのシステムず統合する必芁はありたせん。さらに、私たちが存圚するすべおの囜は、远加の統合に頌るこずなくツヌルを䜿甚できるようになりたす。



プロゞェクトの䜜業を開始する前でさえ、私たちチヌムはDDDを実際に適甚したいずいう願望に぀いお話し合いたした。幞いなこずに、プロゞェクトの1぀はすでにこのアプロヌチをうたく適甚しおいるので、ご芧いただける䟋がありたす。これが「キャッシュデスク」プロゞェクトです。



この蚘事では、開発で䜿甚した戊術的なDDDパタヌン集玄、コマンド、ドメむンむベント、アプリケヌションサヌビス、および制限付きコンテキストの統合に぀いお説明したす。DDDの戊略的パタヌンず基本に぀いおは説明したせん。説明しないず、蚘事が非垞に長くなりたす。これに぀いおは、「ドメむンドリブンデザむンに぀いお10分で䜕を孊ぶこずができたすか」ずいう蚘事ですでに説明したした。「」



リビゞョンの新しいバヌゞョン



監査を開始する前に、正確に䜕を数えるかを知る必芁がありたす。このために、リビゞョンテンプレヌトが必芁です。これらは「オフィスマネヌゞャヌ」の圹割によっお構成されたす。リビゞョンテンプレヌトはInventoryTemplate゚ンティティです。次のフィヌルドが含たれおいたす。



  • テンプレヌト識別子;

  • ピッツェリアID;

  • テンプレヌト名;

  • 改蚂カテゎリ毎月、毎週、毎日。

  • ナニット;

  • 保管堎所ずこの保管堎所の原材料 



この゚ンティティには、CRUD機胜が実装されおいるため、詳现に぀いおは説明したせん。



監査人は、テンプレヌトのリストを持っおいたら、圌は開始するこずができ、監査を。これは通垞、ピッツェリアが閉じおいるずきに発生したす。珟時点では、泚文はなく、原材料も動いおいたせん。バランスに関するデヌタを確実に取埗できたす。



監査を開始するず、監査人は冷蔵庫などのゟヌンを遞択し、そこで原材料を数えたす。冷蔵庫の䞭で、圌は5パックのチヌズそれぞれ10 kgを芋お、蚈算機に10 kg * 5を入力し、[さらに入力]を抌したす。次に、䞀番䞊の棚にさらに2぀のパックがあるこずに気づき、[远加]をクリックしたす。その結果、圌は2぀の枬定倀それぞれ50kgず20kgを持っおいたす。



メヌタリング特定の地域の怜査官が入力した原材料の量を呌び出したすが、必ずしも合蚈ずは限りたせん。怜査官は、1キログラムの2぀の枬定倀を入力するこずも、1回の枬定で2キログラムを入力するこずもできたす。任意の組み合わせを入力できたす。重芁なこずは、監査人自身が明確でなければならないずいうこずです。





蚈算機むンタヌフェヌス。



そのため、監査人は段階的に1〜2時間ですべおの原材料を怜蚎し、監査を完了したす。



アクションのアルゎリズムは非垞に単玔です。



  • 監査人は監査を開始できたす。

  • 監査人は、開始されたリビゞョンに枬定倀を远加できたす。

  • 監査人は監査を完了するこずができたす。



システムのビゞネス芁件は、このアルゎリズムから圢成されたす。



ドメむンのアグリゲヌト、コマンド、むベントの最初のバヌゞョンの実装



たず、DDD戊術テンプレヌトセットに含たれる甚語を定矩したしょう。この蚘事ではそれらを参照したす。



戊術的なDDDテンプレヌト



Aggregateは、゚ンティティオブゞェクトず倀オブゞェクトのクラスタヌです。クラスタヌ内のオブゞェクトは、デヌタ倉曎に関しおは単䞀の゚ンティティです。各集蚈には、゚ンティティず倀にアクセスするためのルヌト芁玠がありたす。ナニットは倧きすぎないように蚭蚈しおください。それらは倧量のメモリを消費し、トランザクションが正垞に完了する可胜性が䜎くなりたす。



集玄境界は、単䞀のトランザクション内で䞀貫しおいる必芁があるオブゞェクトのセットです。このクラスタヌ内のすべおの䞍倉条件を監芖する必芁がありたす。



䞍倉条件は、矛盟するこずのないビゞネスルヌルです。



コマンドナニットに察する䜕らかのアクションです。このアクションの結果ずしお、アグリゲヌトの状態を倉曎でき、1぀以䞊のドメむンむベントを生成できたす。



ドメむンむベントは、䞀貫性を維持するために必芁な、アグリゲヌトの状態の倉曎の通知です。集玄により、トランザクションの䞀貫性が保蚌されたす。すべおのデヌタを今ここで倉曎する必芁がありたす。結果ずしお埗られる䞀貫性により、長期的な䞀貫性が保蚌されたす。デヌタは倉曎されたすが、珟圚は倉曎されたせんが、無期限に倉曎されたす。この間隔は、メッセヌゞキュヌの茻茳、これらのメッセヌゞを凊理するための倖郚サヌビスの準備、ネットワヌクなど、倚くの芁因によっお異なりたす。



ルヌト芁玠䞀意のグロヌバル識別子を持぀゚ンティティです。子芁玠は、集合䜓党䜓の䞭でのみロヌカルIDを持぀こずができたす。それらは盞互に参照でき、ルヌト芁玠のみを参照できたす。



チヌムずむベント



チヌムずしおのビゞネス芁件を説明したしょう。コマンドは、説明フィヌルドを持぀単なるDTOです。



コマンド「addmeasurement」には、次のフィヌルドがありたす。



  • 枬定倀-特定の枬定単䜍内の原材料の量。枬定倀が削陀された堎合はnullになる可胜性がありたす。

  • バヌゞョン-枬定倀は線集できるため、バヌゞョンが必芁です。

  • 原材料識別子;

  • 枬定単䜍kg / g、l / ml、ピヌス;

  • ストレヌゞ゚リア識別子。



コマンドコヌドを远加する枬定
public sealed class AddMeasurementCommand
{
    // ctor

    public double? Value { get; }
    public int Version { get; }
    public UUId MaterialTypeId { get; }
    public UUId MeasurementId { get; }
    public UnitOfMeasure UnitOfMeasure { get; }
    public UUId InventoryZoneId { get; }
}




これらのコマンドの実行から生じるむベントも必芁です。むベントをむンタヌフェヌスでマヌクしIPublicInventoryEventたす。将来、倖郚の消費者ず統合するために必芁になりたす。



むベント「メヌタリング」のフィヌルドは、コマンド「メヌタリングの远加」ず同じですが、むベントが発生したナニットの識別子ずそのバヌゞョンも栌玍される点が異なりたす。



むベントコヌド「凍結」
public class MeasurementEvent : IPublicInventoryEvent
{
    public UUId MaterialTypeId { get; set; }
    public double? Value { get; set; }
	
    public UUId MeasurementId { get; set; }
    public int MeasurementVersion { get; set; }
    public UUId AggregateId { get; set; }
    public int Version { get; set; }
    public UnitOfMeasure UnitOfMeasure { get; set; }
    public UUId InventoryZoneId { get; set; }
}




コマンドずむベントに぀いお説明したら、集玄を実装できたすInventory。



InventoryAggregateの実装





UML集蚈図むンベントリ。



アプロヌチは次のずおりです。リビゞョンの開始により集玄の䜜成が開始さInventoryれたす。このため、factoryメ゜ッドを䜿甚Createし、コマンドでリビゞョンを開始しStartInventoryCommandたす。



各コマンドは、集蚈の状態を倉曎し、むベントをリストに保存したす。リストはchanges、蚘録のためにストレヌゞに送信されたす。たた、これらの倉曎に基づいお、倖界向けのむベントが生成されたす。



アグリゲヌトがInventory䜜成されるず、その埌のリク゚ストごずにアグリゲヌトを埩元しお、その状態を倉曎できたす。



  • changesナニットが最埌に埩元されおからの倉曎が保存されたす。

  • 状態はRestore、アグリゲヌトの珟圚のむンスタンスで、バヌゞョンで゜ヌトされた以前のすべおのむベントを再生するメ゜ッドによっお埩元されたすInventory。



これは、Event Sourcingナニット内でのアむデアの実装です。Event Sourcingリポゞトリのフレヌムワヌク内でアむデアを実装する方法に぀いおは、少し埌で説明したす。Vaughn Vernonの本からの玠晎らしいむラストがありたす





ナニットの状態は、発生した順序でむベントを適甚するこずによっお埩元されたす。



次に、チヌムによっおいく぀かの枬定が行われAddMeasurementCommandたす。監査はコマンドで終了しFinishInventoryCommandたす。アグリゲヌトは、䞍倉条件に準拠するようにメ゜ッドを倉曎する際にその状態を怜蚌したす。



ナニットはInventory完党にバヌゞョン管理されおおり、各枬定倀も同様であるこずに泚意するこずが重芁です。枬定はより困難です-むベント凊理方法の競合を解決する必芁がありたすWhen(MeasurementEvent e)。コヌドでは、コマンドの凊理のみを瀺しAddMeasurementCommandたす。



集蚈むンベントリコヌド
public sealed class Inventory : IEquatable<Inventory>
{
    private readonly List<IInventoryEvent> _changes = new List<IInventoryEvent>();

    private readonly List<InventoryMeasurement> _inventoryMeasurements = new List<InventoryMeasurement>();

    internal Inventory(UUId id, int version, UUId unitId, UUId inventoryTemplateId,
        UUId startedBy, InventoryState state, DateTime startedAtUtc, DateTime? finishedAtUtc)
	
        : this(id)
    {
        Version = version;
        UnitId = unitId;
        InventoryTemplateId = inventoryTemplateId;
        StartedBy = startedBy;
        State = state;
        StartedAtUtc = startedAtUtc;
        FinishedAtUtc = finishedAtUtc;
	
    }

    private Inventory(UUId id)
    {
        Id = id;
        Version = 0;
        State = InventoryState.Unknown;
    }
	
    public UUId Id { get; private set; }
    public int Version { get; private set; }
    public UUId UnitId { get; private set; }
    public UUId InventoryTemplateId { get; private set; }
    public UUId StartedBy { get; private set; }
    public InventoryState State { get; private set; }
    public DateTime StartedAtUtc { get; private set; }
    public DateTime? FinishedAtUtc { get; private set; }
    public ReadOnlyCollection<IInventoryEvent> Changes => _changes.AsReadOnly();
	
    public ReadOnlyCollection<InventoryMeasurement> Measurements => _inventoryMeasurements.AsReadOnly();

    public static Inventory Restore(UUId inventoryId, IInventoryEvent[] events)
    {
        var inventory = new Inventory(inventoryId);
        inventory.ReplayEvents(events);
        return inventory;
    }

    public static Inventory Restore(UUId id, int version, UUId unitId, UUId inventoryTemplateId,
        UUId startedBy, InventoryState state, DateTime startedAtUtc, DateTime? finishedAtUtc,
        InventoryMeasurement[] measurements)
    {
        var inventory = new Inventory(id, version, unitId, inventoryTemplateId,
            startedBy, state, startedAtUtc, finishedAtUtc);

        inventory._inventoryMeasurements.AddRange(measurements);

        return inventory;
    }

    public static Inventory Create(UUId inventoryId)
    {
        if (inventoryId == null)
        {
            throw new ArgumentNullException(nameof(inventoryId));
        }

        return new Inventory(inventoryId);
    }

    public void ReplayEvents(params IInventoryEvent[] events)
    {
        if (events == null)
        {
            throw new ArgumentNullException(nameof(events));
        }

        foreach (var @event in events.OrderBy(e => e.Version))
        {
            Mutate(@event);
        }
    }

    public void AddMeasurement(AddMeasurementCommand command)
    {
        if (command == null)
        {
            throw new ArgumentNullException(nameof(command));
        }

        Apply(new MeasurementEvent
        {
            AggregateId = Id,
            Version = Version + 1,
            UnitId = UnitId,
            Value = command.Value,
            MeasurementVersion = command.Version,
            MaterialTypeId = command.MaterialTypeId,
            MeasurementId = command.MeasurementId,
            UnitOfMeasure = command.UnitOfMeasure,
            InventoryZoneId = command.InventoryZoneId
        });
    }

    private void Apply(IInventoryEvent @event)
    {
        Mutate(@event);
        _changes.Add(@event);
    }

    private void Mutate(IInventoryEvent @event)
    {
        When((dynamic) @event);
        Version = @event.Version;
    }

    private void When(MeasurementEvent e)
    {
        var existMeasurement = _inventoryMeasurements.SingleOrDefault(x => x.MeasurementId == e.MeasurementId);
        if (existMeasurement is null)
    {
        _inventoryMeasurements.Add(new InventoryMeasurement
        {
            Value = e.Value,
            MeasurementId = e.MeasurementId,
            MeasurementVersion = e.MeasurementVersion,
            PreviousValue = e.PreviousValue,
            MaterialTypeId = e.MaterialTypeId,
            UserId = e.By,
            UnitOfMeasure = e.UnitOfMeasure,
            InventoryZoneId = e.InventoryZoneId
        });
    }
    else
    {
        if (!existMeasurement.Value.HasValue)
        {
            throw new InventoryInvalidStateException("Change removed measurement");
        }

        if (existMeasurement.MeasurementVersion == e.MeasurementVersion - 1)
        {
            existMeasurement.Value = e.Value;
            existMeasurement.MeasurementVersion = e.MeasurementVersion;
            existMeasurement.UnitOfMeasure = e.UnitOfMeasure;
            existMeasurement.InventoryZoneId = e.InventoryZoneId;
        }
        else if (existMeasurement.MeasurementVersion < e.MeasurementVersion)
        {
            throw new MeasurementConcurrencyException(Id, e.MeasurementId, e.Value);
        }
        else if (existMeasurement.MeasurementVersion == e.MeasurementVersion &&
            existMeasurement.Value != e.Value)
        {
            throw new MeasurementConcurrencyException(Id, e.MeasurementId, e.Value);
        }
        else
        {
            throw new NotChangeException();
        }
    }
}

// Equals
// GetHashCode
}




「枬定枈み」むベントが発生するず、この識別子を持぀既存の枬定倀の存圚がチェックされたす。そうでない堎合は、新しい枬定倀が远加されたす。



その堎合、远加のチェックが必芁です。



  • リモヌト枬定を線集するこずはできたせん。

  • 着信バヌゞョンは前のバヌゞョンよりも倧きくする必芁がありたす。



条件が満たされるず、既存の枬定倀に新しい倀ず新しいバヌゞョンを蚭定できたす。バヌゞョンが小さい堎合、これは競合です。このために、䟋倖をスロヌしたすMeasurementConcurrencyException。バヌゞョンが䞀臎し、倀が異なる堎合、これも競合状況です。バヌゞョンず倀の䞡方が䞀臎する堎合、倉曎は発生しおいたせん。このような状況は通垞発生したせん。



「枬定」゚ンティティには、「枬定の远加」コマンドずたったく同じフィヌルドが含たれおいたす。



゚ンティティコヌド「フリヌズ」
public class InventoryMeasurement
{
    public UUId MeasurementId { get; set; }
    public UUId MaterialTypeId { get; set; }
    public UUId UserId { get; set; }
    public double? Value { get; set; }

    public int MeasurementVersion { get; set; }

    public UnitOfMeasure UnitOfMeasure { get; set; }

    public UUId InventoryZoneId { get; set; }
}




パブリックアグリゲヌトメ゜ッドの䜿甚は、ナニットテストによっお十分に実蚌されおいたす。



ナニットテストコヌド「改蚂開始埌に枬定倀を远加」
[Fact]
public void WhenAddMeasurementAfterStartInventory_ThenInventoryHaveOneMeasurement()
{
    var inventoryId = UUId.NewUUId();
    var inventory = Domain.Inventories.Entities.Inventory.Create(inventoryId);
    var unitId = UUId.NewUUId();
    inventory.StartInventory(Create.StartInventoryCommand()
        .WithUnitId(unitId)
        .Please());

    var materialTypeId = UUId.NewUUId();
    var measurementId = UUId.NewUUId();
    var measurementVersion = 1;
    var value = 500;
    var cmd = Create.AddMeasurementCommand()
        .WithMaterialTypeId(materialTypeId)
        .WithMeasurement(measurementId, measurementVersion)
        .WithValue(value)
        .Please();
    inventory.AddMeasurement(cmd);

    inventory.Measurements.Should().BeEquivalentTo(new InventoryMeasurement
    {
        MaterialTypeId = materialTypeId,
        MeasurementId = measurementId,
        MeasurementVersion = measurementVersion,
        Value = value,
        UnitOfMeasure = UnitOfMeasure.Quantity
    });
}




すべおをたずめるコマンド、むベント、むンベントリ集蚈





むンベントリの終了を実行するずきのむンベントリの集玄ラむフサむクル。



この図は、コマンド凊理のプロセスを瀺しおいたすFinishInventoryCommand。凊理する前Inventoryに、コマンド実行時のナニットの状態を埩元する必芁がありたす。これを行うには、このナニットで実行されたすべおのむベントをメモリにロヌドしお再生したすp.1。 



改蚂の完了時点で、すでに次のむベントがありたす-改蚂の開始ず3぀の枬定倀の远加。これらのむベントは、コマンド凊理の結果ずしお登堎StartInventoryCommandし、AddMeasurementCommandそれに応じお、。デヌタベヌスでは、テヌブルの各行に、むベント自䜓のリビゞョンID、バヌゞョン、および本文が含たれおいたす。



この段階で、コマンドを実行したすFinishInventoryCommandp.2。このコマンドは、最初にナニットの珟圚の状態の有効性リビゞョンが状態にあるこずInProgressを確認し、次にFinishInventoryEventリストにむベントを远加するこずによっお新しい状態倉曎を生成したすchanges項目3。



コマンドが完了するず、すべおの倉曎がデヌタベヌスに保存されたす。その結果、むベントの新しい行ずFinishInventoryEventナニットの最新バヌゞョンがデヌタベヌスに衚瀺されたすp.4。



タむプInventoryリビゞョン-ネストされた゚ンティティに関連する集玄芁玠ずルヌト芁玠。したがっお、タむプInventoryはナニットの境界を定矩したす。集玄境界には、タむプMeasurement枬定の゚ンティティのリスト、および集玄で実行されたすべおのむベントのリストchangesが含たれたす。



機胜党䜓の実装



機胜ずは、特定のビゞネス芁件の実装を意味したす。この䟋では、枬定の远加機胜に぀いお怜蚎したす。この機胜を実装するには、「アプリケヌションサヌビス」ApplicationServiceの抂念を理解する必芁がありたす。



アプリケヌションサヌビスは、ドメむンモデルの盎接クラむアントです。 Application Servicesは、ACIDデヌタベヌスを䜿甚するずきにトランザクションを保蚌し、状態遷移がアトミックに保持されるようにしたす。さらに、アプリケヌションサヌビスはセキュリティ䞊の懞念にも察凊したす。



すでにナニットがありたすInventory..。機胜党䜓を実装するために、アプリケヌションサヌビスを完党に䜿甚したす。その䞭で、接続されおいるすべおの゚ンティティの存圚ず、ナヌザヌのアクセス暩を確認する必芁がありたす。すべおの条件が満たされた埌にのみ、ナニットの珟圚の状態を保存し、むベントを倖郚に送信するこずができたす。アプリケヌションサヌビスを実装するには、を䜿甚したすMediatR。



機胜コヌド「枬定倀の远加」
public class AddMeasurementChangeHandler 
    : IRequestHandler<AddMeasurementChangeRequest, AddMeasurementChangeResponse>
{
    // dependencies
    // ctor

    public async Task<AddMeasurementChangeResponse> Handle(
        AddMeasurementChangeRequest request,
        CancellationToken ct)
    {
        var inventory =
            await _inventoryRepository.GetAsync(request.AddMeasurementChange.InventoryId, ct);
        if (inventory == null)
        {
            throw new NotFoundException($"Inventory {request.AddMeasurementChange.InventoryId} is not found");
        }

        var user = await _usersRepository.GetAsync(request.UserId, ct);
        if (user == null)
        {
            throw new SecurityException();
        }

        var hasPermissions =
        await _authPermissionService.HasPermissionsAsync(request.CountryId, request.Token, inventory.UnitId, ct);
        if (!hasPermissions)
        {
            throw new SecurityException();
        }

        var unit = await _unitRepository.GetAsync(inventory.UnitId, ct);
        if (unit == null)
        {
            throw new InvalidRequestDataException($"Unit {inventory.UnitId} is not found");
        }

        var unitOfMeasure =

Enum.Parse<UnitOfMeasure>(request.AddMeasurementChange.MaterialTypeUnitOfMeasure);


        var addMeasurementCommand = new AddMeasurementCommand(	
            request.AddMeasurementChange.Value,
            request.AddMeasurementChange.Version,
            request.AddMeasurementChange.MaterialTypeId,
            request.AddMeasurementChange.Id,
            unitOfMeasure,
            request.AddMeasurementChange.InventoryZoneId);

        inventory.AddMeasurement(addMeasurementCommand);

        await HandleAsync(inventory, ct);

        return new AddMeasurementChangeResponse(request.AddMeasurementChange.Id, user.Id, user.GetName());
    }

    private async Task HandleAsync(Domain.Inventories.Entities.Inventory inventory, CancellationToken ct)
    {
            await _inventoryRepository.AppendEventsAsync(inventory.Changes, ct);
 
            try
            {
                await _localQueueDataService.Publish(inventory.Changes, ct);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "error occured while handling action");
            }
    }
}




むベント゜ヌシング



実装䞭に、いく぀かの理由でESアプロヌチを遞択するこずにしたした。



  • Dodoには、このアプロヌチの成功䟋がありたす。

  • ESを䜿甚するず、むンシデント䞭の問題を簡単に理解できたす。すべおのナヌザヌアクションが保存されたす。

  • 埓来のアプロヌチを採甚した堎合、ESに移行するこずはできたせん。



実装の考え方は非垞に単玔です-コマンドの結果ずしお衚瀺されたすべおの新しいむベントをデヌタベヌスに远加したす。アグリゲヌトを埩元するために、すべおのむベントを取埗しおむンスタンスで再生したす。毎回倧量のむベントを取埗しないようにするために、Nむベントごずに状態を削陀し、このスナップショットの残りを再生したす。



Inventory Aggregate Store ID
internal sealed class InventoryRepository : IInventoryRepository
{
    // dependencies
    // ctor

    static InventoryRepository()
    {
        EventTypes = typeof(IEvent)
            .Assembly.GetTypes().Where(x => typeof(IEvent).IsAssignableFrom(x))
            .ToDictionary(t => t.FullName, x => x);
    }

    public async Task AppendAsync(IReadOnlyCollection<IEvent> events, CancellationToken ct)
    {
        using (var session = await _dbSessionFactory.OpenAsync())
        {
            if (events.Count == 0) return;

            try
            {
                foreach (var @event in events)
                {
                    await session.ExecuteAsync(Sql.AppendEvent,
                        new
                        {
                            @event.AggregateId,
                            @event.Version,
                            @event.UnitId,
                            Type = @event.GetType().FullName,
                            Data = JsonConvert.SerializeObject(@event),
                            CreatedDateTimeUtc = DateTime.UtcNow
                        }, cancellationToken: ct);
                }
            }
            catch (MySqlException e)
                when (e.Number == (int) MySqlErrorCode.DuplicateKeyEntry)
            {
                throw new OptimisticConcurrencyException(events.First().AggregateId, "");
            }
        }
    }

    public async Task<Domain.Models.Inventory> GetInventoryAsync(
        UUId inventoryId,
        CancellationToken ct)
    {
        var events = await GetEventsAsync(inventoryId, 0, ct);

        if (events.Any()) return Domain.Models.Inventory.Restore(inventoryId, events);

        return null;
    }
    
    private async Task<IEvent[]> GetEventsAsync(
        UUId id,
        int snapshotVersion,
        CancellationToken ct)
    {
        using (var session = await _dbSessionFactory.OpenAsync())
    {
            var snapshot = await GetInventorySnapshotAsync(session, inventoryId, ct);
            var version = snapshot?.Version ?? 0;
        
            var events = await GetEventsAsync(session, inventoryId, version, ct);
            if (snapshot != null)
            {
                snapshot.ReplayEvents(events);
                return snapshot;
            }

            if (events.Any())
            {
                return Domain.Inventories.Entities.Inventory.Restore(inventoryId, events);
            }

            return null;
        }
    }

    private async Task<Inventory> GetInventorySnapshotAsync(
        IDbSession session,
        UUId id,
        CancellationToken ct)
    {
        var record =
            await session.QueryFirstOrDefaultAsync<InventoryRecord>(Sql.GetSnapshot, new {AggregateId = id},
                cancellationToken: ct);
        return record == null ? null : Map(record);
    }

    private async Task<IInventoryEvent[]> GetEventsAsync(
        IDbSession session,
        UUId id,
        int snapshotVersion,
        CancellationToken ct)
    {
        var rows = await session.QueryAsync<EventRecord>(Sql.GetEvents,
            new
            {
                AggregateId = id,
                Version = snapshotVersion
            }, cancellationToken: ct);
        return rows.Select(Map).ToArray();
    }

    private static IEvent Map(EventRecord e)
    {
        var type = EventTypes[e.Type];
        return (IEvent) JsonConvert.DeserializeObject(e.Data, type);
    }
}

internal class EventRecord
{
    public string Type { get; set; }
    public string Data { get; set; }
}




数か月の運甚埌、ナニットむンスタンスにすべおのナヌザヌアクションを保存する必芁がないこずに気付きたした。䌁業はこの情報を䞀切䜿甚したせん。そうは蚀っおも、このアプロヌチを維持するにはオヌバヌヘッドがありたす。すべおの長所ず短所を評䟡した埌、ESから埓来のアプロヌチに移行する予定です。蚘号EventsをInventoriesずに眮き換えMeasurementsたす。



倖郚の制限されたコンテキストずの統合



これは、制限されたコンテキストInventoryが倖の䞖界ず盞互䜜甚する方法です。





リビゞョンコンテキストず他のコンテキストずの盞互䜜甚。この図は、コンテキスト、サヌビス、およびそれらが盞互に属しおいるこずを瀺しおいたす。



以䞋の堎合Auth、Inventory及びDatacatalog、各サヌビスに察しお1぀の制限されたコンテキストがありたす。モノリスはいく぀かの機胜を実行したすが、珟圚はピッツェリアの䌚蚈機胜にのみ関心がありたす。改蚂に加えお、䌚蚈には、ピッツェリアでの原材料の移動受領、転送、償华も含たれたす。



HTTP



このサヌビスはHTTPInventoryをAuth介しお察話したす。たず、ナヌザヌはに盎面したすAuth。これにより、ナヌザヌは䜿甚可胜な圹割の1぀を遞択するように求められたす。



  • システムには、ナヌザヌが監査䞭に遞択する「監査人」の圹割がありたす。

  • .

  • .



最埌の段階で、ナヌザヌはからのトヌクンを持っおいたすAuth。リビゞョンサヌビスはこのトヌクンを怜蚌する必芁があるAuthため、怜蚌を芁求したす。Authトヌクンの有効期限が切れおいるかどうか、所有者のものかどうか、たたは必芁なアクセス暩があるかどうかを確認したす。すべおが順調であればInventory、スタンプをCookieナヌザヌID、ログむン、ピッツェリアIDに保存し、Cookieの有効期間を蚭定したす。



泚。Authこのサヌビスがどのように機胜するかに぀いおは、「承認の埮劙さOAuth2.0テクノロゞヌの抂芁」の蚘事で詳しく説明したした。メッセヌゞキュヌを介しお



他のサヌビスずInventory察話したす。同瀟は、RabbitMQをメッセヌゞブロヌカヌずしお䜿甚し、その䞊のバむンディングであるMassTransitを䜿甚しおいたす。



RMQむベントの消費



ディレクトリサヌビス---䌚蚈、囜、郚門、ピッツェリアの原材料など、必芁なすべおの゚ンティティDatacatalogを提䟛しInventoryたす。



むンフラストラクチャの詳现には觊れずに、むベントを消費する基本的な考え方に぀いお説明したす。ディレクトリサヌビスの偎では、すべおがすでにむベントを公開する準備ができおいたす。原材料゚ンティティの䟋を芋おみたしょう。



デヌタカタログむベント契玄コヌド
namespace Dodo.DataCatalog.Contracts.Products.v1
{
    public class MaterialType
    {
        public UUId Id { get; set; }
        public int Version { get; set; }
        public int CountryId { get; set; }
        public UUId DepartmentId { get; set; }

        public string Name { get; set; }
        public MaterialCategory Category { get; set; }
        public UnitOfMeasure BasicUnitOfMeasure { get; set; }
        public bool IsRemoved { get; set; }
    }

    public enum UnitOfMeasure
    {
        Quantity = 1,
        Gram = 5,
        Milliliter = 7,
        Meter = 8,
    }

    public enum MaterialCategory
    {
        Ingredient = 1,
        SemiFinishedProduct = 2,
        FinishedProduct = 3,
        Inventory = 4,
        Packaging = 5,
        Consumables = 6
    }
}




この投皿はに公開されおいexchangeたす。各サヌビスは、exchange-queueむベントを消費するための独自のバンドルを䜜成できたす。





RMQプリミティブを介しおむベントずその消費を公開するためのスキヌム。



最終的に、サヌビスがサブスクラむブできる゚ンティティごずにキュヌがありたす。残っおいるのは、新しいバヌゞョンをデヌタベヌスに保存するこずだけです。



Datacatalogのむベントコンシュヌマヌコヌド
public class MaterialTypeConsumer : IConsumer<Dodo.DataCatalog.Contracts.Products.v1.MaterialType>
{
    private readonly IMaterialTypeRepository _materialTypeRepository;

    public MaterialTypeConsumer(IMaterialTypeRepository materialTypeRepository)
    {
         _materialTypeRepository = materialTypeRepository;
    }
 
    public async Task Consume(ConsumeContext<Dodo.DataCatalog.Contracts.Products.v1.MaterialType> context)
    {
        var materialType = new AddMaterialType(context.Message.Id,
            context.Message.Name,
            (int)context.Message.Category,
            (int)context.Message.BasicUnitOfMeasure,
            context.Message.CountryId,
            context.Message.DepartmentId,
            context.Message.IsRemoved,
            context.Message.Version);
    
        await _materialTypeRepository.SaveAsync(materialType, context.CancellationToken);
    }
}




RMQむベントの公開



モノリスのアカりンティング郚分は、Inventoryリビゞョンデヌタを必芁ずする残りの機胜をサポヌトするためにデヌタを消費したす。他のサヌビスに通知したいすべおのむベントは、むンタヌフェヌスでマヌクされおいたすIPublicInventoryEvent。この皮のむベントが発生するず、倉曎ログchangesからそれらを分離し、ディスパッチキュヌに送信したす。このために、2぀のテヌブルが䜿甚さpublicqueueれpublicqueue_archiveたす。



メッセヌゞの配信を保蚌するために、通垞「ロヌカルキュヌ」ず呌ばれるパタヌンを䜿甚したす。これはを意味しTransactional outbox patternたす。アグリゲヌトの状態の保存Inventoryずむベントのロヌカルキュヌぞの送信は、1぀のトランザクションで行われたす。トランザクションがコミットされるずすぐに、ブロヌカヌにメッセヌゞを送信しようずしたす。



メッセヌゞが送信された堎合、キュヌから削陀されたすpublicqueue。そうでない堎合は、埌でメッセヌゞを送信しようずしたす。次に、モノリスおよびデヌタパむプラむンのサブスクラむバヌがメッセヌゞを消費したす。このテヌブルpublicqueue_archiveには、ある時点で必芁になった堎合にむベントを再ディスパッチするのに䟿利なデヌタが氞久に保存されたす。



メッセヌゞブロヌカヌにむベントを公開するためのコヌド
internal sealed class BusDataService : IBusDataService
{
    private readonly IPublisherControl _publisherControl;
    private readonly IPublicQueueRepository _repository;
    private readonly EventMapper _eventMapper;

    public BusDataService(
        IPublicQueueRepository repository,
        IPublisherControl publisherControl,
        EventMapper eventMapper)
    {
        _repository = repository;
        _publisherControl = publisherControl;
        _eventMapper = eventMapper;
    }

    public async Task ConsumePublicQueueAsync(int batchEventSize, CancellationToken cancellationToken)
    {
        var events = await _repository.GetAsync(batchEventSize, cancellationToken);
        await Publish(events, cancellationToken);
    }

    public async Task Publish(IEnumerable<IPublicInventoryEvent> events, CancellationToken ct)
    {
        foreach (var @event in events)
        {
            var publicQueueEvent = _eventMapper.Map((dynamic) @event);
            await _publisherControl.Publish(publicQueueEvent, ct);
            await _repository.DeleteAsync(@event, ct);
       }
    }
}




レポヌトのためにむベントをモノリスに送信したす。損倱ず䜙剰のレポヌトを䜿甚するず、任意の2぀のリビゞョンを盞互に比范できたす。たた、すでに述べた重芁なレポヌト「倉庫残高」がありたす。 



なぜデヌタパむプラむンにむベントを送信するのですかすべお同じ-レポヌト甚ですが、新しいレヌルでのみです。以前は、すべおのレポヌトがモノリスに存圚しおいたしたが、珟圚は削陀されおいたす。これには、生産デヌタず分析デヌタの保存ず凊理ずいう2぀の責任がありたす。OLTPずOLAPです。これは、むンフラストラクチャず開発の䞡方の芳点から重芁です。



結論



ドメむンドリブンデザむンの原則ず実践に埓うこずにより、ナヌザヌのビゞネスニヌズを満たす信頌性が高く柔軟なシステムを構築するこずができたした。たずもな補品だけでなく、倉曎が簡単な優れたコヌドも入手できたした。あなたのプロゞェクトにドメむンドリブンデザむンを䜿甚する堎所があるこずを願っおいたす。



DDDの詳现に぀いおは、DDDevotionコミュニティおよびDDDevotionYoutubeチャンネルをご芧ください。DodoEngineeringチャットのTelegramで蚘事に぀いお話し合うこずができたす。



All Articles