やる気がストロングZERO

やる気のストロングスタイル

DDDのメリットを勘違いしてた件

僕はDDDで「俺が考える最強の契約管理システムを作る」というのをやってみている。
もう100回を超えた。

www.youtube.com

目的としては、外部からあれこれ言われず自分が思う通りに実装を進めて、自分だけの責任で躓いて、自分で「良い・悪い」を体験する事である。 アーキテクチャとしてはDDDを意識している。

着手当初思っていたこと =>すぐに実装速度があがってくる

DDDで実装すると、最初は手数が多い分実装スピード遅いが、次第に実装スピードが上がっていき「Railsのドキュメントに愚直に沿った開発」よりも早く完成させることができると思っていた。

なぜなら、DDDで実装すると凝集性が高まりコードの再利用がしやすくなるので、次第に書くコード量が減っていき実装速度が上がってくるはず、と思っていたからだ。

着手中感じたこと => 全然実装速度があがってこない

「あれ?ぜんぜん実装速度があがってこない。」

新しい機能を載せようとするたびに、アプリケーションサービス、ドメインサービス、リポジトリ、エンティティ、データマッパー、DTOを定義してそれぞれに責務を守らせたメソッドを定義していかないと行けない。

どこに何をもたせるか?を考えたり「DB => データマッパー => エンティティ(or 集約) => DTO => view」のように似たような構成のデータ構造体にデータを受け渡していくのもかったるいと感じた。

たぶんRailsだったらActiveRecordモデルを定義して、処理メソッドを定義して、これをViewまで持ち回るようにすればもっと手数が少なくて実装できるだろうなと感じた。

これじゃあ「DDDで〜とか息巻いて言ってるけど実装速度遅いし、単なる意識高い系じゃん」とか現場で言われそうで不安になった。

新規開発のステージではDDDはコスト感を感じやすい

twitterでこんな感じでつぶやいてた。

開発に費やしている時間は、その後保守を含めたライフサイクルのなかでほんの少しの時間でしかない。
ここで少しばかり時間がかかったとしても、その後の保守が楽になるのであれば投資としては成功と言える。

DDDでの初期開発中はメリットよりもデメリットを感じやすい期間なのだと感じた。

何に実装時間(コスト)がかかっているか? => レイヤー分割

Railsのドキュメントに愚直に沿った開発と比べて、DDDでの開発で時間を食っている主な部分は「レイヤー分割のために発生する作業」である。

レイヤー分割するにはある程度の実装コストを払わなければならない。

レイヤー分割の大切さとそれにかかるコストについては Clean Architectureでも述べられている。

※この書籍、2年ほど前に読んだときは内容が抽象的に思えてイマイチ内容を掴みきれなかったが、以下のドメイン駆動設計入門を読んでコードでの実装イメージを掴んでから再度読んでみると色々理解できた。

なんのためにレイヤー分割するのか? => コアドメインを依存から守るため

コストのかかるレイヤー分割をわざわざやるのは、コアドメインを依存から守るためである。

コアドメインで実装される処理は、Railsでなければ実現できないわけでも、CSV出力でしか実現できないわけでもない。

たとえば販売管理の処理はRailsが登場する前から存在しているし、出力は紙帳票や、web viewでも、エクセルで吐かせても、なんでもいいはず。

なのに「フレームワークRails以外に切り替えられない」や「出力はCSVでしか対応できない」みたいな販売管理システムはたぶん多く存在する。

これらはレイヤー分割しておらず、ActiveRecordのメソッドや、出力CSVの生成の処理がコアドメインの内部に食い込んできていてこれらを引き剥がせなくなってしまっているのが原因である。

レイヤー分割するメリットは?

不必要な依存を持ち込まないようにする

工数を余計にかけてまでレイヤー分割するメリットは、上記に記載したActiveRecordCSVへの依存のような、不必要な依存をコアドメインに持ち込まないように出来ることである。

こうすることで、コアドメインが長生きする。
時代とともにフレームワークや、データストアや、スマホアプリや、今後生まれるかもしれないウェアラブルバイスが、どのように移り変わろうとも、これらに依存していなければコアドメインを使い続けられる。

フレームワークが変わったらまるごと実装しなおさなければならない、というような事が起きない。

仕様変更にも追従しやすい

開発プロジェクトを進めていると、仕様変更が頻繁に起こる。

レイヤー分割をきれいに行っておくと、副次的だが単体処理の組み合わせで処理を実現する感じになりやすいため、仕様変更の影響範囲が小さくて済む。

レイヤー分割していなければ、DBからのデータ取得、処理、view用のデータ整形までひっくるめた巨大な処理になっていて「処理自体がDBの機能に依存したものになっていて入れ替えられない」など発生する。

レイヤー分割されたコードだと、少なくとも各レイヤーで処理分割されていて、たとえばデータの読み込み先がDBからcsvになったりしてもデータ取得部分だけ変更すれば良い感じになってる。

実装速度に差が出てくるのはもう少し先の話。

現在「俺が考える最強の契約管理システムを作る」は新規開発の段階で、まだコストを払っているステージだという認識になった。
ただし、試行錯誤しながら機能追加していっている状況で既存コードを修正して追加実装する作業で、もしかしたら既にDDDのメリットを享受し始めているかもしれない、、とも感じている。

というのも、今までのシステム開発にて既存コードに新機能を追加する際「素直に追加実装できない色んな問題(主に不要な依存が原因)がそこにあり、それらを回避してギリギリの形で実装した(初期予想工数の数倍かかった)」という経験をたくさんしてきているが、今の所そういった問題に発展していないので、これは既にメリットなのかもしれない。

娘が生まれたメモ

8/26(水)に娘が生まれたのでメモ

本当は9/4くらいが予定日だったのだが、奥さんが病院に定期検査にいったらそのまま入院することになった。
コロナの状況下なので僕は入院している病院に入ることができず、入院したら次あえるのは出産して退院するとき。
主にLineで状況のやり取りしてたけど、本人が陣痛等やなんか出産準備のあれこれをやってると連絡をすることができず、僕から見ると急に連絡が途絶える感じになって、まぁ大丈夫だとは思いながらも不安だった。

どうせ立会とかもできないので出産当日も仕事してた。

ちょいちょい連絡が途絶えつつ15時くらいからまた連絡が途絶えて、17:50くらいに「生まれましたー」の連絡と写真が来た。

写真を見た時、ホッとしたけどドラマとか漫画で見るような「うぉおー!!」というような感情は特に湧いてこなくて、奥さんも子供も無事だった事に安心した感じだった。

この時点では子供に対しては(直接見ていないからなのか)「これが俺の子かー、マジかー」という若干客観的な感じだった。

奥さんは結構出血していたらしくフラフラで、当日もほとんど寝てない感じだったので早めに眠ったようだったが、 「ちゃんと翌日起きてくるのか??」とまた不安になったりしてた。=> 翌日普通に起きてきた。

出産立会はもともと怖くて出来ないだろうなーと思ってたけど、出産前後のしんどいときには一緒にいて、なんか擦ったりなんだりしてやりたかったなと思ったりした。

翌日「病棟には入れない(子供には会えない)が嫁には会えるで」って聞いたので、午後休をとって会いに行ってきた。

電話で声を聞いた時、なんか凄い声が小さくて「やっぱだいぶしんどいんやな、、」と思ったけど実際に会うとそこそこいつもどおりに話せて安心した。動くのはしんどそうだった。

そのあたりからだったかどうだったか忘れたけど、娘の写真を見ると「それなりにかわいいかもな」と思い始めていた。

帰りに友達だなんだに「生まれました報告」をして(妊娠の事もほとんど言ってなかった)、後は奥さんから娘の写真を送ってもらいながら退院を待つ日々を送っている。

娘の写真はなるほど、暇があれば見てしまうな。

brew upgradeしたらrailsが起動しなくなったので対応したメモ

久々にbrew upgradeを実行したらopensslのなんかが読み込めなかっただかなんだか言われて、railsが起動しなくなった。

bundleもbrewも「ほしいライブラリを便利にインストールしてくれるツール」くらいの認識であまりどういう動きをしているか認識してなくて対応にちょっと苦労したのでメモ。
※あまり内部を意識せずに使えるツールなので、どういうものなのかすぐ忘れてしまう。。

作業ログ

  • brew gradeした。その後railsで使っているgemの一つが[opensslが読み込めない]とかいうエラーを出して起動できなくなった。
  • よくわかってないまま、bundle installを実行して(エラー無く終了)再度railsを起動しようとしたが同じ状況
  • 他にもググって出てくるコマンドを実行してミドルウェアインストールとかなんだかんだした。=>が解決せず。
  • gem,brew,bundlerについてちょっと調べた
  • brew upgradeでopensslのバイナリの場所が変わってしまったかしたのが原因?今のgemライブラリは依然古い方を見に行ってしまっているのかと推測。
  • opensslはmac上に使える状態で存在はしているので、再度gemライブラリをinstallし直せばなんか違いが起こる気がする。
  • bundle installを実行したが、たぶん既にライブラリが存在しているため再インストールは実行されていないと推測
  • vendor/bundle/ruby/2.x.xの2.x.xのディレクトリ名を変更して(存在しないことにして)再度bundle installを実行
  • pgのインストールでコケる。
  • エラーメッセージを見ると「/usr/local/Cellar/postgresql@9.4/9.4.11/bin/pg_configが無い」とか言ってる。見てみるとたしかに無い。
  • brew install postgresql@9.4実行してみる
  • 似たようなフォルダできたけど9.4.11ではない。/usr/local/Cellar/postgresql@9.4/9.4.26/bin/pg_configはある。
  • ていうか、存在してないような9.4.11のpg_configを使おうとしているあたり、どこかでそんな設定を自分でしていると予想
  • railsのroot/.bundle/configにpgに対して/usr/local/Cellar/postgresql@9.4/9.4.11/bin/pg_configを使うように設定がされているのを発見(たぶん入社時に言われるがままに設定した※覚えてない)
  • 設定を削除。brew コマンドでpostgresqlbrewpostgresql@9.4を使うように設定。brew link --force postgresql@9.4(しなくてもよかったかも。)
  • bundle installを実行。全部正常にインストールされる
  • railsが起動した

調べたこと

bundler

gemの依存ライブラリまでバージョンを固定管理したり、環境毎にgemライブラリを独立させたりできるツール。
このツールでインストールされるgemライブラリは、自分の環境では、railsのroot/vendor/bundle/ruby/x.x.x/以下にはいってた。

homebrew

macミドルウェアとかツールをインストールするのに便利なツール。
/usr/local/Cellar/以下にインストールしたプログラムのバイナリとか入ってる。
どうやってpathに入っているのかとかはあまり調べてない。
opensslはこっちで入れておく必要がある(gemライブラリはよくこれをつかうっぽい)

まとめ

仕事中に環境が壊れると焦って、エラーメッセージでググって出てきた「このコマンドをたたくとOK」みたいなやつを実行してしまいがち。 そして余計に環境が汚れていく(自分でなにをしているのかわかってない)
ゆっくり落ち着いて、扱っているものがどういうものなのか把握して対応していかないとだめだなと思った。

【DB設計】データ複製して持ちたくないと思ってる理由

僕はデータを複製して持ちたくないとずっと思っているけど、 色んな意見聞いてると自分でもそれが本当に良い事なのかわからなくなりそうだったので、 なぜ複製が嫌いなのか書きながら考えを整理してみた。

データを複製すると不整合が起きるから嫌だ

ECショップシステムにて
「商品マスターデータ一覧から商品を選んで[販売する]ボタンを押すと、ECショップに商品が表示されて販売開始される」
のような機能があった時、以下のようなテーブル設計を考えてみる。

データ的には 商品マスターから販売商品テーブルにデータコピーされてる。
販売終了時は販売商品テーブルから削除する

商品マスター

id 商品名 価格
1 A商品 1000
2 B商品 2000

販売商品

id 商品名 価格 商品マスターid
1 A商品 1000 1

A商品の値段が上がったので商品マスターの値段を変更した。
でもECショップに表示されている商品金額は変わらない。

商品マスター

id 商品名 価格
1 A商品 5000
2 B商品 2000

販売商品

id 商品名 価格 商品マスターid
1 A商品 1000 1

このタイミングでお客さんが1000円と表示されているA商品を購入した。
しかしなぜかお客さんには5000円で請求されてしまった。
処理コードを見てみると、請求では商品マスターテーブルの金額を参照していた。。

その後商品名が変わった(A商品 => A商品Ver2)のでECサイトで表示を変更するためデータを変更した。
この時、商品マスター側を変更しなければならないのに気が付かなかった。

商品マスター

id 商品名 価格
1 A商品 5000
2 B商品 2000

販売商品

id 商品名 価格 商品マスターid
1 A商品Ver2 1000 1

しばらくして一旦商品の販売を停止した。

id 商品名 価格
1 A商品 5000
2 B商品 2000

販売商品

id 商品名 価格 商品マスターid

その後再度商品販売を開始したら、商品名が古いものになってしまった

商品マスター

id 商品名 価格
1 A商品 5000
2 B商品 2000

販売商品

id 商品名 価格 商品マスターid
1 A商品 5000 1

今回の例だとシンプルなのですぐにデータの関連が理解できるけど、データコピーを繰り返しているとDB上に同じような意味に見えるデータが散在するので、どこでどのデータが使われているのかよくわからなくなっていく。

僕はデータを複製して持ちたくないので、以下のようにしたい。

商品マスター

id 商品名 価格
1 A商品 1000
2 B商品 2000

販売商品

id 商品マスターid
1 1

A商品の値段が変わったので、商品マスターの金額を変更した

商品マスター

id 商品名 価格
1 A商品 5000
2 B商品 2000

販売商品

id 商品マスターid
1 1

ショップ表示金額は商品マスターから取得されてるので5000円と表示される。
お客さんは5000円で商品を購入し、請求は5000円で行われる。

計算結果を持つのもデータ複製の一種と考えている

合計金額とか算出できる結果を持つのもデータ複製だと考えている。

計算元に変更があると、再計算しないといけない。

以下の例の請求テーブルの請求金額は、請求詳細テーブルの金額の合計となっている。

請求

id 顧客id 請求金額
1 1 300

請求詳細

id 請求id 商品id 金額
1 1 1 100
2 1 2 200

なんかの事情で[商品id: 2]の商品を請求しないことになったので、データを修正した。
でも請求金額の調整(もしくは請求金額計算処理の再実行)を忘れていたので結局300円で請求されてしまった。。

請求テーブル

id 顧客id 請求金額
1 1 300

請求詳細テーブル

id 請求id 商品id 金額
1 1 1 100

僕はデータ複製したくないので以下のようにしたいと思ってる。
請求合計金額はロジックが算出する。

請求テーブル

id 顧客id
1 1

請求詳細テーブル

id 請求id 商品id 金額
1 1 1 100
2 1 2 200

なんかの事情で[商品id: 2]の商品を請求しないことになったので、データを修正した。
合計金額は正しく100円で算出された。

請求テーブル

id 顧客id
1 1

請求詳細テーブル

id 請求id 商品id 金額
1 1 1 100

あるデータ、ある処理に変更を加えたときに「関連してこっちも変更しないといけない」というのをできるだけ意識したくない

上記の例でデータ変更を行った時に問題が発生したのは「関連してこっちも変更しないといけない」の対応が漏れていたから。
でもどこが関連しているのかは、ロジックを知っていないとわからない。
これを洗い出す作業は結構大変だと思ってる。

データ複製をしていなければ、変更の影響は即座に参照している部分に反映される。

算出による処理速度が問題になる場合はキャッシュを使う

こんな感じでデータ複製を持たず毎回算出させていると処理速度が問題になってくるケースはあると思う。

その場合は「計算結果をキャッシュとして持たせる」のはアリだと考えている。
ただしキャッシュは「データ複製」に当たるので不整合が発生するのでそのへん気にしないといけない。(このあたりがキャッシュが難しいといわれてる部分だと思う)

「結局データ複製をするならキャッシュでやろうが、テーブル内でやろうが同じなのでは?」と一瞬思ったけど違う。

キャッシュは消えても問題ないデータとして設計される。
だから不整合解消の方法が明確。
基本的にキャッシュ再生成を実行すれば整合性の取れたデータでキャッシュが生成されて不整合が解消される。

テーブル内にデータ複製を持つ設計はキャッシュとしては設計されていない場合が多い。
だから不整合解消の方法はケースバイケースで、単純に再実行すれば正しいデータが上書かれて整合性が取れる場合もあれば(これはキャッシュとして機能していると言えるのかもしれない)、新しいレコードが出来てしまってだめな場合もある。

また、その複製データがキャッシュなのかそうでないのかがわかりやすい形で実装されている必要がある。
キャッシュデータはRDBではなく、redisのようなインメモリDBとかに入れる、みたいに保存場所を分けておけばそれが明確になる。

複製して持つべきものもある

例えば、お客さんの購入履歴テーブルがある。
ここには商品名と商品金額が商品マスターから購入時点で複製されて入っている。

ある日、A商品を買った場合

商品マスター

id 商品名 価格
1 A商品 1000
2 B商品 2000

購入履歴

id 商品id 商品名 商品金額 購入日
1 1 A商品 1000 2020-1-1

その後、ECショップでA商品の金額が変わった。
購入履歴の金額は1000円のまま。
これは正しい挙動と言える。

商品マスター

id 商品名 価格
1 A商品 5000
2 B商品 2000

購入履歴

id 商品id 商品名 商品金額 購入日
1 1 A商品 1000 2020-1-1

複製してなかったら、商品マスターを見に行って
「1000円で購入したはずなのに5000円で購入したことになってた」 となってしまう。

この違いはなにか?
うまく説明できなかったのだけど「その時起こった事実を残さねばならない」という類のデータは、スナップショットとしてデータコピーして持つべき、という感じなんかなと思った。
ログに近い性質を持ったデータ。

スナップショットも導出できるならするべき?

「購入履歴の金額や商品名も、商品マスターにデータ変更履歴を持たせていれば導出できる。 そうであれば、これらもデータを保持するんじゃなくて導出すべきなのでは?」

と言われてたしかになと思った。

ただしこれはログ的な意味が強いと思っていて「あの時点で実際にどういう値で処理が行われたのか」という事実を記録しているもので、
ここを導出で導くのとデータコピーをして持っておくのと、どちらが信頼性が高いかと言えばデータコピーであると思うので、データ複製で持っておくべきかなと考えている。

導出されるようにしておけば、処理の再実行も可能になるので、この問題とは別問題としてあったほうがより良い気はする。

外部サービス連携はデータ複製してもっているのでは?

「外部サービス連携するときって、向こうはデータコピーを持ってる事になるよね?システム内でデータコピーしてても、それって内部でやってるか、外部でやってるかの違いでしかなくない?」 と言われてたしかに、と思った。

例えば商品マスタデータは社内に持っていて、外部のECサービスで商品販売をしてるとすると、商品マスタデータに変更があるたびにECサービスとデータ連携を実行しなければならない。 ECサービスにデータがコピーされて保持されていて自動的に連携されないから。

じゃあ内部で完結する場合に、上記のようにデータ連携処理実行をするような構成にしたほうがいいのかと考えるとそうではない気がする。 余計な処理をしなくても勝手に整合性がとれた状態になってほしい。 外部サービスはそう出来ないから「データ連携処理の実行」をやるしかない、という認識。

外部サービス側のデータコピーは「キャッシュ」に近い特性がある気がする。 境界線と整合性を取るための手段が比較的ハッキリしている。

内部でもってる複製データは、後々それだけみても複製されたデータなのかどうかさえロジックを追わないとよくわからない。

SSHポートフォワードメモ

ローカルポートフォワード

参考)

SSHポートフォワーディングを知った話 - Qiita

serverAからしかアクセス出来ないserverBにあるhttpコンテンツをlocalからみたい場合

localマシンにて、

ssh serverA -L 8080:serverB:80 -N

と実行してから、localマシンのブラウザでlocalhost:8080とアクセスするとserverBのhttpコンテンツを見れる(ハズ)。

まだ試せてないけど。
試した。

たしかにlocalhost:8080でserverBの80番へアクセスできたけど、バーチャルホストとか、アクセスドメインが必要っぽいサイトでは正しくコンテンツを閲覧できなかった。

これはdbとかミドルウェアへのアクセスに使う感じなら良さそうだが、webコンテンツをみたいなら以下のダイナミックポートフォワードのほうが良さそう。

ダイナミックポートフォワード

参考)

sshによるポートフォワーディングまとめ -- ぺけみさお

localマシンにて、

 ssh -D port serverA -N

と実行するとportをSOCKSプロキシとして使用でき、これを使った外部へのアクセスは、外部からみるとserverAからのアクセスに見える(らしい)。

まだ試せてないけど。
試した。

ssh serverA -D 8080 -N

google-chrome --proxy-server "socks5://127.0.0.1:8080" &

を実行してchromeでアクセスipを確認するサイトを見ると、serverAのipが表示された。

意思決定を議論で導くのはよくない

意思決定を多人数で行う議論でするのは良くないと考えるようになったので、その理由を書く。

議論で意思決定を行うと方針に一貫性がなくなる。
議論は意思決定のための材料集めとして使い、意思決定は責任者が独断で行うべき

議論は背景やテクニックが占める割合が大きく、正しい結論が導き出されるとは限らない

社長が言った発言と新卒が言った発言は同じ比重に扱われない。
社長の間違った意見を覆すには、丁寧に反論して納得してもらわなければならないが、新卒の間違った意見だと「勉強しておいて」等で軽く済まされたりする。

こんな感じでそもそも意見の扱いが不公平なので、どうしても偏った結論に流れやすい。

また、議論にはテクニックも存在していて間違った意見でも通すことが出来てしまう。

他にも、

  • 正しい意見は持っていても発言が苦手で議論に出せない
  • 会議時間の終了が迫ってきたので結論を急いだ

等...
様々な要因が原因で、議論で導かれる結論というのはあまり論理的なもので無いことが多い。

議論はランダム性が高いので全体としての一貫性を出すのが難しい

議論の場では毎回重要に捉えられる要素が異なる。

そもそも議論に呼ばれる人(メイン担当者以外)は普段からその議題について考えているわけではなく、席について初めて考え出す。
だから議論ごとに言っていることが変わったり、考え方も変わったりする。

そんな感じで導かれる結論もランダム性が高いので、一貫性を持たせて何かを進めるのには不向き。

議論で意思決定していると、成功したとき・失敗したときの原因が不明瞭になる

議論で導かれる結論はランダム性が高いので、成功・失敗したとき、何が原因だったのかがわかりづらい。
また、評価としても誰の功績なのかも不明瞭になる。

議論は意思決定のための情報集めとしては最適

議論で出てくる多様な意見は、意思決定の材料として無くてはならないもの。

意思決定は責任者の独断で決めていい

上記理由から、最終の意思決定は責任者の独断で決めるのが良いと思う。

「責任者」とは議題の問題に失敗したら給料が下がる人の事で、その問題に対して一番真剣に考えられる人であるはずだし、責任の無い人の意見よりも尊重されるべき。

もちろん責任者がその責任において「〇〇君(責任のないいち作業員)の意見を全面的に信頼して任せる」というのもアリ。