システム開発するときの難しさについて

難しさとは

適当に動くだけなら簡単かもしれないが利用者がいて利害が発生するようなシステム開発は本当に難しい。

システム開発で開発者が主に実装というフェーズで気にすることというとざっと挙げてみて要件定義、アーキテクチャ選定、データ設計、プログラミングというのがあり、そのどれもが一様に難しい。

なぜ難しいか。人間は自分の都合しか考えないし関心のあるところにしか注意が向かないので異様に複雑な文脈が絡み合うので、そのような要件を記述するとなると並大抵のことではスムーズにいかない。

詰まるところシステム開発の難しさは端的に境界が曖昧なことに起因すると思う。

自分がそういう考えなので、なにか機能追加や改修が必要となったとき誰かが安易に「簡単にできます」とかいうのを聞くとものすごく身構えてしまう。

要件というのは代替がきかない本質の部分であってそれ以外のプログラミングの技術やらアーキテクチャの理解度やらは代替のきく表層の部分だと思っているし、実際この感覚はそこまで世間とずれていないから一般的にドメイン駆動開発やらクリーンアーキテクチャなどと名前をつけて日々論じられてるんだろうなと思っている。

この難しさを少しでも薄めるにはどうしたらいいのだろうというのをぼんやり考えてみる。

要件の文脈とは

要件というものは必ず誰か何かなぜしたいという文脈を伴っている。

そしてこれはたびたび開発者が「再利用性」といった免罪符を使って文脈を無視して似たような振る舞いを共通化しようとした時点でいらぬ複雑さを生むことがある。

ものの本にこのようなことが書かれている。

Be careful. Resist the temptation to commit the sin of knee-jerk elimination of duplications. Make sure the duplication is real.

要件の文脈を無視して何かしたいかだけに注目することによって文脈の境界が曖昧なまま入り組んでしまって面倒なことになる。

引用元の書籍はまさにこの点についての問題意識や考え方を提示してくれるものなので一読の価値がある。まあ有名すぎる本なので何を今更という感じだけど。

難しさを薄めるには

具体的な方法の一つとして結論からいうと、プログラミングの手法としては型の定義をすることで一役を担うと思っている。

たとえば「支払いをする」という要件があったとき、暗黙の文脈があるのでそれをはっきりさせなくてはいけない。

経理部が金融機関に支払う」(以下ケースA)というのと「エンドユーザーが弊社に支払う」(ケースB)では同じ支払いという動作でも全く意味が違う。

これをものすごく恣意的に例示してみるとこのようになる。

charge :: String -> String -> Maybe Int -> Int -> ()
charge "AccountingDep" "BankName" (Just accountNumber) amount = accountingToBank(accountNumber,amount)
charge enduserName "OurService" Nothing amount = accountingToUs(enduserName,amount)
chaarge _ _ _ = undefined

chargeという関数で見た目上は共通化しているが、実際は場合分けが発生しており文脈の判断を引数の組み合わせで行わなくてはいけない。 その上プリミティブな値を用いているため過度に汎用的で各文脈におけるそれぞれのパラメーターについての制約が表現できていない。

そもそもケースAとケースBは実装する機能としてそれぞれ端的に要件を表すことができるはずで、ここではそれをしないが関係者間で意味が通じればいいのでそのままCaseAとCaseBというエイリアスを用いる。

Clean Architecture はそれらの問題に対する一つの対処法を提示しているのだが、それをふまえると以下のように改善できる。

newtype Amount = Amount { getAmount :: Int }
newtype AccountNumber = { getAccountNumber :: Int }
data OurAccountingDep = OurAccountingDep
newtype Bank = Bank { name :: String,accountNumber :: AccountNumber }
newtype EndUser = EndUser { name :: String }
newtype OurAccount = { accountNumber :: AccountNumber }

class Validate a where
  validate :: a -> Bool

instance Validate Bank where
  validate a = not . null $ name a

chargeOnCaseA :: Bank -> Amount -> ()
chargeOnCaseA bank amount = ...

chargeOnCaseB :: EndUser -> Amount -> ()
chargeOnCaseB endUser amount = ...

場合分けが必要なくなり文脈が明確になるというのと、テストデータを考えたときに専用の型を定義していることで定義域がはっきりすることがわかる。

ただ、これを面倒だと感じることもあるだろう。

面倒くささとは

Clean Architectureで言うところのEntityやValueObjectを実現するためのデータ型定義は必要だからやるのであって決して面倒なのではない。

これを面倒だと言い出すと極端な話としてスカラ値と素朴なコレクション、そして申し訳程度のクラス定義だけでよくなる。これは極端に言ってるが往々にして本質としてこのようなことが割と受け入れられている現状はあるとは思う。

面倒だというのは厳密な文脈の境界を定義することを後回しにし、神テーブルよろしく神クラスがあることでSOLID原則もクソもないごった煮のデータ構造を相手にすることを意味している。

これがメンテやCI/CDが不要というような書き捨てスクリプトだったりプロトタイプなら話は別で面倒ではあるが、複数人開発の保守管理前提大規模開発だったら必要な配慮であると思っている。

そのような状況で心強いのはコンパイラの存在であって表面的な面倒くささはコンパイラによってだいぶ軽減される。 むしろコンパイラは不確定な人間の判断と違って常に同じロジックで動くので客観的に見たときの信頼度が段違いで、そういう観点では積極的にコンパイラのある言語を採用したいところだ。

ただ、プロトタイプを作ってそのコードベースを参考にそのままプロトタイプの延長でコードを改修し続けているような場合、一度コードベースと現状のビジネス要件を照らし合わせて再評価しなくてはいけない。これってめちゃくちゃ面倒かもしれないが、必要なことで、このセクション冒頭で述べたことと同じようなことである。

そもそも実行時エラーでしか検知できないというのは精神衛生上よくないし、持続可能な開発とは言えない。

Shell scriptで大規模なwebアプリケーションを作りたいと思う人がどれだけいるだろうか。おそらく大体の人が抵抗感を感じより言語仕様が高級な技術選定をすると思う。Shell script自体優劣を主張するものではなく適材適所を考えたときに大規模なwebアプリケーションを作るにあたってより適切な道具があるはずだということを言いたい。

大規模なwebアプリケーションに加えて複数人開発でCI/CDを前提とする場合、考慮することがさらに増えるということだし、となれば機械的なチェックや定性的なチェックは自動化したいところだろう。

どうして表面的な実装コストは気にするのに持続可能性は軽視できるのか、そういう局面に立ってしまう場合はよくよく考えたいところだ。

やらない理由が面倒だからという安直なもののみの場合、明らかにその判断はおかしいと警戒したほうがいい。

結局なんなのか

実際のところシステム開発はプログラミングスキルや便利ツールでどうにかなるものではなく、間違いを起こすし病気にもなるような不安定要素の塊である人間がつくるものなので組織の問題だったり外部環境だったり様々な要素が難しさとして表出してくる。

ただ、自分は今のところシステム開発と御用聞きでおマンマを食べているので健康を維持して安眠するためにもシステム開発に関わる相対的な難しさをどうにか減らしたいと思っている。

そうした場合実は技術選定でどうにかなる部分があるんだろうなと思ってもいて、多分それはぼんやりした難しさ全体の2%くらいなんだけどまあどうにかしたいなという気持ちしかない。

そしてmartin fowlerが言っていたように我々が考える品質というものが何から成り立っているのか忘れないようにしたい。

おわり