npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cdziv/devkit-ddd

v0.2.3

Published

[English](https://github.com/cdziv/devkit/blob/main/packages/ddd/README.md) | 繁體中文

Readme

@cdziv/devkit-ddd

English | 繁體中文

介紹

@cdziv/devkit-ddd 協助您在程式實作面實現領域驅動設計(DDD),建構領域知識清晰、易讀、易維護、可擴展的應用。它不依賴任何框架、ORM,可以按需使用於您的應用中。

提示: 此文件假設您對 DDD 有基本概念,若尚未暸解 DDD 以及不知道是否該引入至您的應用中,Eric Evans 的《領域驅動設計》是很好的開始。

快速起步

在這個章節中,我們將在使用這個函式庫的核心功能,在領域層實作一個簡單的撲克遊戲範例。

安裝

npm install @cdziv/devkit-ddd

值物件

首先,玩家需要一個使用者名稱、籌碼堆。我們將使用者名稱設計為一個值物件:

// 建立一個值為 DomainPrimitive 的值物件
type UsernameValue = string;
class Username extends ValueObject<UsernameValue> {
  // 必須實作驗證方法
  validate(value: UsernameValue): ValidationResult {
    if (value.length < 3 || value.length > 30) {
      return false;
    }
    return true;
  }
}

expect(new Username('john_lennon').value).toBe('john_lennon');

在實例化時傳入一個合法的值,透過 value 屬性取得該值。如果傳入的值沒有通過驗證將拋出錯誤:

const username = new Username('john_lennon');
expect(username.value).toBe('john_lennon');

const invalidName = '';
expect(() => new Username(invalidName)).toThrow(ArgumentInvalidError);

接下來,我們將籌碼堆也設計為一個值物件。籌碼堆有餘額、下注數兩個屬性,我們使用 ChipCount 作為這兩個屬性的值,將領域不變量邏輯封裝在 ChipCount 中,所以我們不需要擔心它們是否為正整數。另外,我們也定義了一些行為使用了 evolve 更新它們的值:

提示: evolve 方法使用 Immer 來實現更新機制,所以你會看到它接受像是 (draft) => { draft.someProps = 'new value' } 的參數。但對於值為 domain primitive 的值物件來說,你可以直接傳入 domain primitive value。

type ChipCountValue = number;
class ChipCount extends ValueObject<ChipCountValue> {
  add(count: ChipCount): ChipCount {
    return this.evolve(this.value + count.value);
  }

  subtract(count: ChipCount): ChipCount {
    if (count.value > this.value) {
      throw new DddError('Not enough chips');
    }
    return this.evolve(this.value - count.value);
  }

  validate(value: ChipCountValue): ValidationResult {
    if (value < 0 || !Number.isInteger(value)) {
      return false;
    }
    return true;
  }
}

type StackValue = {
  balance: ChipCount;
  bet: ChipCount;
};
class Stack extends ValueObject<StackValue> {
  raise(count: ChipCount): Stack {
    if (this.value.balance.value < count.value) {
      throw new DddError('Not enough balance');
    }
    // You should use recipe function when value is not domain primitive
    return this.evolve((draft) => {
      draft.balance = this.value.balance.subtract(count);
      draft.bet = this.value.bet.add(count);
    });
  }

  win(count: ChipCount): Stack {
    return this.evolve((draft) => {
      draft.balance = this.value.balance.add(count);
      draft.bet = new ChipCount(0);
    });
  }

  pay(): Stack {
    return this.evolve((draft) => {
      draft.bet = new ChipCount(0);
    });
  }

  validate(value: StackValue): ValidationResult {
    if (
      !(value.balance instanceof ChipCount) ||
      !(value.bet instanceof ChipCount)
    ) {
      return false;
    }
    return true;
  }
}

請注意,此函式庫將所有領域物件設計為不可變的,所以 evolve 會返回一個全新的值物件而不是自身。這讓領域物件的實體永遠符合領域不變量,因為實例化的過程會呼叫 validate 方法,只要您能成功建立該物件——不論是創建、持久還原、還是透過 evolve——您必須維護其領域不變性,否則,就應該拋出錯誤。

您可以使用 equals 方法來比對兩個值物件的值是否相等:

const chipCount = new ChipCount(100);
const updatedChipCount = chipCount.add(0);
expect(updatedChipCount).not.toBe(chipCount);
expect(updatedChipCount.equals(chipCount)).toBe(true);

實體/聚合根

現在,是時候來設計玩家物件了。玩家具有唯一識別符,應該被設計為一個實體而非值物件。在撲克遊戲中,玩家不僅應該維護內部狀態的領域不變量,需要加注、喊牌、等待外部事件進行行動而改變內部狀態,就像一個窗口般,因此我們將它設計為聚合根。

聚合根需要定義一個 ID:

class UUID extends EntityId<string> {
  // 必須實作 rawId getter
  get rawId(): string {
    return this.value;
  }

  // ... 實作內容 ...
}

type PlayerProps = {
  id: UUID;
  username: Username;
  stack: Stack;
  folded: boolean;
};
class Player extends AggregateRoot<PlayerProps, UUID> {
  // 必須實作 id getter
  get id(): UUID {
    return this.props.id;
  }

  fold() {
    return this.evolve((draft) => {
      draft.folded = true;
    });
  }

  validate(props: PlayerProps): ValidationResult {
    // ... 實作內容 ...
  }
}

透過 props 取得實體/聚合根的屬性,而 equals 只比對 id 是否相同,並不會深入比較屬性。

const playerProps = {
  id: new UUID(randomUUID()),
  username: new UserName('john_lennon'),
  stack: new Stack({
    balance: new ChipCount(100),
    bet: new ChipCount(0),
  }),
  folded: false,
};
const player = new Player(playerProps);

const updatedPlayer = player.fold();

expect(player).toEqual(playerAProps);
expect(player.equals(updatedPlayer)).toBe(true);

領域事件

遊戲中,玩家的行為必須被其他玩家接收到以利進行接下來的行動,所以我們定義領域事件:

提示: 為了讓領域事件能輕鬆地在服務之間傳遞,例如訊息佇列系統,其屬性應該保持可序列化的。所以我們不直接傳入一個值物件作為聚合根 ID ,而是傳入單純的字串。

class PlayerFolded extends DomainEvent {}
class PlayerChecked extends DomainEvent {}

type PlayerRaisedPayload = {
  amount: number;
};
class PlayerRaised extends DomainEvent<PlayerRaisedPayload> {
  constructor(aggregateId: string, amount: number) {
    super({
      aggregateId,
      amount,
    });
  }
}

接著,在玩家行為中加入它們:

class Player extends AggregateRoot<PlayerProps, UUID> {
  // ... 實作內容 ...

  fold() {
    const event = new PlayerFolded(this.id.rawId);
    return this.evolve((draft) => {
      draft.folded = true;
    }).addEvent(event);
  }

  check() {
    const event = new PlayerChecked(this.id.rawId);
    return this.addEvent(event);
  }

  raise(count: ChipCount) {
    const event = new PlayerRaised(this.id.rawId);
    return this.evolve((draft) => {
      draft.stack = this.props.stack.raise(count);
    }).addEvent(event);
  }

  // ... 實作內容 ...
}

當領域事件伴隨聚合根行為被創建,在「確實發生」時——例如成功持久化——聚合根便可以發布收集在自身的領域事件,進入事件系統:

const playerProps = {
  // ... 實作屬性 ...
};
let player = new Player(playerProps);
player = player.fold();
player.events; // [PlayerFolded]

// 持久化狀態
await playerRepository.save(player);
// 發布領域事件
player.publishEvents(eventEmitter);

toJSON 方法

不論是資料持久化、遠端呼叫或是回應客戶端,都會需要將領域物件轉換成可序列化格式,toJSON 遞迴地將值/屬性轉換成一個 JSON 物件:

提示: 您應該盡量避免將領域模型直接洩漏到應用外部,而是轉換成數據傳述對象(DTO)或是視圖模型(View Model)。

const playerProps = {
  id: new UUID('5d56961c-7794-47f5-9332-d96997351069'),
  username: new UserName('john_lennon'),
  stack: new Stack({
    balance: new ChipCount(100),
    bet: new ChipCount(0),
  }),
  folded: false,
};
const player = new Player(playerProps);
const expectedJSON = {
  id: '5d56961c-7794-47f5-9332-d96997351069',
  username: 'john_lennon',
  stack: {
    balance: 100,
    bet: 0,
  },
  folded: false,
};

expect(player.toJSON()).toEqual(expectedJSON);