😺

ソフトウェア設計のマイブーム

に公開

まとまってない記事はもっと雑多な感じの別の媒体に書くことにしているのだが、そうは言っても考え方があり、それに基づいてgit repositoryを作るので、参照先になるドキュメントがあったほうがよく、それはやはり技術記事だろうというところで書く。

intro

新卒の時の上司に「君は野武士だね」と言われたのが割と印象に残っているのだけど、これは自分で理屈決めて進めちゃうところがあるので、ちゃんと本とか読んで一般的に流通している理論や方法論に習うこともしよう!という話だった。
それはその通りで、この記事はマインドセットの話ではないのでこれ以上言及しないが、反省してなるべく書籍を読んでいる。だが最近は書籍を読む時間がなく、ちょっと自分の理論が暴走気味な気がする。
とはいえ、個人の開発も会社の仕事も進んでいくわけで、ソフトウェア設計において、何を指針にしていくか、決めて置かないとスムーズじゃない。
今後アップデートされるとしても、とりあえずそういった自分の関わるプロジェクトから参照するドキュメントとして、ソフトウェア設計の方針や理論について、まとめて置きたい。

大枠の設計

大枠の設計とはなんなのか。新卒のときはSIだったので、要件定義->基本設計->詳細設計->コーディングだった。
いまは自社のweb開発なので、PdMがざっくりした機能を整理して、それに対して機能を詳細に定義して、詳細設計してコーディングという感じ。
個人で開発する際は、何がほしいのか?というのを自分で考えて、それを実現するために、どういう機能がいるか考え、それが既存のアプリケーションなどで代替できないか検討する。

requirement

いずれにしろ、まず最初は何がほしいのか?だろう。
ほしいイメージがなければ、何もできない。解決したい課題と言ってもいいかもしれない。
後述するが、これをrequirementと呼ぶことにしている。

概念には名前がついていたほうがいい。なので名前をつけるのだが、既にある概念を正しく認識できているかわからない。なので書籍を読めという話だが、時間がないので勝手に命名する。
既にある概念は、手垢がついてバズワードと化しているものも少なくない。そういったものは定義が正確ではないのでいずれにしても使いたくない。ドメイン駆動設計という単語にも同様の印象を抱いている。

requirementはとにかく、何がほしいか、ほしい人の言葉で書く。術後がほしいではなく、性質を示し始めたらそれは次のconsiderationの領域になるので辞める。

consideration

ドメイン駆動設計は、バズワードと化しているという印象だが、実際には書籍を読んでちゃんと理解して、向き合うべきだろう。
辞書的な意味でいうと「ユビキタス言語を用いてドメインエキスパートと会話しながら設計する」だろう。いわゆる戦略的なDDDというものだと思っている。

実際にはドメインエキスパートなどという人は世の中に存在するのだろうか。ユビキタス言語というものが仮にあったとしても、生産的な会話が常に行えるイメージは乏しい。
新卒の時のSIには、金融部門、生産部門などで部署が別れていた。推測するに、金融系のシステムの勘所、工場系システムの勘所というのは、部署内のベテランにノウハウが溜まっているのではないだろうか?
それらのベテランこそがドメインエキスパートであるし、現職のweb企業であるならば、古株になりつつある自分こそがドメインエキスパートたるべきなのではないか。
ドメインエキスパートにはその分野のシステムのノウハウを色々持っているだろう。こういうモデリングだと変化に弱いとか、requirementではあー言ってるが、実際はこういうニーズがあるはずだとか。

そしてこれらはrequirementを受けての分析を行う際に必要なスキルだろう。これがドメイン駆動設計の戦略部分と一致するのかは怪しいがなんとなく性質的には似てそうだ。
実際に、ある程度はモデリングが入ってきそうでもある。ただし、筆者の考えでは、ここでおこなったモデリングをそのままプログラム上でモデリングとして落としてはならない。
プログラムは、大枠の設計を実現するための詳細が語られる場所であり、それらの詳細が組み合わさって大きな視点でrequirementやconsiderationが実現されるものだからだ。
ミクロな視点では、更に様々な考慮事項があり、それはモデリングにも影響を与える。

これをconsiderationと読んで、requirementを受けて分析をする形の作業をしておきたい。

physical

物理制約も重要な事項になる。筆者はwebシステムばかりやってるので、ブラウザjsがあり、dockerで動くプロセスがあり、RDBにアクセスするという一辺倒なシステム構成しか知らない。
実際には、androidアプリがあったり、組み込み開発があったり、DB上にストアドプロシージャを登録したり、IoTのプロコトルを使ったり、クラウドQueueを使ったりする。
実際にプログラムを組むことになる際には、プロセスを並列で動かす必要があるかもしれない。

これらはすべてそれぞれ独立したコンピュータであり、コンピュータの物理的な要因だろう。
これらの物理的な要因は、requirementの時点で決まったりもするし、アサインされるプログラマの得意なものを選んだりする。
選ぶ基準についてもいろいろあるが、例えばDBとアプリケーションとブラウザjsというのはかなりオーソドックスなものでいつの間にやら選ばれたりするものだろう。

いつの間にやら選ばれるので、あまり意識されないが、立ち返ってみれば、別の選択肢もある。また、その制約の上でコードを書くことになる。
筆者は、設計論というものが、この物理的な要因を意識せずに語られることが多いような気がしていて、違和感が強い。
実際にはちゃんと語っている書籍もあるであろうはずなので、ちゃんと読まなければならないが。

また、パフォーマンスは実のところrequirementに入ってきたりするので、物理要因も検討するべきだ。

大枠の設計まとめ

以下の3つの項目をあげた。

  • requirement
  • consideration
  • physical

順番に説明したが、実際には一つ検討したら、前に立ち返る必要があるだろう。
requirementを受けてconsiderationを検討したら、隠されたrequirementを明らかにするために再度ヒアリング(対象が自分なら心の声)を行うべきだろう。
considerationの内容によっては、physicalがオーソドックスな構成から変化を求められるだろうし、それによってrequirementの内容を満たせるかも変わる。

機能設計

この機能設計というやつはwebの文脈になる。もしかしたら他のインフラでも似たような考えが浮かぶかもしれないが、筆者がwebを基本としてきたので。
大枠の設計を経て、いよいよシステムが具体化していく段階だ。

work flow

昔、オブジェクト指向UIというものの記事を見た気がするが、ややこしいモデリングに対しては、crudだけでは処理を定義できない。
どういう手順で、どう編集していくかの手順が必要で、ユーザが自由にやりたいことができるのではなく、誘導してやる必要がある。
こういった手続きの流れがあるものも、ないものもあるが、流れがあるならそれらは一連のものとしてグルーピングできるし、単なるcrudなら対象のモデルを中心としてグルーピングすればいいだろう。

この一連のまとまりをwork flowと呼んでおきたい。
work flowの中で、利用する機能がいくつか定義され、webであればurl endpointが定義される。

画面設計

画面上に表示する項目は、プログラマレベルで検討できるものは箇条書きでも構わないと考えている。
ただし、情報の構造は意識し、箇条書きはネストして書いておくべきだ。

画面設計は、work flowの中で利用されるものなので、work flowから検討できる。

db schema

特に説明は不要だろう。
だが他にも言及すべきことはある。たとえば外部のapiを叩くときには、そのapi schemaが必要だろう。
クラウドのqueueもある意味では外部システムなので、そこのモデリングも必要になる。
そういったschemaは定義しておかないと、コードが書きづらい。

application model

db schemaで表現できる制約はそちらに譲ればいいが、モデルの動きというか振る舞いというか、紐づく関数群というか、そういうものは事前に定義できれば開発がスムーズになる。
ここまで来ると、だいぶ内部実装に近い世界になるので、DDDの文脈でドメインエキスパートたるべき経験がないと、やりづらい。
でも、逆になくても、トランザクションスクリプトから起こして行けばいい。必要な関数は、モデリングの振る舞いとして外に出していく。
このあたりの経験があれば、共通というか、抽象度を上げた関数は何を定義すべきかわかるだろう。

言語

言語だけではなく、フレームワーク、ライブラリなど。
これも決めておくと、どのようにコーディングするかイメージがつきやすいだろう。
web開発の文脈では、基本的に必要なライブラリは揃っていることが多いが、ない場合は自作するので、そちらの設計も必要だ

プログラミング

基本構造

physicalの項目でも言及したが、プログラムには基本的な構造があるように思う。

  • 入力があって出力がある
  • プログラムからプログラムへ再帰的に入出力が行われる
  • 入力と出力がある部分はソフトウェアの境界となる

この入力と出力は、physicalの構成の境界で必ず発生する。
だが、多くの設計理論がこの境界を意識しない、あるいはさせない設計を誘導しているように感じており、筆者の感覚と大きく乖離する。
大枠の設計の段階では、physicalから導出されるソフトウェア境界など意識しなくれもいいかもしれない。むしろ意識してしまうと、そちらに思考がフォーカスされて大事なことが抜け落ちるかもしれない。physicalはphysicalで考えればよい。

ただ、プログラミングする段階では意識すべきだろう。
「一つのことをうまくやる」という単語があるし、関数は抽象化のために切るわけで、切った関数の命名によって概念が伝わり詳細を意識しなくて済む。
でもその関数の内部では複雑なことをやっているかもしれない。大枠での概念を、そのまま詳細に落とすべきではないと考える。
詳細を組み合わせることで、大枠の設計を概念的に実現するのだ。

テスト

テストはなんのためにするかというと品質を保証し、変更容易性を高めるためだ。
ソフトウェアは継続的に改修されることを想定するので、この変更容易性は非常に重要な概念だろう。
テストのために設計するなという言説もあるかもしれない。大枠の設計についてはそうだろう。この段階でテストまで考えて設計はできない。
しかし、テストがないプロジェクトでは品質の保証が非常に難しくなる。つまり品質が劣化しやすくなる。これを避けるために設計段階でテストの書きやすさは検討しておくべきだろう。

単体テストと呼ばれるような、細かく部品の品質を担保していく作業については、テストをしやすい設計の上で成り立つものだろう。
本来は、十分に独立した抽象概念にフォーカスできていればテストは容易になりそうなものだが、その十分に独立した抽象概念というやつは、際限なく考えられてしまうし、顕現する形も違う可能性がある。
単体テストをしやすいという概念は、わかりやすいので設計の方針としては妥当だろう。その結果として、十分に独立した抽象概念の獲得を目指していきたい。

これらを実現しようとした場合、入出力を行う部分と、行わない部分を分離するというプラクティスがある。いわゆるDIというものだったりする。
つまり、入出力を行う関数を、関数の引数にいれるというものだ。
ただ、このDIの概念をテストにフォーカスさせると、入出力ではなく、再現性という観点のほうが正しいだろう。
つまり入出力がなくても、乱数生成(実際にはIOしているが)や、sql組み立て、ややこしい計算などは再現性が低く、テストとしては別途フォーカスすべきものだ。
再現性の観点から、テスト設計はすべきだろう。

モデリング

機能設計の段階で、db schemaには言及しているが、実際にはこちらで考える分量もかなりあるはずだ。
モデリングというのは、特定のオブジェクトの項目や型を決めることだけではない。
そもそも型とは言語環境に依存するので、一概に表現できるものではないので、型と言ってもいいのかもしれない。
要は制約だったり、振る舞いだったりというものだ。

classベースのオブジェクト指向は、筆者は嫌ってきたが、いざデータ型を定義すると、そのデータ型に関連する関数は同じ名前空間に集めたくなった。
classベースのオブジェクト指向ではレシーバがあって、methodがあるというお約束は存在するが、データ型に関連する関数群を同じ名前空間に集めるのも、まさに振る舞いと言っていいだろう。
また、クロージャは便利だが、基本的には読みづらい。classというのはクロージャのおばけだという認識なので、クロージャが必要になるのであれば、classを使うべきだろう。
classの無い言語であっても、特定の構造体に対してinterfaceやらtraitやらを「実装」するという概念が存在する。

また制約は型で表現しきればものはバリデーションになるかもしれない。本来はアサーションと呼ぶべきだろうか。ここらへんの違いはあまりわかっていない。

更に関連も重要だ。特定のモデリングを集めた概念の上で関連が示せれば、それはDDDにおいて集約と呼べるべき概念だが、楽ではある。単にその名前空間に振る舞いを定義すればいいだけだ。
ただ、集約がなくても関連があるものもある。これはどちらが振る舞いの主体であるか検討しなくてはならない。
こういうのが面倒な場合は、class methodではなく、単なる関数のほうが取り回しが良いだろう。でも、どうしても振る舞いにしなくてはならないものもある。

入出力のモデリング

Ruby on Railsは筆者は触ったことがないのだが、似たようなフレームワークでlaravelは常用している。
laravelはDIやjobと言った仕組みもあり、厳密には違うのだが、基本構成はRESTful+MVC+ActiveRecordだろう。

これは、モデリングを単一に保ち、すべてをそれで解決する概念のように思う。
正直最近までMVCとはなにか理解できなかった。いつ誰がDBにアクセスするんだ?という。
なんのことはない、model自体はアプリケーション上のものとDB上のものを同一として扱える仕組みなので、正解はMになるのだ。

モデリングは、基本的にアプリケーションのほうが表現が豊かである。制約も自由だし、関連も示しやすい。
でもDBでなくては表現できない一意性制約というものがある。
Active Recordは、アプリケーションでもありDBでもあるので、これらの制約をすべてActiveRecordモデルで表現できるという意味で秀逸なデザインだろう。
RESTfulであれば、MVCのMに対するcrudだけ実装すれば、基本的には事足りる。それらのインタフェース上でやり取りされるデータ型もMを流用できる。

だが、実際にはこれはそんなにうまくいかないことも多いだろう。特に継続的に改修されるシステムは、requirementが変化しつつも既存のモデルを流用したくなったりする。
そうなるとモデリングの意味が変化したりするし、制約も増えたりする。
ユーザから見て、オブジェクト思考UIのようにcrudだけで表現できるアクションだけでは足りなくなったりもする。モデリングが複雑になればなおさらだ。

既に入出力については述べたが、入出力にも別途モデリングがあるべきだろう。
ただし、それはアプリケーションのモデリングよりも機能がすくなくなるはずだ。入出力のinterfaceを表現するものとしてモデリングがある。
これはwebからのrequestであれば、そのrequest/responseのデータモデルだろう。crudと違う概念であれば、独立して検討すべきだ。
DBについては、プログラムからプログラムを再帰的に呼び出しているといえる。つまりDB側と通信の際にもソフトウェア境界があり、入出力があり、そのモデリングがある。
ORMapperライブラリが常用される最近においては、DBのinterfaceはテーブルだと思われがちだが、筆者の意見は違う。
sqlこそがinterfaceであり、それらをグルーピングする概念としてtableに倣うのだと考えている。

この入出力のモデリングは機能が少なくてもよく、内部のアプリケーションでのモデリングに変換されればいいだろう。つまりアプリケーションのモデリングとの変換ロジックを持たせたい。
Active Recordの説明で一意性制約に言及したが、一意性制約はDBのプロセスが担保するものなので、DBに問い合わせればよく、アプリケーションのモデリングでは表現したくない。この辺りは分離して整理しておきたい。
その問い合わせの際に、この入出力のモデリングを利用する形だ。

全部盛りプラクティス

基本構造で説明した3つの概念と、テスト容易性や、入出力のモデリングを愚直に表現すると以下の要素が必要になる。

  • inbound handling
    • transferable
      • in
      • out
  • procedure
  • model
  • outbound modules
    • function
    • transferable
      • in
      • out

inbound handling

これはプログラムの起動時のハンドリングといっていいだろう。
ただし、webサービスにおいては、特定のurlに入ってきたデータのハンドリングとも言える。
入力は基本的には文字列で入ってくるはずだ。linuxは文字列を汎用的なものとして扱う。
transferableのinは、後述するがその言語環境で可能な型表現を用いたい。つまり文字列からその型に変換する役割もある。
逆も然りで、呼び出したprocedureに渡したあとに返ってくるtransferableのoutの型の値をserialize可能な値に変換する役割もある。

procedure

procedureというのは関数の呼び方の一種でもあるし、直訳すると手続きでもある。
このモジュールは、他の設計プラクティスにおいてusecaseなどと呼ばれたりもするが、usecaseというのはシステムをユーザがどう使うかという概念で、より大枠の設計や機能設計の文脈ででるはずの単語という印象がある。
実際にはそのusecaseと同等だったりもするが、work flow自体をusecaseと呼ぶこともできるはずで、別の呼び名で呼びたくてこうしている。
命名には強いこだわりはないが、区別しておきたい。

手続きとしたのは、outbound moduleの呼び出し管理を担うべきだからだ。複数のoutbound moduleを呼び出すのは、宣言的に実装するのは難しい。
モナドをハンドリングしやすいdo構文を備えたhaskellではdo構文は手続き的に見える。
手続き的プログラミングという言葉は嫌われやすいが、実際には手続き的に書く部分と宣言的に書く部分を整理すべきなのだ。
そして、outbound moduleの呼び出し管理をするということは、手続き的な表現になるはずで、つまり機能の実現のための手続き的な呼び出し順序を知っているのがこのモジュールである。

基本的にロジックはoutbound moduleやmodel、transferable側に任せることになる。それらをどのような順番で呼び出し、どう組み合わせるか知っているのがこのモジュールとなる。
呼び出す、outbound moduleやmodelがよい抽象化を行えていれば、この手続きの流れを読むだけで、何が行われているか一目瞭然となるだろうことを目指している。
詳細は各モジュールに潜っていくイメージだ。

outbound module

これはinbound handlingとは逆にプログラムから外に出るモジュール。これらは外に出るので、IOを伴う傾向が強く、procedure上にdiしておきたい。
また、単独のモジュールとしておくことで、後から必要ライブラリを切り替えたい場合にも、そのmoduleに閉じた変更のみで済む。
IOを伴うとしたが、cpu boundな処理、非常に複雑な計算は、同様にprocedureが依存しない形を取りたく、これらもoutbound moduleと呼んでよいだろう。
同一コードベース上で別processを動かす非同期、並列プログラムも同様だ。これらは、別のprocessにoutboundな通信をしているとも捉えられる。
そして、そのoutbound moduleの中で、再帰的にinbound handling, procedure, outbound moduleが現れる。

outbound moduleは、それらアプリケーションの外界の事情を吸収する層でもある。
なんらかの都合で、たとえばGoogle Cloud StorageからAmazon S3に移行する場合にも、その違いを吸収し、その他のモジュールに変更の影響を与えないようにする。ラッパーだ。
これによって、何らかのインフラ変更、ライブラリ変更の影響を最小限に止められるようにしておく。

model

モデリングは様々なところにあるので、ふさわしい命名では無い気がする。別の呼び方で思いつくのはobjectとかだろうか。ただobjectも使い古された非常に手垢のついた単語でもある。
entityと呼んでもいいが、これはDBにあるレコードを実体化しているという意味での実体だろう。dbのレコードを対比して命名したくはない。outbound moduleの概念はdbアクセスだけに適用されるわけではないからだ。

とにかくいずれにしろ、applicationの中心として、modelが作られ、構造体の型が定義され、その振る舞いとしての関数、constructorやバリデーションが定義されてモデリングを形成する。
おもなロジックを記載するところであり、procedureから呼び出されるという点においてはoutbound moduleと変わらないが、再現性は比較的高いので、procedureにdiする必要性は低いだろう。

modelは制約の表現を色々と持つ。型で表現しきれればいいが、依存型が必要だったりとかなり重たい言語実装が必要になったりする。基本的にはバリデーションで実装されるだろう。アサーションと呼ぶべきかもしれない。
ふるまいといっていいだろうが、それらの関数は言語機能に従って実装すればいい。特に実装を多態としたい要求がなければ、単なる関数としてもいいだろう。
いずれにしろ、特定のmodelに関連する関数は、同じ名前空間においておきたい。

transferable

これはinbound handlingやoutbound moduleのようにプログラムのインタフェースとなる部分のモデリングだ。DTOと呼ばれるものが近い気がしたので、命名を拝借してtransferableとした。
プログラムのIOは基本的に文字列で行われるので、それらを変換するとtransferableになる。inとoutがあり、in/outもmodelに相互変換するロジックを持つ。
アプリケーションの計算はmodelが役割を持つので、変換しないとロジックを実行できないからだ。そして変換に集中することで、modelの機能が肥大化しないようにできる。
したがって、modelに依存しているが、model自体はtransferableに依存しない。これは、outbound moduleの種類ごとにtransferableがあるはずで、変換ロジックもそちらに寄せたほうがいいはずだからだ。

modelはさまざまな制約の表現として、バリデーションを持つが、transferableはいずれにしろmodelに変換されるので、言語環境が提供する以上の型チェックは不要だろう。
これはいずれにしろmodelに変換され、modelのfactory関数でバリデーションされるからだ。modelの制約がtransferableに漏れてしまう方が、制約の記述が分散して読みづらくなる。ただ言語機能が提供する型制約は言語環境で汎用性が高いので使うほうが便利だ。
また、outbound moduleのoutな値は既にバリデーションされた値であるはずなので、特にチェックも不要だ。
言語環境の型に従うというのは、そうは言ってもテストケースを減らすために、利用言語で共通の知識である型はプログラミングを楽にしてくれるからだ。すべてstringで来るよりも、考慮すべきことは減って楽になる。

関連

できれば図で示したいところだが、以下のように表現しておく

  • 呼び出し
    inbound handling -> procedure -> 各種outbound module
  • モデリングの依存
    inbound handlingのtransferable -> model <- 各種outbound moduleのtransferable

procedureでは、transferableもmodelも扱う。inbound handlinやoutbound moduleではtransferableしか扱わない。
いわゆる、オニオンアーキテクチャのような図でも説明できるが、モデリングと処理の関係も示しつつ、依存の方向はそれぞれ違うので、上記の記述のほうがいいだろう。

patternや簡易化

これらはmodelとtransferableで複数の構造体を定義していくことになる。これは非常に面倒と感じる人も多いだろう。
inbound handlingのinは独立したモデリングになる感覚の人は多いだろうが、outはmodelをそのまま流用したいことも多いはずだ。ただ、ここでサボらずに変換ロジックを組むと、公開したくないサロゲートキーやパスワードみたいなセンシティブな情報はfilterしやすくはなるが。
DBのモデリングも、ほぼaplication上のmodelと同一ということも多いはずだ。でも外部のAPIから取ってくるデータモデルがあれば、それらを組み合わせてapplocation modelを作るのでDBのモデリングだけでは片手落ちにはなる。

これらをどう設計し、簡易化しておくかは、状況に寄るだろう。上記に述べた通り、outbound moduleの種数や、センシティブなデータを扱う傾向があるかなど検討事項になる。
これらの簡易化が、うまくハマっているケースもたくさんある。repositoryは、dbアクセスのout transferableを無視して、application modelのデータ型をreturnするという見方もできる。
active recordなんかも同様だ。これは更に解釈を拡大して、そもそもdbモデリングとapplication modelを同一としている。RESTfulだとすれば、inbound handlingにおけるtransferableも同一として扱う傾向がある。

したがって、パターン言語は、上記に述べたものをどう組み合わせるかで説明できる。今までになかった概念も立ち上げられるかもしれない。
繰り返すが、簡易化し、パターンを適用するのは、状況を見て考えるべきだ。筆者としては、なるべく分離したほうがいいという考えで、それは将来にわたる変更を想定できなかったり、干渉地帯である変換関数という場所の確保が、今後の変更に強くなるための場所であるように感じるからだ。

そして、この細分化した概念があれば、先に述べたように、プログラムパターンへの評価もしやすくなるだろう。ここはこういった事情で、このパターンを適用するが、本来はこういった事情をサボっていることを意識しよう!という具合に。
全部盛りプラクティスの概念は、常にこれをしようというものではない。さまざまなアプリケーションに適用されている設計パターンを解釈して評価し、改善点を探すためにも使えるものであるはずだ。

設計言語との関連

また、モデリングが出てくるが、これはドメイン駆動設計では決して無い。そもそもこれは、大枠の設計で検討されたrequirementとconsiderationからモデリングを導出し、physicalの制約から考えて、必要なモジュール分割を適用したものだからだ。
なので、ドメイン駆動設計の戦術的な理屈は検討されていない。似たような部分はあるとは思うが。

そもそも本来は、physicalの要素と、considerationの要素を両立するために、どう設計するかという視点が必要なはずだ。
そのために、極限まで要素を分解し、あとからサボれる部分を再結合し、設計パターンを適用するという思考のほうが、より設計に説得力を持たせられるだろう。
requirementについても、例えばよしなにサジェストしてほしいが、決めているものがあれば、決めて置きたい場合は、request_xxxとderivate_xxxという2つの項目がモデリングにいるだろう。requirementによっても設計は変化する。

その他実装プラクティス

認可

認証ではない。認証は認証サービスやOCIDプロトコル、ログイン機能に任せる。これは特定の機能として独立すべきものなので、設計の文脈では特に言及せず、設計プロセスの中で機能として定義していく。
認可は、逆にどの機能にも存在する。よくあるパターンとしては、webのinbound handlingで処理するパターンだ。middlewareで実装される。
だが、これが特定のリソースに対して権限制御する場合はどうだろうか。DBアクセスしてみないとわからない。認可はprocedureで行う必要が出てきて、middlewareでの実装と分離してしまう。
ここは割り切って、procedureで認可を行うほうがいいだろうと考える。もちろん認可用のmodelや機能を用意して、呼び出す形を取る。
procedureは手続き的に書くモジュールなので、step by stepでearly returnが書かれていても不思議ではない。その中に認可チェックのearly returnが加わるだけである。

transaction

transactionというのは処理の単位として語られる単語でもあるが、基本的にはRDBのセッションのことを指すべきだろう。つまりDBアクセス以外のoutbound moduleにおいては関係の無いことだ。
webシステムはDBアクセスが基本的な入出力となる関係上、フレームワークのコンテナ上でコネクションとトランザクションを管理しがちだが、これらは本来他のoutbound moduleと同列に扱ってもいいはずだ。
同列というのは、procedure上で、transactionの開始、終了を管理するということで、フレームワークに頼らないということだ。
transactionをrollbackする条件は、実のところdbに限らず、例えば、外部システムのAPIアクセスによっても左右される。どこでtransactionを開始終了するかは本来コントロールしたいはずなのだ。

なので、procedure上で、outbound moduleの一つであるdbアクセスモジュールの機能として呼び出せばいいと考えている。
dbアクセスモジュールを注入してやれば、特にmockingも難しくないはずで、テストも簡単だ。
これは過激な理屈だとは思っていたが、以前twitterでも同種のことを言っている人がいて安心した。世間一般の感覚としてはフレームワークに管理してもらいたいというプログラマが多いとは思うので、実プロジェクトへの導入は注意深くしたい。

logging

transactionの管理をprocedureに任せると、AOP的なモジュールとモジュールの間に挟まる概念の必要性が低くなる。ここで考慮しておかないと行けないのはloggingだろう。
そもそもいつlogをだすべきなのか?ということはかんがえなくてはならない。基本的にはinbound handling側でin/out時に、また重たいoutbound moduleでのin/out時に出すべきだろう。
cpu boundだったり、ロジックの複雑性のためにoutbound module化しているものも然りだ。つまりは、inbound handlingやoutbound moduleで任意のログを出せばよく、procedureでは不要と考える。
どのprocedureが呼ばれるかはinbound handlingが知っているので、そこでログに示せばよい。内部の手続き的stepまでログを出す必要はdebugのとき以外無いだろう。
ちなみに筆者はprint debugは好きなのでよくやる。

DI

outbound moduleは、当然副作用があり、つまり再現性がなくprocedureでDIをしたい欲求があるように思う。
したがって、DIが必要になり、DIコンテナがいるということになる。
DIコンテナでよくあるパターンが設定ファイルを書くというものだが、これを嫌ってJavaでアノテーションを使おうというものもある。
このアノテーションのいいところは、そのprocedureが依存しているモジュールを直接その名前空間に書くことで、実際には何を呼び出すのかわかりやすいという点にある。
正直、これはfactory関数でその依存を解決して、factoryの中でコンストラクタを呼び出すのも似たようなものではないか。
なので、DIをしたい際にはfactory関数でもよいと考えている。

factory関数でまずいのは、呼び出し元の名前空間に定義したfactory関数に強く依存があることだ。ただ、テストとしては問題ない。
factory関数とconstructorを分けておき、constructorは引数にDIすればいい。テストの際はconstructorを呼び出す。
実コードでは、さらなる呼び出し元でfactory関数を呼び出してやることで、その依存関係が保たれる。
テストなら問題ないとしたのは、プログラムとして動くほうが主で、テストは従だからだ。つまり、本来の想定は表現され、その他のケースではうまくモックすればいいという考え方だ。

これがもし、実際に動くコード上で、依存関係を切り替えながらするということであれば、factoryのfactoryを用意すべきだろう。
pluginシステムを採用しているプログラムでは、plugin自体がioを持つことも少なくないはずだ。これらのコードは実際に動くコードになるのだから、facotryで、どのpluginを採用するか決定し、そのfactoryの中でそのpluginを初期化すればよい。
いずれにしろ、factoryのfactoryによって、プラガブルになるので、コンテナDIする必要がない。これをDIコンテナに任せようとすると、どのpluginをどう使うかというコードをコンテナ設定側に記載することになる。これは良くないだろう。

また、将来的に特定のモジュールを切り替えることを想定するかもしれない。ライブラリも改善が進み、違うIOを伴う違うライブラリをつかいたくなるかもしれない。
これに関しては、outbound module自体が、これらのライブラリのラッパーとして機能するはずで、そのoutbound module上で移行させればよい。
outbound moduleのfactory関数の中で、切り替え対象のfactory関数を呼び出せばいい。return typeは同じはずだ。

最後に、DIは、再帰的に依存関係を解決するというものだ。ただ、この依存関係のネストが深くなること自体、取り回しがしづらいのでコードが難しくなる印象が強い。
procedureで入出力を管理しているのだから、例えばmodelのロジックの中で入出力の呼び出しが必要になるのならば、procedure側でそれらを組み合わせるほうがいいと考える。
つまりは、クロージャとして関数をラップして渡すことになる。クロージャはわかりやすいプログラミングテクニックでは無いと思うが、それよりもprocedureレベルで入出力とmodelロジックが管理できないことのほうが見通しを悪くする。
modelロジックがdbアクセスモジュールをDIしてしまうと、いつ入出力が行われるのかprocedure視点では把握できないということだ。
もし前段のmodelのロジックの中でDBアクセスし、そのDB状態に依存した次の処理があると、かなり見通しが悪くなる。
modelの中でどうしてもDBアクセスしたい事情もあるが、そうなったときは実はmodelのロジックを再検討して分割するチャンスでもある。結論分割しないでも構わない。
ただ、筆者の経験上、DBアクセスが必要になるときは、そもそもユーザに問い合わせる必要も出てくることも多く、そもそもprocedureの単位を分割して、シンプルにできることも多い。
procedureの分割は、つまりは機能設計へのフィードバックということになるので、ここでは論じないが、そういった事情もあるだろう。

そう考えるとDIを適用すべき実例としてはテスト以外に無い。そしてテストは主従がはっきりしているので、実コード場はfactory関数でも問題ない。
DIというのは、各モジュールを疎結合に保つことで、取り回ししやすくする仕組みだ。取り回しをするのであればconstructorをどう呼び出すかという視点で考えればよい。通常使われるコードはfactory関数の中でconstructorを呼び出す

この理屈は、かなり過激な方の思想だろう。特にinbound handlingで、procedureのfactory関数を呼び出すことになる。つまりinbound handlingにおいて、依存性の解決が行われる。
よくあるフレームワークでは、この依存性の解決が行われた状態で、transferableのinが処理されるわけだが、本当にそうだろうか?
どちらかというと、入力のデータモデルの処理が先にあり、リソースアクセスの管理は、必要に応じて作るものである気がする。そう考えると、inbound handlingの機能として持たせてもいいのでは無いかと考えている。
ただ、過激なので、実プロジェクトでは相談しながら適用したいとおもう。

DB Access

DBアクセスの代表的なパターンといえばActive Recordだろう。あるいはRepositoryも然りだ。他にもtable gatewayとか、record gatewayとか色々ある。
極限まで分解してしまうと、そもそもmodelとtransferableは別ととらえなければならない。Active Recordはそもそもmodelを統一するものだし、repositoryもあつかうmodelの形でin/outが行われることが想定されている。
なので、ここで筆者が推奨する方法とは違うだろう。DBのtransferableとしてin/outを定義して使うべきだという考えだ。
ただし、そもそも他のoutbound moduleが無いのであれば、modelとこのtransferableも同一とみなしていいだろう。上記のサボるポイントである。

あとRepositoryもActive Recordも、特定のmodelを中心として、モジュールを切るという形を取る。例えば、employee tableとcompany tableのrepositoryは別に用意するだろう。
だが、companyとemployeeを組み合わせて取ってくるカスタムなデータはどこに定義しておくべきだろうか。company employee repositoryを定義してもいいが、どちらかというとそこに関数が集中して、肥大化しそうな気もする。関連の強いテーブルはどちらが主従がわからない。

modelとtransferableは分けて考えるので、modelへの紐づけは、transferable側の変換ロジックが担うはずだ。dbアクセスするoutbound moduleでまで関連を示す必要がない。
つまり、特定のmodelを中心としてクエリを集めておく必要性が低くなる。わかりやすさのためにディレクトリを切って置くのはいいと思うが。

また、苦労して特定のデータを取ってくるクエリを定義したとして、それが使われるprocedureは多いだろうか。
DBはsqlでアクセスするが、sqlは宣言的な構文だ。自由に組み立てることはできるが、宣言的に書いたほうがわかりやすい。これらの要素を検討すると、特定の一つのクエリ毎に名前空間を用意するほうが良いと考えている。
プログラミング言語の機能にもよるが、procedure名前空間で、直接それら独立したクエリの関数を組み合わせて一つのDBアクセス機能モジュールを用意することが可能なはずだ。
以前typescriptでやったときは、単なる関数を同名でオブジェクトのプロパティとして、procedureのfactory関数で組み合わせるということをした。

これにより、sqlクエリ自体が一つのファイルに閉じるので、独立性高く保つことができ、それを集めたprocedure上でも取り回しが楽になる。DBアクセスというもの自体を、procedure上でカスタマイズしてモジュール化できるので、複数のXxxRepositoryに依存しなくて済む。
これにも何かしらパターン名をつけたいがいい命名は思い浮かばない。
これも割と過激な理論だろう。実プロジェクトで適用する際には注意深く相談すべきだ。

その他

モデリングの科学

modelというモジュール領域は用意したので、モデリングはしやすくなると思う。大枠での設計においてもconsiderationで思考するときにモデリングも検討するだろう。
ただ、モデリングそれ自体を体系的に分析することはできていない。モデリングの手法だとたとえばイベントソーシングというやり方もあるが、基本はブレストであり、分析の手法というより出し切るための手法という印象がある。
モデリングを体系的に理解するときには、たとえばDBの正規化の話とかはキーワードになるかもしれない。いずれにしろ、このあたりは別途キャッチアップしていく。
とはいえ、実際にモデリングするとなると、割と手癖でできている状態なので、あとはそれを体系的に理解して再構築すべきというところだろう。

いずれにしろ、モデリングを行う設計ステップ、モジュールの構成は定義できているので、モデリング自体にフォーカスすることができるはずだ。

改善サイクル

プログラミングで、システムの基本的な構造を示し、モジュールを分割していくとこうなるのではというのを示した。
そして、それによって、どこにどういったモデリングをすればいいかわかるようになっている。
また、構造に従ってモジュールを切ることで、テストしやすさも上がっているはずだ。

これは、自分が仕事で、難しめな改修案件をずっとやっていて、その中で得た経験も検討ルーツになっている。
モデリングが問題なのか、アーキテクチャが問題なのか、最初は理由もわからず、コードをこねくり回していたが、出来上がったコードは、このようにアーキテクチャ的な決まりごとの上で、モデリングが検討されるという構図を持っていた。
つまり、どこにどうモデリングするという指標がない状態では、様々な文脈が混在し、混乱を巻き起こす。個人的にActive Recordがnot for meなのは、モデリング自体の分析ができず、一つのモデルに押し込めてしまう傾向があるからだ。

そしてモデリングができると、今度は、機能設計や大枠の設計にフィードバックできる。
モデリング上実装しづらいことや、分析していくと非同期かしたほうが良いことなどが、モデリングの検討中にでてくるので、機能を分割したり、大枠でのドメインモデリングの変更などフィードバックできる。
これは、初期開発でなく、改修の際にも有効だ。モデリングがしやすいと、分析がしやすく、フィードバックとして大枠の設計に戻れるので、分散システムや、機能の分割を提案しやすい。

内部のアーキテクチャができ、その結果としてテストが書きやすくなる。
その次にモデリングを検討し、よい抽象度でコードがかけて、読みやすいリポジトリができる。
そこまで来ると、機能設計や分散システムの検討が容易になる。
という、改善におけるサイクルが回りやすくなるという、仮説がある。
そのためにも、基本的なプログラムの構造を意思し、どこにどうモデリングして、どういったパターンを適用していて、それはどう再構築できるのか?を意識しておくといい。

大枠から降りてきた仕様ではあるが、ボトムアップでフィードバックできるはずなのだ。
それをするためにも、内部品質を上げることが重要と考える。

対立軸を意識する

いろいろと述べてきたが、こういった設計論を展開する前に、より汎用的な思考のプラクティスとして、対立軸を意識するというのがある。
これは様々な場面で役に立つ印象がある。

そもそも仕事とは、何らかの問題、課題に対して解決策を提供することで対価を得るということだと思っている。
その解決策を導くのが対立軸を意識するということだ。
なんらかの対立した概念を両立、あるいは妥協させたり、時間的なコストを払って解決したり解決策というのは様々ある。

ただ、ソフトウェアの技術者としては、技術で解決策をだしたいところだ。
この技術で解決策を出すという構図は、大枠での設計で述べたrequirementとconsiderationで検討した領域が対立しているところに、ソフトウェアを介在させるということだと思っている。
ただソフトウェアには物理的な制約もあり、それをphysicalで検討して置きたいという構図だ。

ただ、技術というのは必ず解決策となるわけではない。無駄なものもあるだろう。時代に合わなくなったりもする。
しかし長生きしている技術というのは必ず何らかの領域において解決策たり得るものであるはずだ。

大枠での設計で検討したことも、まだ解決策として具体化されていない。それらのソリューションが機能設計であり、最後にはプログラミングに落ちてくる。
そのように解決のサイクルが回っている。そして解決できないものは前のステップにフィードバックする。

対立軸は、様々な場所に存在する。これらを意識すれば、対立したもの、嫌いなものも評価できるようになるだろう。
これは設計を行う前段でのマインドセットとして重要と考えている。

コード量との対立とAI

Ruby on Railsは極端なモデリングの簡略化によって、大枠の設計からプログラミングの設計までを、scaffoldingによって一気に作りきり、その状態からrequirementを提出する利用者とプロトタイプを見ながら開発するというもので、それは開発速度を爆速にしてくれそうだ。
だが、継続的に開発する際には難易度が高くなってしまう。モデリングが肥大化し、整理するにも整理する場所が用意されていないからだ。
逆にこの記事でのべて全部のりプラクティスはモジュールの分割がかなり細かく、コード量が増える印象もある。
ただ、これもAIによるコーディングが解決してくれるのではないかと期待している。どちらかというと、サボってパターンを適用するよりも、極限まで細分化してやったほうが、AIがその名前空間に書くべきコードがクリアになって、理解しやすいのではないだろうか。
テストも含めてコード量は多くなるが、細分化されて一つ一つは理解しやすくなり、AI handlingableになれば、Railsほどではないにしても、開発速度をあげれないだろうか。と考えている。

outro

批判的だったり、まとまってなかったりしているが、意図的に整理していない。整理は今後書籍を読みながら整理するほうが良いだろう。あくまで現段階の考えをdumpしている。
批判的な意見もあるが、基本的には全面的に批判的には思っていないはずだ。技術というのは長生きしているものは有効なソリューションであるはずだからだ。何かしら使われる理由があって、その理由が自分に当てはまるか否かこそが、最も重要なことだ。
そのためには、設計における要素を分解し、再構築する段階において、技術要素を説明していくしかない。そのための考え方を述べてきた。
今までに存在している設計論を否定的に捉えてもいいことがないのだ。否定したいのならば、対立軸があるはずで、対立軸を明らかにすることで、じつはその設計論をうまく活用し、次のステップとしてより良い設計論に消化できるはずなのだ。
なぜなら、対立軸さえわかれば、それらを踏まえて納得しやすい理屈が明らかになり、そのソリューションとして新しい設計論が出てくるはずだからだ。

今後、しばらくはこの理屈をもって、ソフトウェア設計を行い、各種書籍や、仕事で議論を通じてフィールドバックを得ながら、理屈を強固にしていきたい。
各設計パターンを分解して再構築できるのであれば、それらの設計パターンを書き換える、ソフトウェアの内部品質の改善においても、より良いプロセスを提案できるだろう。この手の話になると、結論として作り直そうということも多いが、そうではなく、内部を段階的に改善していく施策のほうが良いこともあるかもしれない。
また、これらの理論は、あくまで一つのシステムをどう捉えるかに閉じている。分散システムについては、これらのrequirement,consideration,physicalの3軸で説明できることも、他に検討すべき視点もあるかもしれない。勉強したい。
モデリングについても、モデリングというものがなんなのか、それ自体を科学して理解し、手法を再構築するところを目指したい。
それらにキャッチアップしていくのが、今後の筆者の課題である。たまたま纏まっていないこの文章を発見した読者は、温かい目で見守ってほしい。

GitHubで編集を提案

Discussion