はじめに
自販機っぽいものをクリーンアーキテクチャを意識して作ってみました。
使い方
コードの説明から入るよりまず、使い方の説明をしたいと思います。
自販機インスタンスの生成
const machineUseCase = container.get<IMachineUseCase>(TUseCase.machineUseCase);
let machine = machineUseCase.initFromDB(); // 現在のdbの状態から自販機インスタンスを生成
machineインスタンスは以下のような内容
Machine {
inlets: // 飲み物を補充する投入口
[ { id: 1,
type: 'drink',
itemName: 'コーラ',
price: 120,
temperatureType: 'cold',
maxStockNumber: 10, // 投入口に入れれるドリンクの最大数
stock: [] }, // ドリンクモデルの配列
{ id: 2,
type: 'drink',
itemName: 'コンポタ',
price: 130,
temperatureType: 'hot',
maxStockNumber: 10,
stock: [] } ],
sales: { '10': 10, '50': 10, '100': 1, '500': 0, '1000': 0 }, // 売り上げ金
paidAmount: Money { '10': 0, '50': 0, '100': 0, '500': 0, '1000': 0 }, // 現在投入されている額
change: Money { '10': 0, '50': 0, '100': 0, '500': 0, '1000': 0 } } // お釣り
投入口1にアイテムを補充
machineUseCase.storedItem({ inletId: 1 }); // dbからアイテムを取ってきて補充する
この時点でのmachineインスタンスは以下
Machine {
inlets:
[ { id: 1,
type: 'drink',
itemName: 'コーラ',
price: 120,
temperatureType: 'cold',
maxStockNumber: 10,
stock: [ { name: 'コーラ', type: 'drink', temperatureType: 'cold' } ] }, // アイテムが補充されてる
{ id: 2,
type: 'drink',
itemName: 'コンポタ',
price: 130,
temperatureType: 'hot',
maxStockNumber: 10,
stock: [] } ],
sales: { '10': 10, '50': 10, '100': 1, '500': 0, '1000': 0 },
paidAmount: Money { '10': 0, '50': 0, '100': 0, '500': 0, '1000': 0 },
change: Money { '10': 0, '50': 0, '100': 0, '500': 0, '1000': 0 } }
お金を入れる
machineUseCase.pay({
100: 1,
10: 10
});
この時点でのmachineインスタンスは以下
Machine {
inlets:
[ { id: 1,
type: 'drink',
itemName: 'コーラ',
price: 120,
temperatureType: 'cold',
maxStockNumber: 10,
stock: [ { name: 'コーラ', type: 'drink', temperatureType: 'cold' } ] },
{ id: 2,
type: 'drink',
itemName: 'コンポタ',
price: 130,
temperatureType: 'hot',
maxStockNumber: 10,
stock: [] } ],
sales: { '10': 10, '50': 10, '100': 1, '500': 0, '1000': 0 },
paidAmount: { '10': 10, '50': 0, '100': 1, '500': 0, '1000': 0 }, // ユーザーが入れたお金が入ってる
change: Money { '10': 0, '50': 0, '100': 0, '500': 0, '1000': 0 } }
投入口1のアイテム購入
machineUseCase.buyingItem({ inletId: 1 }); // 購入されたアイテムはstockから減らして売上、お釣りを計上する
この時点でのmachineインスタンスは以下
Machine {
inlets:
[ { id: 1,
type: 'drink',
itemName: 'コーラ',
price: 120,
temperatureType: 'cold',
maxStockNumber: 10,
stock: [] }, // アイテムが一つ減っている
{ id: 2,
type: 'drink',
itemName: 'コンポタ',
price: 130,
temperatureType: 'hot',
maxStockNumber: 10,
stock: [] } ],
sales: { '10': 17, '50': 9, '100': 2, '500': 0, '1000': 0 }, // 120円売り上げが加算されてる
paidAmount: { '10': 0, '50': 0, '100': 0, '500': 0, '1000': 0 },
change: Money { '10': 3, '50': 1, '100': 0, '500': 0, '1000': 0 } } // お釣りが出ている
利用方法のイメージは以上です。
ディレクトリ構成
src
├── domain
│ └── model
│ ├── change
│ │ ├── index.ts
│ │ └── service.ts
│ ├── inlet
│ │ ├── index.ts
│ │ └── service.ts
│ ├── item
│ │ └── index.ts
│ ├── machine
│ │ ├── index.ts
│ │ └── service.ts
│ ├── money
│ │ ├── index.ts
│ │ └── service.ts
│ ├── sales
│ │ ├── index.ts
│ │ └── service.ts
│ └── service.ts
├── repository
│ ├── inlet
│ │ ├── index.ts
│ │ ├── interface.ts
│ │ └── type.ts
│ ├── item
│ │ ├── index.ts
│ │ ├── interface.ts
│ │ └── type.ts
│ ├── machine
│ │ ├── index.ts
│ │ ├── interface.ts
│ │ └── type.ts
│ └── sales
│ ├── index.ts
│ ├── interface.ts
│ └── type.ts
├── usecase
│ └── machine
│ ├── index.ts
│ ├── interface.ts
│ └── type.ts
│
├── index.ts
├── inversify.config.ts
└── inversify.decorator.ts
コード説明
ざっくり説明するとdomain配下はモデル定義とドメインロジックが入っています。
repositoryはデータの永続化を担うところで、dbに対するcrud的なメソッドを持ってます。
usecaseはドメインとリポジトリの処理を組み合わせて一つの処理にまとめあげているイメージです。
なので依存関係的には
usecase => domain
usecase => repository
となってます。
ここで1点問題があって、クリーンアーキテクチャには依存関係は外側から内側への一方向のみ許す、というものがあります。
repositoryはusecaseよりも外側に位置しているのでusecase => repositoryの依存は内側から外側に依存することになり、クリーンアーキテクチャ的にはあまりよろしくないコードになるみたいです。
そのため、inversifyJSというものを使って依存性の逆転をしています。
以下のような感じでusecaseにrepositoryをinjectしています。
usecaseはリポジトリの抽象に依存している状態になっているので、その抽象を違う形(別のdb使うとか)で実装した物に置き換えることも容易になります。
この状態を依存性が逆転している状態と呼ぶ?ようです。
@injectable()
export default class MachineUseCase implements IMachineUseCase {
@lazyInject(TInletRepository.inletRepository) private _inletRepository: IInletRepository;
@lazyInject(TItemRepository.itemRepository) private _itemRepository: IItemRepository;
@lazyInject(TSalesRepository.salesRepository) private _salesRepository: ISalesRepository;
また、リポジトリを抽象化するのにtypescriptのinterface機能を使っています。jsにはinterfaceがないため、jsでクリーンアーキテクチャをやりたい場合、自然とtsになってくるのではと思います。
クリーンアーキテクチャでは特定のUIや特定のdbに依存しないという特性があります。
今回作成した自販機アプリも、自販機インスタンスの状態を表現するUIを作りさえすればreactでもvueでも動くような気がしてます。(まだ私自身作っていない)
今回作成したコードはここにあります。
https://github.com/pokotyan/vending-machine-ddd