設計まわりにおける思考の変遷
このスクラップは脱稿できるような質にない、勉強中であったり考え中であったり書き途中であったりするものなので、おおむね正しくないものだと思ってください。また過去の記述は、必ずしも現在の自分の意見と一致していないことがあります。
private なメソッドは不要という意見、およびその近傍について
TR;DR / private なメソッドは不要か?
結局は状況次第(だいたいぜんぶそう
private なメソッドが生えてきたとき、切り出すことは妥当かと検討することは、きっと良いこと。
前提
自分は真面目に触ったことのある言語は PHP, TypeScript だけなので、他の言語では仕様的に克服できる側面があるのかもしれない。
リンク
Privateメソッド禁止、ということについて提起されている記事
禁止という言葉は強すぎるかもしれないが、示唆に富む視点であるように思われた。
なぜ private なメソッドを書きたくなるのか?
- 適当な粒度で public なメソッドを分割したい
- 外部からの依存が全くないことを担保したい
- クラス内部からしかコールされないであろう共通処理を抜き出したい
private なメソッドを生やす方が妥当であるケースも否定できないのでは
private なメソッドに依存できるのは同クラスのメソッドだけであるが、たとえば別クラスとして切り出されたメソッドは結局のところ public なので、どこからでも依存されうる。
『Privateメソッドを作りたくなった時は存在するべきクラスを見逃している』とリンクの記事にはあるが、必ずしもそうではないと自分は思う(もちろん、そうであるケースもあり検討すべきだろう)。
そのクラスの処理と密接に関係していて、まずそのクラス以外から呼ばれることはない、でもメソッドに切り出したい、だから private なメソッドとして生やしたい、というケースは否定できないはず。そうでもないかな?
ルールの遵守をエンジニアの倫理観に委ねる、ということについて
話が逸れるようだが、考えを深めたい部分なので、あえて触れたい。
ルールの遵守をエンジニアの倫理観に委ねるより、コードとして遵守を強制できた方が堅牢である。これは個人的は揺らぎない。
一方で、そこそこ複雑な設計手法を実現しようとすると、多くのルールについて遵守をエンジニアの倫理観に委ねる必要が生じる。たとえば DDD や CA で書かれたプロジェクトでも、エンジニアがその気になればリポジトリの実装にドメインロジックを漏らすことだって出来たりするはずだ(静的解析でそこまで縛っているプロジェクトもあるかもしれないけれど)。
何が言いたいかというと、ルールの遵守をエンジニアの倫理観に委ねるというのは、コードとして遵守を強制することより堅牢ではないという前提があるにせよ、開発の現場では一般的に行われているということである。
private なメソッドが存在しない世界
public/protected/private といったアクセサによって、公開/非公開を制御するという考えを捨てる。
理想論としては、抽象に依存すべきである。すなわち interface として公開されていないものについては、そのモジュール内で public であっても、モジュール外からみたら private なのである。
モジュールの粒度を適切に保っており、かつ interface があるにも関わらずソレを飛び越えて具象に依存するような不届きものが現れないのならば、private にしたかったメソッドをなんらかのカタチで public にしたところで、依存関係が波及する範囲を限定することができる。
現実的な視点からの批判
すべてのモジュールに interface を用意するというのは、現実的には面倒が勝ることが多い。
DDD や CA の適用を考えるような大規模プロジェクトであればあるいは。。。という肌感で、小中規模のプロジェクトならば、部分的に具象に依存することを妥協することもあるだろう。
また基本的に隠蔽されているに越したことはなく、そのクラスからしか呼び出されていない private なメソッドを、わざわざなんらかのカタチで public にする、ということには妥当な根拠が必要だ。private なメソッドは単体テストがしづらい、という理由も考えられるが受動的な感じがする。
リンクにあるような、わかりやすく機能過多なクラスだったら妥当な感じがするが、 private なメソッドにしておいたほうがおさまりがよく見える処理というのも存在しうるだろう。当初の想定とは異なりモジュール内で参照したくなったら公開するから、と言われたらソレもアリかなと自分は許容する(モジュール外で参照したくなったら話し合いたい)。
いや、存在してもリンクにあるように分類できるからソレを除けば存在し得ないよ、というのならば、それが明文化されているのならば、もちろんソレもアリ。個人的には禁止するより、自分が private なメソッドを生やしたくなったときや、レビューで生えてきたとき、より良い実現方法を考える習慣をつけるくらいが妥当かな。
あらためて、private なメソッドは不要か?
結局は状況次第(だいたいぜんぶそう
大規模開発でモジュールが適切に分割され interface が用意されているのならば、原則禁止とするのは、方針としては考えられる。中小規模開発で禁止するのは、モジュールの結合度が高くなってしまう可能性がある。
private なメソッドのテストについて
よく語られるので、備忘録として
なぜ interface を使いたい気持ちになれないのか
TL;DR
- モジュールの実装を見たり知ることについて、もとよりそれほど抵抗がない(実装の隠蔽を重視していない)
- そのモジュールをつくったのが自分自身であるときは、そもそも中身を熟知している
- そのモジュールが正しく動いているというかという点から疑う必要が生じることもあり、より中身を知っていることに価値がある
- 開発者が実装を隠蔽するに足る interface を記述しようとしていない
- interface が存在しなくても具象に依存すれば実装はできるため、開発速度などの要求によって無視される
- 開発の規模が大きくなく、具象に依存することによる問題が表面化しない
- 開発者がモジュールという視点でソースコードを把握する習慣がない(結合度や依存関係を重視していない)
interface は、実装の隠蔽と、結合度や依存関係の制御、この両者が充足されていないと、意義を理解されづらい。
たとえば
(設計まわりの話で、プログラミングに関係ないモノを例示として扱いたくはなかったが、他に良い表現が浮かばなかったので。例示が望ましく無く、共感を得られないかもしれない)
電子レンジがあるとする。
電子レンジを使うとき、中身がどうなっているかなど、見たくもないし、知りたくもない。
パッと見でボタンやツマミにある表示、説明書などを読めば使えるようになるし、そうでなければ製品として問題がある。
我々が実装するモジュールは、電子レンジほど、パッと見で分かるものが画一化されていない。
いままで文明の利器に触れてこなかったひとが、初めて電子レンジを見たときと同じくらい、パッと見で分からないことが多いだろう。
他方で、我々はモジュールの中身を見たり知ることについて、それほど抵抗がない。少なくとも電子レンジの中身よりは。
特にそのモジュールをつくったのが自分自身であるときは、そもそも中身を熟知している。また外部パッケージでないときは、そのモジュールが正しく動いているというかという点から疑う必要が生じることもあり、より中身を知っていることに価値がある。
なぜ interface を使う/使いたい気持ちになれない?
モジュールに対して interface を定義するのは、今回関心がある側面で言えば、モジュール間の結合度を下げたり依存関係を整理したり、実装を隠蔽するためである。
開発の規模が大きくないときは、interface を使っていなくても問題となりづらい。 interface を使うのは合理的ではないと判断される可能性すらある。
開発の規模が大きくないと、密結合していたり依存関係が相互になっていたり、実装を知ることが強要されていたとしても、開発者が十分に抱え込める(と開発者が無意識に信じている)ことが多いように思われる。そうであるならば、interface を使うのは面倒であるように感じられ、また使うことの意味を実感できる機会も少ないだろう。
また『実装を知ることが強要されて』いること自体にも抵抗が小さい。延いては属人化することや、途中で加わる人員の学習コストを重視していない。
大規模開発であっても、開発速度の要求が大きければ、interface を実装する手間すら惜しくなるかもしれない。あるいは開発者がモジュールという視点でソースコードを把握する習慣がなく、結合度や依存関係を重視していないために、interface が実装されないこともあるかもしれない。
ポリモーフィズムを実現するための interface とは異なり、結合度や依存関係のための interface は、究極的に言えばなくても実装はできる。interface を削除して具象に依存すれば良い。抽象に依存することが、場合によってはエンジニアのエゴだと見做されることすらある。それが正しいかどうか、結局は状況次第(だいたいぜんぶそう
実装を隠蔽するに足る interface が記述されていない、ないし記述することに慣れていないことによって、必要性を感じられないことも考えられる。電子レンジに説明書がなく、ボタンやツマミになにも表示がない、でも電源も入らないという状況で、使い方を知る必要があるのならば中身を見るしかない。
なぜ PHP で interface を使いたい気持ちになれないのか
TL;DR
PHP で実装を隠蔽する意義に疑問があるため。すなわち型の表現力に乏しく、interface によって実装を隠蔽するとき、多くを PHPDoc に書かなければならないため。
思考の経緯
表題で PHP と言いつつ、JavaScript (JS) と TypeScript (TS) の話をする。
JS は難しい。黒魔術のように思える。JS で整合的に成立されている中大規模なリポジトリを知るたび、信じられないものを見たような気持ちになる。
TS は素晴らしい。JS の難しさを、型定義ファイルがすべて覆い尽くしてくれているからだ。出来の良い型定義がなされていると、公式サイトにあるドキュメントを参照せずとも使い方を推察できてしまう。
TS は(ほぼ)完全に実装を隠蔽している。そして、それを可能たらしめているのは、TS の型の表現力だろう。JS による実装を読まなければならない必要が生じることもあるが、それは型の表現力の問題というより、型定義ファイルないしパッケージの品質に問題があることがほとんどであるように思える。
話を PHP に戻そう。
8 系になってより豊かになってきているとはいえ、TS と比較すれば、PHP は型の表現力がかなり乏しい。ジェネリクスだって言語としてはサポートされていない。
(性質上、言語としてサポートされることはなく、Psalm や PHPStan がその役を担い拡張させていくことになるだろうが)
PHP で interface を記述したところで、コードとして実装に及ぼす影響は乏しい。よって実装を隠蔽するにあたっての不足は、PHPDoc で補わなければならない。
ルールの遵守をエンジニアの倫理観に委ねるより、コードとして遵守を強制できた方が堅牢である。
コードを変更したときコメントを変更し忘れて内容に齟齬が生じた、なんてことは、ほとんどのエンジニアが経験したであろう問題だ。もし型レベルで interface を詳細に定義できたならば、実装を破壊的に変更したとき、静的解析のエラーで検知できる範囲が広がるだろう。
実装を隠蔽するに足る PHPDoc は、書くのにも保守するのにもコストが生じる(他方で、エンジニアはモジュールの実装を見たり知ることについて、もとよりそれほど抵抗がない)。つまり PHP においては、interface の存在価値の1つである、実装の隠蔽を行う費用対効果が高くないのだ。
ちなみに。interface によって結合度を下げたり依存関係の整理をすると、変更による影響を限定でき、ビルド時に再コンパイルが必要なモジュールが減る、みたいなことが Clean Architecture に書いてあった気がする。インタプリタ言語である PHP は、そういう点でも interface の恩恵を感じにくいのかもしれない。
なぜ DP / DDD / CA を理解できないのか
TL;DR
- それらは最初に学ぶことではないから。
- 設計の原則、OOP の原則からまず学ぶべき。
- DP / DDD / CA を適切に実践できるのは、いくつかの前提条件が理想的に満たされているときのみであるから。
- ミスマッチがある現場では、一部を達成できない(ないし、実践することが目的の実現手段として適切ではなくなってしまっている)から。
- ただ DP / DDD / CA だけを学んでも、それらが解決したい問題を本質的に理解することが困難だから。
- もちろん、原典を通読すれば理解できる (難解かもしれないが...)。ここで言いたいのは、原典を読まずに一部を切り抜いた記事だけでは困難であることを伝えている。
思考の経緯
GoF をはじめとするデザインパターン (DP) や Domain-Driven Design (DDD), Clean Architecture (CA) といった比較的高次的な設計手法を使用すること自体を、目的としてしまっているひとが多すぎるのではないか。そう思う理由としては、これらに関する記事があまりに粗製濫造されすぎているためだ。なお DDD は開発手法も多く含んだ話なので、設計に関する部分、いわゆる軽量 DDD といわれる部分に話を限定する。
GoF 本を読んだり、DDD 本を読んだり、CA 本を読んだりして、半分でもいい、すんなり理解できたと感じたひとはどれだけ居ただろうか。自分には理解できなかった。
DDD / CA は前提知識を要求しているのでは?
DDD, CA は高次的な設計手法であって、暗黙的に前提知識を要求しているように思える(開発手法にも触れている DDD はそれなりの実務経験も)。
DDD 本 (2003) においては PoEAA 本 (2002) の概念が多く含まれている。Domain Model, Transaction Script, MVC, Value Object, Repository といった用語は(定義が一致しているかはさておき)PoEAA 本にもある。他には例えば Factory, Adapter, Composite などは GoF 本 (1995)でも、不変条件といった契約プログラミングは メイヤーの本 (1988)でも、Layers, MVC は POSA 本 (1996) でも語られている(参考文献にある通りなのだが)。CA 本 (2017) は、 Layers -> iDDD 本 (2013) にある Alistair Cockburn が提唱したヘキサゴナルアーキテクチャ -> Jeffrey Palermo が提唱したオニオンアーキテクチャ (2008) の流れを汲むものであり、さらに高次的であると見做すこともできる。
じゃあどうすればいいのか
参考文献として挙げられた主要な本を読む、ということをここでは結論にしたくない(読んだ方が良いのは間違いないだろうし、再帰的に文献を掘っていけば最終的には DDD も CA も理解できるようになるだろうが、そこまでの設計ガチ勢はたぶんそういない)。GoF 本自体が古く、現在から評価するとパターンそのものや分類が不適当だと判断されることもあるので、まず GoF 本を読んで理解しようとしても、少なくとも自分のような凡人には難しかった。
手段を知るのではなく、目的を知るべきなのだと自分は考える。より適切に言うならば、手段を用いたくなる原因だ。思考停止してビジネスロジックを実現するためだけにコーディングしているのならば、こういった手段を用いたいという発想に至らないはず。
そういうわけで、まず(OOP における)望ましいとされる原則を学ぶべきだと思う。手段を用いたくなる原因は、たいていのケースでは望ましいとされる原則に沿うことができないためだろう。既読で良かった本を雑多に挙げる。
DDD については、DDD 本の解説本である iDDD 本の解説本である「実践ドメイン駆動設計」から学ぶDDDの実装入門 や、ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本 あたりから読み進めたほうが理解が速いハズ。
気持ちはわかる
だって DP や DDD, CA ってカッコイイもん。
でも平々凡々な中学生がいきなり微積をやってもわからないように、基礎から積み上げないと、実のある理解に至れないように思う。世の中には DP / DDD / CA を懇切丁寧に詳説してくれたり、質疑応答までしてくれる神のようなひとがいるので、教科書通りの実装をすることはできるかもしれない。だが、それでも順序を違ってしまっているのならば、分かった気にしかなれないだろう、たとえ写経してもきっとそうだ。
DP / DDD / CA も教科書通りに実装しようとすると、要求仕様や言語仕様などにより、実現困難だったり不都合な方が側面が生じることが十分に考えられる。手段を目的としているのでは?問題解決のためではなく、ただ使いたいから使っているだけなのでは?教科書通りの DP / DDD / CA 以外の実現方法を知らないだけなのでは?自分もよく知らんけど。。。
リンク
https://qiita.com/ROPITAL/items/165bef33492ba27cfbf7 )
( 邦訳:良さげな記事だったので備忘録として
Factory Method / Abstract Factory の整理
TL;DR
GoF 本にある Factory Method / Abstract Factory は、実務ベースでは呼び分ける意味がないように思えるから、オブジェクトを生成する責務を持つクラスが Factory ということで良い気がする。
Factory の本質は生成の処理を分離することにあって、ポリモーフィズムが関係してくるのは実務上むしろ少ない印象。
ファクトリの具象に依存した処理
(設計まわりの話で、プログラミングに関係ないモノを例示として扱いたくはなかったが、他に良い表現が浮かばなかったので。例示が望ましく無く、共感を得られないかもしれない)
- 引数として
カレーの材料
を受け取る -
カレーの材料
を使ってカレー
をつくる -
カレー
を食べる - おしまい
とあるならば、2. を別のクラス、すなわちファクトリとして切り出す、というだけ。
- 引数として
カレーの材料
を受け取る -
カレーの材料
をカレーファクトリ
に渡す -
カレー
を受け取る -
カレー
を食べる - おしまい
メリットは カレー
を生成する処理への依存がなくなること。乱暴に言えばインスタンス化する ( new
する ) ところが カレーファクトリ
に移る。
たとえば カレー
のつくりかたに変更があったとき、前者なら カレー
をつくる全クラスに影響が及ぶが、後者ならば カレーファクトリ
の変更だけで済む(材料の変更があったら、そうとも限らないけれども)。関心が分離されているし DRY にも KISS にもなるような気がする。
ファクトリの抽象に依存した処理
- 引数として
カレーの材料
を受け取る -
カレーの材料
をカレーファクトリ
の抽象に渡す -
カレー
を受け取る -
カレー
を食べる - おしまい
たぶん カレーファクトリ
の具象は Dependency Injection (DI) で渡されるはず。メリットはファクトリの具象に依存していないため、より抽象化され、実装を隠蔽できる( カレーファクトリ
を生成する処理への依存については、依存しているファクトリが具象でも抽象でも DI で渡されるだろうので、この処理自体にはほぼそのへんの影響なさそう)。
また抽象に依存した副産物として、この処理はポリモーフィズムによって、マズいカレーをつくるときでも、ウマいカレーをつくるときでも使える。マズいカレーファクトリ
または ウマいカレーファクトリ
を inject してやればいい。この例だと受け取って食べるのは カレー
の抽象になる。
Factory Method vs Abstract Factory
GoF 本だと Factory Method と Abstract Factory という名前が与えられているが、正直よくわからない。
GoF 本によると、Factory Method には具象を extends する/抽象を implements する、2つの方法があると書かれている。Abstract Factory は『しばしば factory method によって実装されるが、Prototype パターンを使って実装することも可能である』と書かれている。
Factory Method はオブジェクトを生成するメソッド(具象・抽象を含む)のことであり、Abstract Factory はオブジェクトを生成するメソッドの抽象を持つクラスのことである、とも読める気がする。
そうだとして、オブジェクトを生成して返すメソッドを Factory Method だとあえて名前をつける必要もなければ( Factory クラスはオブジェクトの生成のみを行うべきであるため、したがって単に Factory クラスのメソッドとすれば良い)、
Interface や Abstract で全部ないし一部が抽象化されていたら Abstract Factory だとあえて名前をつける必要もない(抽象に依存すべきという原則に沿えば、Factory にも抽象を用意するのは自然であり、したがって単に Factory クラスの抽象とすれば良い)ように思う。いずれにせよ生成の処理が適切に分離されていれば良い。
Prototype よって実装される Abstract Factory というのも、奇妙というか、単に Prototype の抽象で良いと思う。イミュータブルに実装された Factory とは異なり、Prototype は clone 元となるミュータブルなオブジェクトを制御する振る舞いも必要なので、両者を同じインターフェースに統一するのも無理が生じるだろう。
Static Factory Method
余談となるが Effective Java では Static Factory Method パターンが紹介されている。オブジェクトを生成するメソッドを static に定義するというものだ。これは DI された Factory のメソッドをコールすれば良く、言語仕様的な問題がないのならば不要であろう。
思考の流れ
ポリモーフィズムしたいからファクトリの抽象に依存した処理にする、というのは発想の順序としては唐突な感じがする。理想論としては、すべてファクトリの抽象に依存した処理にすべきである。しかし現実的に考えると、全部のファクトリに抽象を用意するのは面倒だから、差し支えない範囲でファクトリの具象に依存した処理で妥協する。そのうち、ポリモーフィズムが必要な箇所が出てきたから、ファクトリの抽象に依存した処理にする、という流れがよくあるパターンかもしれない。
パフォーマンス要求を根拠とするオブジェクト生成のパターン
サマリ
-
Flyweight
- インスタンス化のコストが大きいとき/大量のインスタンスが必要なとき使用する
- Lazy Initialization した Flyweight と、Eager Initialization した Flyweight がありそう
- null ないしそれに準ずる状態で初期化しておく。コールされたとき、インスタンス化してプロパティにインスタンスをセットしつつ返す。次にコールされたときは、そのプロパティからインスタンスを返す ( Lazy Initialization )
- あらかじめインスタンス化してプロパティにセットしておく。コールされたとき、そのプロパティからインスタンスを返す ( Eager Initialization )
-
Object Pool
- インスタンス化のコストが大きいとき/大量のインスタンスが必要なとき/インスタンス化可能なオブジェクト総数に制限があるとき/インスタンス化を比較的早いタイミングでまとめて行いたいとき使用する
- あらかじめ所与の数だけインスタンス化してプロパティにセットしておく。コールされたとき、再利用可能だったらそれを、そうでなければ未使用のインスタンスから返していき、すべて使用済みになったら新しくインスタンス化してプロパティにセットしつつ返す ( つまり Eager Initialization )
-
Lazy Initialization ( Lazy Loading のうちの 1 種
- インスタンスを実際に使用するかという判断を遅らせたいとき/インスタンス化を比較的遅いタイミングでまとめて行いたいとき使用する
- null ないしそれに準ずる状態で初期化しておく。インスタンスの振る舞いがコールされたとき、インスタンス化してプロパティにインスタンスをセットしつつ振る舞いを返す。次に振る舞いがコールされたとき、そのプロパティのインスタンスから振る舞いを返す
雑感
動機
Factory Method / Abstract Factory は、設計 / OOP の望ましい原則を満たすためのデザインパターンであるとも言えるが、他方でパフォーマンス要求を満たすためのデザインパターンも存在する。オブジェクト生成という領域においては、Flyweight / Object Pool / Lazy Initialization パターンなどがある。
自分の持っている本では、Flyweight は GoF 本、Object Pool は『オブジェクト指向のこころ』、 Flyweight, Object Pool は Game Programming Patterns 、Lazy Initialization は PoEAA 本に解説がある。
イミュータブル/ミュータブル
性質上、すべてミュータブルであるはず。グローバルに影響させたいときや DI できないときなどは、イミュータブルにしたり Singleton で実装するかもしれない。
実装の隠蔽について
これらについては、実装を完全に隠蔽することが、必ずしも正しいとは限らない。
パフォーマンス要求を根拠としている以上、実装を考慮してコールしたほうが、よりパフォーマンスと向き合いやすいと思われるためである。抽象だけでパフォーマンス最大となるコールの仕方を表現できるのならば、完全に隠蔽したほうが望ましいだろう。
それぞれのパターンの区別について
名称 | 保持しておくインスタンスの数 | 生成タイミング | オブジェクトの再利用 |
---|---|---|---|
Flyweight | 任意 | 任意 | する |
Object Pool | 複数 (単数なら Eager Initialization した Flyweight とみなされそう) |
コールされるいくらか前 (インスタンス化したにも関わらず、一部が使用されないこともある) |
する |
Lazy Initialization | 任意だが大抵は単数 | 振る舞いがコールされたとき | 任意 (再利用したら Lazy Initialization した Flyweight とみなされそう) |
Object Pool は、あらかじめ所与の数だけインスタンス化しておくことに特徴がある。この所与の数というのは、メモリ空間だったりコネクションの数だったり、そういった外的な要因で決定されるだろう。
あらかじめ所与の数だけインスタンス化しておいた Flyweight は、つまり Object Pool なので、あえて Flyweight と呼ばれるときは、そうでないのだと思い至るのが自然か。
Lazy Initialization 自体は、オブジェクトのインスタンス化を遅らせて必要最低限にするもので、基本的にオブジェクトの再利用までは考慮されていないだろう。されているのならば、それは Flyweight とみなしたほうが自然であるかもしれない。
整理と反省
Flyweight, Object Pool と同じ目線?で Lazy Initialization を扱ったのは、ちょっと違ったような気もする
Object Pool, 使ったことないから正直よくわかってない。だから、上述した説明は正直微妙な気がする
Game Programming Patterns に詳しい説明があった。これをちゃんと読んで、また書き直した方が良さそう (TODO
...
Flyweight は生成したオブジェクトを破棄せず再利用するパターンに過ぎなく、そのうち所与の数を予め生成しておくものを Object Pool とする、というのが自分の認識。
Flyweight 自体は Eager Initialization してイミュータブルにしても Lazy Initialization してミュータブルにしても、Singleton にしても DI しても、その本質は損なわないように予想している。
...
Flyweight は Singleton として任意のクライアントから参照でき、イミュータブルにするのが望ましく、スレッドセーフである
Flyweight はイミュータブルであり、同じ状態を持つインスタンスを繰り返し生成するコストを避けるもの
JOIN に消極的な姿勢をとる実装について
PHP + MySQL でのバックエンドサーバを例として記述する。
まったく JOIN をしないとか、そう極端なことを考えたいわけではない。
メリット
- PHP の単体テストでカバーできる範囲が広がる
- SQL の動作を担保するためには MySQL になるべく分岐を網羅できるようなテストデータを突っ込むなりする結合テストが必要になってしまう
- 知らないだけで良いツールがあったりするのだろうか
- SQL の動作を担保するためには MySQL になるべく分岐を網羅できるようなテストデータを突っ込むなりする結合テストが必要になってしまう
- SQL ( ORM / クエリビルダ ) のレビューがしやすくなる
- 1つの処理あたりの記述量が減り、見通しやすくなるため
- MySQL を動かすサーバよりも PHP を動かすサーバの方がスケールしやすい(細かい制御を行える)ことが多い
デメリット
- 発行されるクエリの数が増える
- PHP の複雑さが増す
- とはいえ SQL の複雑さが増すよりはマシなように思える
- 生の SQL が多少複雑なのは読めるが、PHP で条件分岐するなどして動的に SQL を構築するとなると、しんどみが増す
- とはいえ SQL の複雑さが増すよりはマシなように思える
- MySQL に処理をさせた方が遥かに速いことが多い
- とはいえ JOIN は重い処理であることは確か
- この実行時間の差まで気を払わなければいけない案件は、そこまで多くない(?)
- テーブルの構造とドメインモデルの結合度が上がってしまうかもしれない(?)
- テーブルの構造から、それぞれの属性がマッピングされた(しばしば便利な振る舞いも持つ)オブジェクトを自動生成して、テーブルから fetch したら自然とそのオブジェクトが返るような実装を行うこともできる ( PoEAA でいう explicit interface な Record Set ) 。個人的な体感では、デメリットよりメリットのほうが多いような気がするが…?
雑感
- PHP で組み立てられた複雑な SQL を読みづらいと自分が思っているだけで、言語によってはそうでもない…?
- それなりに育った JOIN に消極的でないプロジェクトについて、消極的な姿勢に舵切りするのは大変そうな気がする…?
- 実行時間を短縮したいのならば RDBMS をやめて KVS とかにしたりキャッシュ戦略を見直すとか他にも手段があるのでは…?
- JOIN のコストを感じるくらい応答速度を求められる案件でないのならば、可読性や単体テストのしやすさをとって、アプリケーションサーバにロジックを寄せてみるのもありでは…?
リンク
備忘録として
設計とドメインロジックの実装に関するポエム
直接的に業務で必要とされるのは、ドメインロジックの実装である
設計まわりを熟考せずにドメインロジックを手早く実装できるひとと、設計まわりを熟考してしまいドメインロジックを実装するのが比較的遅いひとならば、前者のほうが尊ばれることが多いかもしれない。
設計まわりに執着することは、必ずしも良い作用の方が大きいとは限らない
たとえば Robert C.Martin の Clean Code, Clean Architecture を読んでその内容を十分に理解したひとと、そうでないひとがいる。
前者のひとは、書籍で良いと示された方針・感覚に沿って実装することを望むだろう。沿っていないコードに遭遇したときは、驚きが生じるかもしれない。また、沿っていないコードを書かざるを得ない状況に陥ったとき、ストレスを感じてしまうかもしれない。
そうだとしたならば、前者のひとはリファクタリングする土壌がない、複数人で開発を行なっているチームではパフォーマンスを発揮できない可能性がある。転じて言うと、上掲のような書籍の内容を活かすことができない現場というのはきっと確かに存在していて、そういった場所では前者のひとは、ややもすると書籍を読んでいないひとよりもストレスを多く感じるかもしれない。
一方で、後者のひとは、そういった先入観がない。どのような現場でも、書いてある通りのことを受け入れ、書いてある通りに実装を続けるだろう。良い設計を維持しているチームにアサインされたならば、その人は良い設計を維持するコードを書くかもしれない。
設計のことを考えるのは、ドメインロジックを手早く実装できるようになってから
考えられた設計の上にあるからこそドメインロジックを手早く実装できるようにも思えるが、考えられた設計というのはドメインロジックを手早く実装するための一要素に過ぎない。
すでに動いているコードがあり、リファクタリングのフェーズでないのならば、もっとも関心を抱くべきことは、どのようにすればより良い設計になるかということではない。ドメインロジックを理解することである。理解するのに困難であるようであれば、理解しやすいように整理する。
既存のコードについてどうすれば良い設計になるかをひたすら話しているひとより、既存のドメインロジックについてひたすら整理して理解の助けになってくれるひとのほうが、必要とされるケースが多いように思われる。
可読性・保守性・堅牢性
考えられた設計によるメリットとして可読性、保守性、堅牢性があるとする。
可読性について、メンバーの移動がなく、ずっと同じメンバーがアサインされているのならば、問題は表面化しづらいかもしれない。メンバーの移動がないなら、良かれ悪かれ画一的な記述になっているだろうので、慣れてさえしてしまえば、それなりに読めるようになると考えられる。
保守性について、保守性が低いコードを書き続けることにより、実装するとき余計な時間がじわじわ増えていくことが考えられる。ただこれは、メンバーの熟練によって相殺されてしまったり、何らかの指標を継続的に記録していないためそもそも観測できず、やはり問題は表面化しづらいかもしれない。
堅牢性について、依存関係が整理されていない/広いスコープの状態が多い/低凝集・高結合なコードは、変更が予期しない部分により波及するかもしれない。これは回帰テストや網羅的な QA で埋めることもできるだろうが、予期しない波及が増えるぶん取りこぼす可能性も増す。しかし、それによって発生したインシデントが、設計によるものだと結論づけられることは考えづらいように思われる。
設計は天才の必要条件ではない
ソフトウェアは、そのソフトウェアの開発者と、そのソフトウェアを利用者とを比較すると、後者が圧倒的に多い。ソフトウェアの目的は後者に価値を提供することであって、前者の利便性は比較してしまうと副次的なものになってしまう。
どんな酷い実装であろうと、利用者にとって天才と評価するに足るソフトウェアを開発したひとは、天才と評価されるだろう。天才というのは言い過ぎにしても、もっと小さいコミュニティにおける"評価されるエンジニア"というレベルでも、同じことが言えるはず。
設計本のある側面における分類について
設計に関する技術書は、参考文献として挙げられている書籍を理解していれば読む意味が薄れるものと、そうでないものがある、と分類できるとする。言い方を変えると、参考文献の内容を取捨選択して理解しやすく再翻訳しているものと、参考文献の内容から更に発展した著者の主張を交えたものがある。(あえてあやふやな言葉を使うが)実践書は両者の中間にあるのかもしれない。
いわゆる入門書は概ね前者であるべきで、入門書を読み終えたあと、興味がある分野について参考文献に当たりさらに理解を深める。。。という側面はまさに入門である。視点を変えると、入門書で語られている用語やその概念は、概ね参考文献に基づきそれを逸脱しない範囲で、再翻訳として理解しやすくなるよう努めるべきである。参考文献として挙げられている ( ないし挙げられているべき ) 書籍を理解している人が、その入門書を読んだとき、首をかしげるような内容であるならば、それは良著とは言い難いだろう。もし入門書(記事)を書く側にまわったとき、理解が浅いと思われる用語に出会ったら、面倒でも逐一立ち止まり出典を探し正しい理解を求めるか、いっそその用語を使う解説は諦めたほうが誠意があるように思われる。もし用語を造りたくなったときは、それはその意味に当てはまる用語を知らないためであるか、あるいは著者の主張になるため( 入門書として知識が比較的少ない読者を想定しているのならば特に )十分な考慮が必要になるはずである。
記事に載せるコードブロックを完璧にしたいとき、すべきこと
- その言語の LTS のうち記述時点で最も新しいバージョンを使用する
- 必要に応じてパッケージを使用しても良い
- そのパッケージは広く使われているもの、LTS のうち記述時点で最も新しいバージョンを使用する
- 以下にあげられているものであれば、テストするパッケージ、DI するパッケージ、静的解析するパッケージがある
- 単体テストが通るように書く
- そのコードブロックの実行を制御するクラスは、テストクラスにする
- そのコードブロックで返り値に意味があるのならば、テストクラスでアサーションする
- そのテストが通るようにする
- 必要ならば DI する
- linter のような構文を整形するツールを通す
- 空の宣言や空行などは、任意に省略しても良いかもしれない
- psalm / phpstan のようなコードの質を指摘するツールを通す
- プレイグラウンドに飛ぶリンクと、github で clone して docker からテストしばけば動くやつ、2 パターン用意しておく