エンジニアだって怖いものは怖い
バックエンド系のエンジニアってDBに入れるデータがミスってたり必要な情報が保存されていなかったりすると取り返しつかないことになるし怖いじゃないですか。特にお金とか人の命が絡んでくると絶対にやりたくないと思うんですよね。エンジニアだって人間なので感情があります。
お金とか扱おうとすると過去にさかのぼってこの情報が追えないとだめだとか、特殊フローがいろいろあったり、整合性は絶対ミスれないなんて複雑さがあったりしてつらいんですよね。そういった複雑さとか泥臭さと戦うというのは嫌いではないんですが(うまく動いたりすると楽しいし設計考えるのは楽しい)つらい気持ちで頑張っても「これって動いて当然の仕組みだしこの頑張りは報われるのかな?」って気持ちになるんですよね。
技術的選択をするときに思い出すんですが、チームメンバーが生き生きとしていられないような選択肢はまずありえないと思うんよね。
売上のためにエンジニアが犠牲になって無限に障害対応しないといけないとか無限に複雑さを抱えるとか、そういったつらい状況になっていくとエンジニアは逃げていくし、エンジニアのメンタルリスクも一緒に考慮しないといけないなーと思う最近でした。
エンジニアがいないとプロダクトは作れないし、継続もできないのでやっぱりまずはメンバーの幸せは必須条件だなー。
サーバーサイドエンジニアの仕事について考える
最近フロントエンドエンジニアの人と「フロントエンドエンジニアってこういう仕事だよねー」という話をしたので、自分が普段やっているサーバーサイドエンジニアのしごとについて考えたくなった。
完全に主観だし、主語が大きいから「サーバーサイドエンジニアの仕事」ではなく「おれきゅーの仕事」と読み替えてもらったほうが正しいかもしれない。
整合性
フロントエンドの場合デザイナやPM、バックエンドエンジニアの話をいい感じに吸収して使い勝手の良いフロントを提案したりしながらいい感じにしていく人ってイメージで、それに対してサーバーサイドエンジニアはプロダクトの整合性について責任を持つ人ってイメージ。
DBのデータに変なデータが入ったりすると場合によってはプロダクトが完全に破滅するし、軽くてもいろんなユーザーに被害が出る。被害が出なくても将来的にそれが負債になって身動きが取れなくなったりする。そうならないように全力で考える人って考えてる。
整合性の問題はDBだけではなくて、たとえばアプリに対してのAPIのスキーマとかにも出てくる。
アプリから見た場合、APIのバージョンは1つだけかもしれないがサーバーから見ると複数のアプリのバージョンから叩かれることになる。なのでjsonを返している場合、リネームやプロパティの削除が互換性の問題で一切できないと考えたほうが良い。サーバーサイドエンジニアは将来に対しての覚悟とかそういう慎重さを持ったほうが良いのかなって思ってる。
意思決定
仕事をしていると実装案が複数あり、どれか一つを決めないといけないときが出てくる。というか毎回そうなると思うんだけど僕だけだろうか?
大体の場合サーバーサイドではAPIスキーマやDBとはずっと付き合っていくことになるので大体頭を抱えることになる。そういうときの一つの指標として「後戻りしやすい方を選ぶ」というのが良さそう。
例えば「カラムのnot null制約をかけるかどうか」という話であれば、「not null制約を掛けておいたほうがnullableに変更しやすい」という点で選びやすい。これはかなり簡単な例だけどこんな感じに困ったらどちらのほうが「後戻りしやすい?」という問いをすると良さそう。
結局サーバーサイドエンジニアの仕事って何
おれきゅーはこう思って仕事してる
- 将来に備えて決定をする/責任を持つ
- 正しいデータを守る人
送金メソッドを考える
これは例だが、口座Aから口座Bへお金を送金するといった処理をどのように書くかで悩んでいる。
送金だけでなく、ユーザーAがユーザーBをフォローするといった操作でも同じように困る。
何に困るかと言うと、引数の取り違えが起こりそうだからなんとかしたいなと思っている。
前提
今回の話で前提としているのは3層+ドメインモデルのアーキテクチャで以下のリポジトリを参考にしているので参照してみてほしい。
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のパターンは結構筋良さそうな気がする。直感だけど。
クラスを作ることをサボらない
JJUG CCC 2018 Fallのいくつかのセッションで型についての話があったので書きたくなった。
この話はJavaやScalaみたいな静的型の世界の思想で、多分Rubyとかでは違う思想だとおもう。
Javaを書いてるときのおれきゅーの脳内
型を定義して取りうる値を狭めることで 間違った使い方ができないようにする ことを最初に考えてる。多分。防御的プログラミングってやつなのかな?しらんけど。
とにかくエラーはより早いタイミングで見つかるほうが良くて、実行時よりコンパイル時にわかるほうが便利だし、コンパイル時よりエディタ上でリアルタイムに分かるほうがもっと良い。
たとえば汎用的な型を使うとこんなミスがあり得る。
long id = 10; new User(id); new Item(id);
静的型なら多分こうかけるほうが間違えなくて嬉しい。
Id<User> id = Id.of(10); new User(id); new Item(id); //コンパイルエラー
型に意図をもたせる
これはirofさんのセッションに出てきた話で、突然String str
という変数が出てきたとき、これが何なのか、どんな操作が許されているのかがわからない。Stringという型には文字列以上の情報が含まれていなくて、splitしたりsubstringできたりする。仮にこれがDisplayNameという型であれば、splitやsubstringなんて操作はないし、表示用に整形するだとかDisplayNameとしての操作だけが提供されて間違えることはない。
コンストラクタやfactoryメソッドで入力値をチェックすることで例えば空文字の場合はインスタンス化できないようにすると、DisplayNameの取りうる値の範囲が制限されてDisplayNameを扱うときに考えなければいけないことを減らすことができる。
Railsにはcomposed_ofというあまり使われていないマイナーな機能があってそれを好んで使っている。これはDBのカラムの値を値にマッピングする機能で、JPAでいうEmbeddedと同じような機能。Rails界隈であまり使われていない機能でも好んで使うのは取りうる値の制限ができるという理由から来ている。
宣伝ですが、composed_ofについては技術書典5で頒布した「pixiv PAYの薄い本」で書いてある。ちなみに表紙は僕が一番好きなイラストレーターのしらたま先生です。
Folioという証券会社のScalaの事例では、金額などの計算に型を使うことでうまく意図をもたせていた。
val yen = JPY(100) yen * 5 // JPY(500) yen * yen // JPY同士の掛け算は定義していない。そんな意味不明な計算はありえない。 yen / 5 // 割り算とかもありえないのでコンパイルエラー
これはわかりやすい例なのでそんなことでミスしないだろうと思いがちだが、ドメイン知識が複雑になると力を発揮する。
別の例で、損益と損益の引き算の例では以下のようなコードが示されていた。
/** 実現損益と含み損益をあわせて損益 */ case class PnL(unlialized: UnlializedPnl, realized: RealizedPnl) { /** 損益の差は損益ではなく損益の差 (例えば前日比)*/ def -(other: PnL): PnlDiff = PnlDiff(this.value - other.value) def value: JPY = unlialized.value + realized.value }
計算結果の型によって計算の意図が読みやすくなるし、型が明確に分かれることで取り違いがなくなったり、気をつけたり意識することの脳内リソースを開放できる。
クラスはどれだけ作れば良いか
別の概念のものなら息をするように作る細かく分ける。
名前考えるのは面倒だけど、作るのは一瞬。ずっと使うものだしどんどん作ろう。プリミティブは滅ぼそう。
とにかく最初はやりすぎかな?って思うくらいやればいい。分けるのは大変だけど統合は簡単。しらんけど。とりあえずやってから考えよ。
おれきゅーのコードを書くときの心構え
息をするようにクラスを作る。名前を決める。
クラスを作るのをサボらない。
参考
コードをどまんなかに据えた設計アプローチ - Speaker Deck
Scala と Microservices でつくる 証券会社とスタートアップ / FOLIO in JJUG CCC 2018 Fall - Speaker Deck