diff --git a/transfer-money/app/service/account_api.go b/transfer-money/app/service/account_api.go new file mode 100644 index 0000000..4decd71 --- /dev/null +++ b/transfer-money/app/service/account_api.go @@ -0,0 +1,21 @@ +package service + +import ( + "github.com/agiledragon/ddd-dci-sample-in-golang/transfer-money/domain/service" +) + +func CreateAccount(accountId string, amount uint, phoneNumber string) { + service.GetAccountService().Create(accountId, amount, phoneNumber) +} + +func TransferMoney(srcAccountId, dstAccountId string, amount uint) { + service.GetAccountService().TransferMoney(srcAccountId, dstAccountId, amount) +} + +func GetAmount(accountId string) (uint, error) { + return service.GetAccountService().GetAmount(accountId) +} + +func DestroyAccount(accountId string) { + service.GetAccountService().Destroy(accountId) +} diff --git a/transfer-money/domain/model/account.go b/transfer-money/domain/model/account.go new file mode 100644 index 0000000..62a5b14 --- /dev/null +++ b/transfer-money/domain/model/account.go @@ -0,0 +1,22 @@ +package model + +import "github.com/agiledragon/ddd-dci-sample-in-golang/transfer-money/domain/model/base" + +type Account struct { + base.AggregateRoot + balance *balance + phone *phone + MoneySource *moneySource + MoneyDestination *moneyDestination +} + +func newAccount(accountId string, balance *balance, phone *phone) *Account { + account := &Account{ + AggregateRoot: base.NewAggregateRoot(accountId), + balance: balance, + phone: phone, + } + account.MoneySource = newMoneySource(accountId, balance, phone) + account.MoneyDestination = newMoneyDestination(accountId, balance, phone) + return account +} diff --git a/transfer-money/domain/model/account_factory.go b/transfer-money/domain/model/account_factory.go new file mode 100644 index 0000000..9b88e53 --- /dev/null +++ b/transfer-money/domain/model/account_factory.go @@ -0,0 +1,10 @@ +package model + +type AccountFactory struct { +} + +func (this AccountFactory) Create(accountId string, amount uint, phoneNumber string) *Account { + b := newBalance(amount) + p := newPhone(accountId, phoneNumber) + return newAccount(accountId, b, p) +} diff --git a/transfer-money/domain/model/account_repo.go b/transfer-money/domain/model/account_repo.go new file mode 100644 index 0000000..a0af1de --- /dev/null +++ b/transfer-money/domain/model/account_repo.go @@ -0,0 +1,17 @@ +package model + +var repoInst AccountRepo + +type AccountRepo interface { + Add(account *Account) + Get(accountId string) *Account + Remove(accountId string) +} + +func SetAccountRepo(repo AccountRepo) { + repoInst = repo +} + +func GetAccountRepo() AccountRepo { + return repoInst +} diff --git a/transfer-money/domain/model/balance.go b/transfer-money/domain/model/balance.go new file mode 100644 index 0000000..24a39d4 --- /dev/null +++ b/transfer-money/domain/model/balance.go @@ -0,0 +1,24 @@ +package model + +import "github.com/agiledragon/ddd-dci-sample-in-golang/transfer-money/domain/model/base" + +type balance struct { + base.ValueObject + amount uint +} + +func newBalance(amount uint) *balance { + return &balance{amount: amount} +} + +func (this *balance) increase(amount uint) { + this.amount += amount +} + +func (this *balance) decrease(amount uint) { + this.amount -= amount +} + +func (this *balance) get() uint { + return this.amount +} diff --git a/transfer-money/domain/model/base/aggregate_root.go b/transfer-money/domain/model/base/aggregate_root.go new file mode 100644 index 0000000..28cf145 --- /dev/null +++ b/transfer-money/domain/model/base/aggregate_root.go @@ -0,0 +1,9 @@ +package base + +type AggregateRoot struct { + Entity +} + +func NewAggregateRoot(id string) AggregateRoot { + return AggregateRoot{NewEntity(id)} +} diff --git a/transfer-money/domain/model/base/entity.go b/transfer-money/domain/model/base/entity.go new file mode 100644 index 0000000..ca699f9 --- /dev/null +++ b/transfer-money/domain/model/base/entity.go @@ -0,0 +1,21 @@ +package base + +type Entity struct { + id string +} + +func NewEntity(id string) Entity { + return Entity{id: id} +} + +func (t *Entity) Id() string { + return t.id +} + +func (t *Entity) Equal(other *Entity) bool { + return t.id == other.id +} + +func (t *Entity) NotEqual(other *Entity) bool { + return !t.Equal(other) +} diff --git a/transfer-money/domain/model/base/role.go b/transfer-money/domain/model/base/role.go new file mode 100644 index 0000000..11408a8 --- /dev/null +++ b/transfer-money/domain/model/base/role.go @@ -0,0 +1,4 @@ +package base + +type Role struct { +} diff --git a/transfer-money/domain/model/base/value_object.go b/transfer-money/domain/model/base/value_object.go new file mode 100644 index 0000000..f7fbcb4 --- /dev/null +++ b/transfer-money/domain/model/base/value_object.go @@ -0,0 +1,4 @@ +package base + +type ValueObject struct { +} diff --git a/transfer-money/domain/model/money_destination.go b/transfer-money/domain/model/money_destination.go new file mode 100644 index 0000000..aff77a0 --- /dev/null +++ b/transfer-money/domain/model/money_destination.go @@ -0,0 +1,25 @@ +package model + +import ( + "github.com/agiledragon/ddd-dci-sample-in-golang/transfer-money/domain/model/base" +) + +type moneyDestination struct { + base.Role + accountId string + balance *balance + phone *phone +} + +func newMoneyDestination(accountId string, balance *balance, phone *phone) *moneyDestination { + return &moneyDestination{accountId: accountId, balance: balance, phone: phone} +} + +func (this *moneyDestination) getAccountId() string { + return this.accountId +} + +func (this *moneyDestination) transferMoneyFrom(srcAccountId string, amount uint) { + this.balance.increase(amount) + this.phone.sendTransferFromMsg(srcAccountId, amount) +} diff --git a/transfer-money/domain/model/money_source.go b/transfer-money/domain/model/money_source.go new file mode 100644 index 0000000..2630a4d --- /dev/null +++ b/transfer-money/domain/model/money_source.go @@ -0,0 +1,29 @@ +package model + +import ( + "github.com/agiledragon/ddd-dci-sample-in-golang/transfer-money/domain/model/base" +) + +type moneySource struct { + base.Role + accountId string + balance *balance + phone *phone +} + +func newMoneySource(accountId string, balance *balance, phone *phone) *moneySource { + return &moneySource{accountId: accountId, balance: balance, phone: phone} +} + +func (this *moneySource) TransferMoneyTo(dst *moneyDestination, amount uint) { + if this.balance.get() < amount { + panic("insufficient money!") + } + dst.transferMoneyFrom(this.accountId, amount) + this.balance.decrease(amount) + this.phone.sendTransferToMsg(dst.getAccountId(), amount) +} + +func (this *moneySource) GetAmount() uint { + return this.balance.get() +} diff --git a/transfer-money/domain/model/phone.go b/transfer-money/domain/model/phone.go new file mode 100644 index 0000000..93e329c --- /dev/null +++ b/transfer-money/domain/model/phone.go @@ -0,0 +1,26 @@ +package model + +import ( + "fmt" + "github.com/agiledragon/ddd-dci-sample-in-golang/transfer-money/domain/model/base" +) + +type phone struct { + base.ValueObject + accountId string + phoneNumber string +} + +func newPhone(accountId, phoneNumber string) *phone { + return &phone{accountId: accountId, phoneNumber: phoneNumber} +} + +func (this *phone) sendTransferToMsg(dstAccountId string, amount uint) { + msg := fmt.Sprintf("%s send %d money to %s", this.accountId, amount, dstAccountId) + GetPhoneProvider().Send(this.phoneNumber, msg) +} + +func (this *phone) sendTransferFromMsg(srcAccountId string, amount uint) { + msg := fmt.Sprintf("%s receive %d money from %s", this.accountId, amount, srcAccountId) + GetPhoneProvider().Send(this.phoneNumber, msg) +} diff --git a/transfer-money/domain/model/phone_privder.go b/transfer-money/domain/model/phone_privder.go new file mode 100644 index 0000000..064f928 --- /dev/null +++ b/transfer-money/domain/model/phone_privder.go @@ -0,0 +1,15 @@ +package model + +type PhoneProvider interface { + Send(phoneNumber, msg string) +} + +var providerInst PhoneProvider + +func SetPhoneProvider(provider PhoneProvider) { + providerInst = provider +} + +func GetPhoneProvider() PhoneProvider { + return providerInst +} diff --git a/transfer-money/domain/service/account_service.go b/transfer-money/domain/service/account_service.go new file mode 100644 index 0000000..053fd0c --- /dev/null +++ b/transfer-money/domain/service/account_service.go @@ -0,0 +1,44 @@ +package service + +import ( + "errors" + "github.com/agiledragon/ddd-dci-sample-in-golang/transfer-money/domain/model" + "sync" +) + +type AccountService struct { + repo model.AccountRepo +} + +func (this *AccountService) Create(accountId string, amount uint, phoneNumber string) { + account := model.AccountFactory{}.Create(accountId, amount, phoneNumber) + this.repo.Add(account) +} + +func (this *AccountService) TransferMoney(srcAccountId, dstAccountId string, amount uint) { + from := this.repo.Get(srcAccountId) + to := this.repo.Get(dstAccountId) + from.MoneySource.TransferMoneyTo(to.MoneyDestination, amount) +} + +func (this *AccountService) GetAmount(accountId string) (uint, error) { + if account := this.repo.Get(accountId); account != nil { + return account.MoneySource.GetAmount(), nil + } + return 0, errors.New("accountId is not existed") + +} + +func (this *AccountService) Destroy(accountId string) { + this.repo.Remove(accountId) +} + +var once sync.Once +var serviceInst *AccountService = &AccountService{} + +func GetAccountService() *AccountService { + once.Do(func() { + serviceInst.repo = model.GetAccountRepo() + }) + return serviceInst +} diff --git a/transfer-money/infra/account_repo_impl.go b/transfer-money/infra/account_repo_impl.go new file mode 100644 index 0000000..f1e8f52 --- /dev/null +++ b/transfer-money/infra/account_repo_impl.go @@ -0,0 +1,18 @@ +package infra + +import "github.com/agiledragon/ddd-dci-sample-in-golang/transfer-money/domain/model" + +type AccountRepoImpl struct { +} + +func (this *AccountRepoImpl) Add(account *model.Account) { + +} + +func (this *AccountRepoImpl) Get(accountId string) *model.Account { + return nil +} + +func (this *AccountRepoImpl) Remove(accountId string) { + +} diff --git a/transfer-money/infra/phone_provider_impl.go b/transfer-money/infra/phone_provider_impl.go new file mode 100644 index 0000000..3746d45 --- /dev/null +++ b/transfer-money/infra/phone_provider_impl.go @@ -0,0 +1,8 @@ +package infra + +type PhoneProviderImpl struct { +} + +func (this PhoneProviderImpl) Send(phoneNumber, msg string) { + +} diff --git a/transfer-money/test/account_test.go b/transfer-money/test/account_test.go new file mode 100644 index 0000000..c151080 --- /dev/null +++ b/transfer-money/test/account_test.go @@ -0,0 +1,90 @@ +package test + +import ( + "fmt" + "github.com/agiledragon/ddd-dci-sample-in-golang/transfer-money/app/service" + "github.com/agiledragon/ddd-dci-sample-in-golang/transfer-money/domain/model" + . "github.com/smartystreets/goconvey/convey" + "testing" +) + +type SpyPhoneProvider struct { + phoneNumbers []string + msgs []string +} + +func (this *SpyPhoneProvider) Send(phoneNumber, msg string) { + this.phoneNumbers = append(this.phoneNumbers, phoneNumber) + this.msgs = append(this.msgs, msg) +} + +type FakeAccountRepo struct { + accounts map[string]*model.Account +} + +func (a *FakeAccountRepo) Add(account *model.Account) { + a.accounts[account.Id()] = account +} + +func (a *FakeAccountRepo) Get(accountId string) *model.Account { + return a.accounts[accountId] +} + +func (a *FakeAccountRepo) Remove(accountId string) { + delete(a.accounts, accountId) +} + +func TestAccount(t *testing.T) { + provider := &SpyPhoneProvider{ + phoneNumbers: make([]string, 0), + msgs: make([]string, 0), + } + model.SetPhoneProvider(provider) + repo := &FakeAccountRepo{make(map[string]*model.Account)} + model.SetAccountRepo(repo) + + Convey("TestAccount", t, func() { + + Convey("create account", func() { + accountId := "zhangsan123" + initialAmount := uint(1000) + phoneNumber := "13310998098" + service.CreateAccount(accountId, initialAmount, phoneNumber) + actualAmount, err := service.GetAmount(accountId) + So(err, ShouldBeNil) + So(actualAmount, ShouldEqual, initialAmount) + service.DestroyAccount(accountId) + }) + + Convey("transfer money", func() { + srcAccountId := "zhangsan123" + srcInitialAmount := uint(1000) + srcPhoneNumber := "13310998098" + service.CreateAccount(srcAccountId, srcInitialAmount, srcPhoneNumber) + + dstAccountId := "lisi456" + dstInitialAmount := uint(200) + dstPhoneNumber := "19916588070" + service.CreateAccount(dstAccountId, dstInitialAmount, dstPhoneNumber) + + amount := uint(300) + service.TransferMoney(srcAccountId, dstAccountId, amount) + srcActualAmount, err := service.GetAmount(srcAccountId) + So(err, ShouldBeNil) + So(srcActualAmount, ShouldEqual, srcInitialAmount-amount) + dstActualAmount, err := service.GetAmount(dstAccountId) + So(err, ShouldBeNil) + So(dstActualAmount, ShouldEqual, dstInitialAmount+amount) + + srcMsg := fmt.Sprintf("%s send %d money to %s", srcAccountId, amount, dstAccountId) + dstMsg := fmt.Sprintf("%s receive %d money from %s", dstAccountId, amount, srcAccountId) + So(provider.phoneNumbers[0], ShouldEqual, dstPhoneNumber) + So(provider.msgs[0], ShouldEqual, dstMsg) + So(provider.phoneNumbers[1], ShouldEqual, srcPhoneNumber) + So(provider.msgs[1], ShouldEqual, srcMsg) + + service.DestroyAccount(srcAccountId) + service.DestroyAccount(dstAccountId) + }) + }) +}