送金メソッドを考える

これは例だが、口座Aから口座Bへお金を送金するといった処理をどのように書くかで悩んでいる。
送金だけでなく、ユーザーAがユーザーBをフォローするといった操作でも同じように困る。

何に困るかと言うと、引数の取り違えが起こりそうだからなんとかしたいなと思っている。

前提

今回の話で前提としているのは3層+ドメインモデルのアーキテクチャで以下のリポジトリを参考にしているので参照してみてほしい。

github.com

application層に送金メソッドを作って、ControllerやBatchなどから送金を依頼されるというユースケースを考えている。
愚直に考えるとこんな感じだろうか。いろいろ省略して書いているのであしからず。

@PostMapping("transfer")
public String transfer(@RequestParam long amount, @RequestParam long toAccountId, User user) {
  BankAccount fromBankAccount = bankService.findByUser(user);
  BankAccount toBankAccount = bankService.findById(Id.of(toAccountId)):
  TransferAmount amount = new TransferAmount(amount);

  // ここでfromBankAccountとtoBankAccountの順番を間違えてもコンパイルエラーにならない。間違えやすい!
  bankService.transfer(fromBankAccount, toBankAccount, amount);
  return "...";
}

ましな案を考える

FromBankAccountとToBankAccountの型を分ける

@PostMapping("transfer")
public String transfer(@RequestParam long amount, @RequestParam long toAccountId, User user) {
  FromBankAccount fromBankAccount = new FromBankAccount(bankService.findByUser(user));
  ToBankAccount toBankAccount = new ToBankAccount(bankService.findById(Id.of(toAccountId))):
  TransferAmount amount = new TransferAmount(amount);

  // 型で意図が分かりやすいので間違えにくくなる…?
  // FromBankAccount作るときに間違えるかもしれないけどまだマシかも。ラベル引数できればよかったのにね。
  bankService.transfer(fromBankAccount, toBankAccount, amount);
  return "...";
}

ちょっと面倒だけど、型を作ってラベルとしての役割をもたせてみる。
順番でやるよりはコードを見たときに「あれ?これミスってね?」と気付けるチャンスが増えているのでまぁマシ。

TransferRequestを渡す

TransferRequestを作って送金依頼をするパターン。よさ。
よりラベル感があって間違えにくいかも。

@PostMapping("transfer")
public String transfer(@RequestParam long amount, @RequestParam long toAccountId, User user) {
  BankAccount fromBankAccount = bankService.findByUser(user);
  BankAccount toBankAccount = bankService.findById(Id.of(toAccountId)):
  TransferAmount amount = new TransferAmount(amount);

  bankService.transfer(TransferRequest.builder().from(fromBankAccount).to(toBankAccount).amount(amount).build());
  return "...";
}

BankAccountにtransferメソッドを生やす

EntityやValueObjectができる操作は限られていて、主にできることは計算/判断/変換でDBを叩いたりWebAPIを叩くことはできない。(ActiveRecordみたいに何でもやるマンにすると仕事し過ぎかなってきがする。
とはいえそう呼び出したい気持ちがあるので、TransferRequestを作るメソッドを生やす案を考えてみた。

public class BankAccount {
  public TransferRequest transfer(BankAccount toBankAccount, TransferAmount amount) {
    return new TransferRequest(this, toBankAccount, amount);
  }
}

Controllerでの呼び出しはこんなかんじだろうか。

@PostMapping("transfer")
public String transfer(@RequestParam long amount, @RequestParam long toAccountId, User user) {
  BankAccount fromBankAccount = bankService.findByUser(user);
  BankAccount toBankAccount = bankService.findById(Id.of(toAccountId)):
  TransferAmount amount = new TransferAmount(amount);

  // 送金依頼を作って送金する
  TransferRequest request = fromBankAccount.transferTo(toBankAccount, amount);
  bankService.transfer(request);
  return "...";
}

なんかアリな気がしないでもない。筋は良さそう?

引数を非対称にする

送金元はEntity、送金先はIdオブジェクトとすることでミス防ごうという案。

@PostMapping("transfer")
public String transfer(@RequestParam long amount, @RequestParam long toAccountId, User user) {
  BankAccount fromBankAccount = bankService.findByUser(user);
  Id<BankAccount> toBankAccountId = Id.of(toAccountId):
  TransferAmount amount = new TransferAmount(amount);

  // 送金依頼を作って送金する
  bankService.transfer(fromBankAccount, toBankAccountId, amount);
  return "...";
}

これは面白いかもしれない。クラスを作るのが面倒なときとかの妥協案として選ぶのはアリ寄りのアリかもしれない。

結局

どれがいいんでしょうね?BankAccount#transferのパターンは結構筋良さそうな気がする。直感だけど。