序論
プログラミングの手法や設計について様々あるものの多すぎます。
それらは抽象度がバラバラだったり重要度が付けられていません。
これらが説明されていないために悲劇的コードが生まれてしまいます。
そもそも論、どのような思考で設計すればよいのでしょうか。
プログラミングの本当のプリンシプルとは何でしょうか。
これを私の経験をもとにまとめました。
また、以下を前提とした内容としています。
- アプリケーション作成の文脈でのプログラミングについて書いてます
- フロントエンドエンジニアに不足しがちな思考に重点をおいて書いてます
- 私はコンピュータサイエンスに明るくありません
アプリケーション作成と設計の原則
設計の必要性
なぜ設計が必要なのでしょうか。
設計がなくてもアプリケーションを構築することはできます。
しかし設計のないアプリケーションは複雑になります。
すると高度なアプリケーションを作り上げることはできません。
設計は アプリケーションの複雑性を下げるため にするのです。
便利にするため、シンプルにするため、簡単に開発できるようにするため、高速に開発するため、保守性を上げるため、可用性を上げるため、拡張性を上げるため、属人性をなくすため・・・他にもいろいろあるかもしれませんが、どれもプリンシプルではありません。
アプリケーションの構成要素
全てのアプリケーションは 状態・表示・副作用 の3つからなります。
副作用って言葉を使うとピンとくる人が少なくなると思うので、
要するに以下の3つのことだと思ってください。
- 状態
- 状態の表示
- 状態の変更
例えばWebアプリケーションでいえば データベース(状態)
出力したhtml(状態の表示)
データベースを書き換えるサーバ処理(状態の変更)
の3つになり、Reactでいえば state(状態)
jsx(状態の表示)
setStateの実行(状態の変更)
の3つになります。
ただこれは大雑把に言ってるだけで、インプットになるものはなんでも状態と考えておきます。cookieも状態ですし、フロントエンドにとってはURLも状態です。とにかく無理矢理この3つに当てはめて思考することで設計を始めることができます。
アプリケーションの構築方法
全てのアプリケーション作成の基本的な所作は 処理を分割して組み合わせる ことです。
当たり前だと思いますが、当たり前ができない人が意外と多いのです。
- パーツに分離する
- パーツにインターフェース(API)を用意する
- パーツとパーツを組み合わせる
ここでいう パーツ はいろいろなものを含みます
- 関数
- クラス
- モジュール
- コンポーネント
- ページ
- ライブラリ
- パッケージ
- レイヤー
- マイクロサービス
- アプリケーション
どれも分離してインターフェースを決めて組み合わせます。
それぞれの中で細かい話は無限にありますが、どのレベルであってもやることは変わりません。
単体の関数でアプリケーションが成立することはなく、
単体のアプリケーションでもアプリケーションが成立することはありません。
アプリケーション作成の原則まとめ
まとめると
状態・表示・副作用 の3つに分解して整理し、
それらをパーツ化してAPIを定義して組み合わせ、
これをうまくやることでアプリケーションの複雑性を下げること
これが設計です。
そしてこれができていないとつらいアプリケーションが出来上がってしまいます。
いくらリーダブルコードやドメイン駆動設計を読んでも活かせません。
具体的に大事な法則
プログラミングの法則やテクニックをまとめた本としては、以下の名著があります
プリンシプル オブ プログラミング3年目までに身につけたい一生役立つ101の原理原則
ただ初心者の人が読んでもうまく活かすことができないと思います。
アプリケーションの複雑性を下げること に重点をおいて抽出しました。
とにかく他の法則よりもこれが重要です。
YAGNIとかSOLIDとかDRYなどは傍に置いてください。
一般的なプログラミング法則
・関心の分離
パーツをうまく分離するには、関心ごとに分離することが重要です。
・参照透過性
状態・表示・副作用 がうまく分離されているには、参照透過性があることが重要です。
・名前重要
名前をつけるには適切な抽象化と分離ができる必要があり、名前が適切につけられているかどうかは重要です。
・テスト容易性
テストを容易にするには、パーツが適切に分離されている必要があり、テストしやすいコードは結果的によい設計といえます。
・KISSの原則
うまくやろうとすると複雑になりがちで、しかもそれが設計に関係することはほとんどありません。
うまくやる前に、瑣末な処理はシンプルに愚鈍にしておいて、根本的な設計を考えることがより重要です。
独自のプログラミング経験則
その他にも独自の経験則をまとめました。
他の法則にもよく書いてある内容ですが、
適切に伝えたいことを伝えるために独自にまとめました。
・条件分岐の宣言化
宣言型プログラミングをしようとすると自然となると思うのですが抽象度が高すぎるので抽象度を下げてうまく言おうとするとこうなります。
ありがちなのはif文を関数に分離してしまうパターンです
例えば以下のようなコードがあったとします
function(arg1, arg2, arg3) {
if (arg1) {
if (arg2) {
処理(1);
} else {
if (arg3) {
処理(2);
}
}
} else {
if (arg3) {
処理(3);
}
}
};
これを関数に分離してこのようにしてしまうのはアンチパターンです。
見た目上のネストが減っただけで、複雑性はより増しています。
function(arg1, arg2, arg3) {
if (arg1) {
bar(arg2, arg3);
} else {
baz(arg3);
}
};
事前に条件分岐を整理し、ifを減らすことで複雑性が下がります
function(state) {
if (state === 'foo') {
処理(1);
}
if (state === 'bar') {
処理(2);
}
if (state === 'foo') {
処理(3);
}
};
この例では関数ですが、どのレベルのパーツであっても同様の考え方が必要です。
状態と処理は分離し、できるだけ宣言的にプログラミングしましょう。
・リソースの単一化
これは状態を一箇所にまとめましょうということと、
状態の参照先は1つにしましょうということの両方です。
関数ベースだとほとんど起きないと思いますが、クラスベースのコードで起きがちです。
・インターフェースの最小化
たとえば関数でいえば引数を富豪的に渡すのはやめて、必要なものだけ渡しつつ、
そもそも引数に必要なものが少なくなるようにしましょうということです。
また、外部のことを知ることがないインターフェースを作りましょう。
・副作用の非循環
ある副作用が別の副作用を起こすことがあります。
データをfetchする → 状態が変わる → 状態の変化をもとに別の副作用を起こす
これはあまりやらないようにすることが大前提ですが、場合によっては必要です。
時に循環してしまうことがあり、これは絶対に陥ってはいけないパターンです。
データをfetchする → 状態が変わる → 状態の変化をもとに別の再度データをfetchする
このパターンは循環するような副作用のつながりが起きており、無限ループを起こします。
そんなバカなことはしない思うかもしれませんが、オブジェクト指向ベースでイベントのやりとりする設計になっていたり、
Reactでいえばhooksのexhaustive depsを元に副作用を起こすようなコードになっていると起きがちです。
状態・表示・副作用 の3つを整理し、 副作用は1回 で済むようにしましょう。
最後に
前提すぎて当たり前な重要なことをまとめました。
しかしながら教えるのが難しく、とはいえこれさえなんとかなっていれば後から修正が可能という内容です。
今回の内容を初学者の人にもうまく伝授するよい方法があれば教えてください。