😸

IT・プログラミング分野で出会った原理原則まとめ

2023/02/15に公開

IT分野にかかわる中で出会った原理・原則をまとめた自分用のメモです。随時更新中。

  • 一言でまとめるとこういうこと
  • 詳しい説明

のフォーマットで書いています。

プログラミング系

DRY原則

一言でまとめると

同一コードの繰り返しを避けましょう。ただし、扱っている概念が同じなら。

詳細

「Don't Repeat Youself.」の略。Andy HuntとDave Thomasが「達人プログラマー―システム開発の職人から名匠への道」で提唱したのが、はじまりです。

同じ内容が記載された重複コードは、変更時にその数だけ同じ修正をなんどもすることになり大変。つまり、メンテナンス性を悪化させます。

1度の修正で変更を完結させられるよう、抽象化や共通化をもちいて、重複コードを極力除きましょう、というのがこの原則の意味するところです。

ただし、なんでもかんでも共通化すればよい、というわけではないのに要注意。見かけは同じでも、扱っている概念が異なる(=コードの責務が異なる)のであれば共通化はすべきではありません。

例えば、極端な話

  • カスタマーに一意に振られたユーザーID
  • データベース上のデータに一意に自動採番されるID

という全く異なる2つの概念が、たまたまどちらも「16桁の文字列」という制約を持っていたとして、共通の validate() 関数を用意するのはアンチパターンです。

明らかに扱っている概念が異なりますし、おそらくコードの書かれているレイヤーも全く別。この状況で共通化を図るのは、かえって可読性、メンテナンス性を損なうのでNGです。

YAGNI原則

一言でまとめると

必要なときがくるまで、余計なコードを書くのはやめよう。

詳細

「You ain't gonna need it」の略。エクストリーム・プログラミングの考え方の1つ。

「今はいらないけど、たぶんあとで必要になるだろう」の考えで書かれたコードは、大半が結局使われません。

となると、

  1. そのコードを書く時間
  2. あとからそのコードを読む時間

のどちらも無駄でしかありません。使える時間は限られているので、極力無駄は省きたいですよね。

なので、コードは本当に必要になってから書きましょう、代わりにやっておくべきことは、いつでもコードを追加できるように、シンプルで可読性の高い実装をキープすることです。

KISSの原則

一言でまとめると

「1番シンプルな」コードを書こう。

詳細

「Keep it simple, stupid.」の略。

複雑さはそれ自体がリスクです。読み解くのに時間がかかり、ひどいときには誤解を招くことになりかねません。

同じ目的を達成できる1番シンプルなコードを書き続けて、誰が見てもすぐにわかる状態をキープしましょう。

単一責任の原則

一言でまとめると

1つのクラスは、1つの関心事しか扱っちゃだめ!

詳細

SOLID原則の S に相当。

クラスには「1つの関心事を、正常動作させる責任」を持たせます。

例えば、

class Amount {
    // regular: 定価
    // discount: 割引価格
    constructor(mode:'regular'|'discount'){}

    getPrice(){
        switch(mode){
            case 'regular':
                // ...
                return regularPrice
            case 'discount':
                // ...
                return discountPrice
        }
    }
}

のような状態は、単一責任でありません。明らかに定価と割引価格という2つの関心事を扱っています。

また、「XXXXがYYYYのときのZZZZクラスは、AAAAを表す」のように形容詞的な説明が頭にくっついて初めて意味をなす場合は、その単位でclassを分割すると単一責任にしやすくなります。

今回の例だと、定価と割引価格でそれぞれクラスを独立させるとよいです。

単一責任原則違反がいくところまでいった状態が、神クラスです。

設計要件をギッチギチに詰めたValueObjectで低凝集クラスを爆殺する

オープン・クローズドの原則

一言でまとめると

新しい仕様の追加に、既存コードの変更でなく、コード追加で対応できるようにしよう。

詳細

SOLID原則の O に相当。

今後機能や種類が追加された際に、既存のコードを「変更」するのではなく「追加」することで対応できる設計を心がけましょう。

例えば、何種類かあるパターンごとに少しずつ計算ロジックを変えたいようなケースで、switch文を使うとパターンが増えた際に変更がマストになります。

// 敵の最大ヒットポイントを難易度別の計算をかけて返す
type Difficulty = "EASY" | "NORMAL" | "HARD"

const calcMaxHitPoint = (type: Difficulty) => {
    const standardHitPoint = 1000
    switch(type){
        case EASY:
            return standardHitPoint * 10
        case NORMAL:
            return standardHitPoint * 20
        case HARD:
            return standardHitPoint * 30
        // 例えば VERY_HARDを実装しようと思ったら、ここに変更を加えないといけない
    }
}

一方、interfaceを使って実装すれば、パターンが増えても計算ロジックの変更は不要で、interfaceを実装した新規classを追加するだけで済みます。

interface Difficulty{
    HitPointCoefficient: number
}

class EASY implements Difficulty{
    HitPointCoefficient = 10
}

class NORMAL implements Difficulty{
    HitPointCoefficient = 20
}

// VERY_HARD実装時はclass追加のみ
// class VERY_HARD implements Difficulty{
//     HitPointCoefficient = 40
// }

const calcMaxHitPoint = (difficulty: Difficulty) => {
    const standardHitPoint = 1000
    return standardHitPoint * difficulty.HitPointCoefficient
}

ただし、やりすぎるとclassが氾濫してかえって保守性が落ちるため、どこまでやるかは都度見極めましょう。

SOLID原則 ◆オープン・クローズドの原則◆

リスコフの置換原則

一言でまとめると

抽象クラスやインターフェースを実装するなら、実装元をみてわからない実装を入れ込んではいけない

詳細

SOLID原則の L に相当。

継承・実装していい場合、だめな場合を示した原則です。

荒くまとめるなら、実装クラスをもとのベースクラスに置き換えても、エラーが起きないならOK、です。

もう少し詳しくいうと、

  • 引数, 入力にベースクラス以上の制約をつけない
  • 戻り値, 出力は、ベースクラスと同じかより厳しい制約をつける

等々を守る必要があります。

違反すると、

  • 常にベースクラスと実装クラス両方を気にしなくてはいけなくなる
  • オープンクローズドの原則を守りづらくなる

など、保守性を落とす原因となるため気をつけましょう。

よくわかるSOLID原則3: L(リスコフの置換原則)

リスコフの置換原則(LSP)をしっかり理解する

インターフェイス分離の原則

一言でまとめると

使わないメソッドやプロパティをインターフェイスに持たせない

詳細

SOLID原則の I に相当。

バグのもとになるので、インターフェイスには本当に使うメソッドやプロパティだけを持たせようという原則です。

逆にいえば、なんでもできるインターフェイスは作ってはだめ、ということです。
責務に応じて、細かくインターフェイスを分割しましょう。

だめな例:

interface Animal {
    name: string;
    run: ()=>{};
    swim: ()=>{};
}

class Salmon implements Animal{
    name = "鮭"

    // なぜか鮭が走れる
    const run = // ...

    const swim = // ...
}

よい例:

interface Fish {
    name: string;
    swim: ()=>{};
}

class Salmon implements Animal{
    name = "鮭"

    const swim = // ...
}

依存性逆転の原則

一言でまとめると

ツールやインフラに依存するのはNG。抽象に依存して、かんたんに変えられるようにしよう。

詳細

SOLID原則の D に相当。

  • 上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべき
  • 「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべき

という原則。

抽象に依存させることで、2つのモジュールが疎結合になり、片方の変更時にもう片方まで変える必要がなくなります。

また、切り替えが容易になり、ユニットテストの書きやすさが飛躍的に上がります。

オニオンアーキテクチャ etc の保守性の高いアーキテクチャで頻出の実装パターンです。

だめな例:

class UserFactory {
    const getUser = () => {
        // SpecificDBClientに密結合
        // テストしづらく、ツールの変更にも弱い
        const client = new SpecificDBClient()
        const user = client.getItem("id")
        return user
    }
}

よい例:

interface DBClientInterface {
    getItem: (id: string) => User
}

class UserFactory {
    // 抽象に依存させる
    // Clientの切り替えが容易
    constructor(private readonly client: DBClientInterface){}

    const getUser = () => {
        const user = client.getItem("id")
        return user
    }
}

驚き最小の原則

一言でまとめると

命名も実装も設計も「それはないだろ」と他の人を驚かせることをするのはやめましょう

詳細

こちらの記事のアンチパターン集が、めちゃめちゃわかりやすいです。これは驚く。

あっと驚かせるJavaプログラミング(をやめよう)

コンウェイの法則

一言でまとめると

システムのアーキテクチャは、組織構造を反映したものになる

詳細

こちらが図付きでわかりやすいです。

コンウェイの法則と逆コンウェイの法則から組織構造を考える

早すぎる最適化

一言でまとめると

パフォーマンスは「測定してから」最適化しよう

詳細

ここでクラス生成すると遅くなるのでは?のような、「こうやったら早くなるのでは」的な考えは、コーディングをしていると常々湧いてくるものの当たっていない場合も多いです。

まずはコードを動くコードを書いて、問題が発生してから実測して考えましょう。

Discussion