Closed10

Clean Code メモ

ShionShion

関数型プログラミングとオブジェクト指向型プログラミング

関数型プログラミングとは

  • 関数ベースでコードを書く
  • 純粋な関数を使う(同じ入力なら必ず同じ出力を返す)
  • 副作用がない(データを直接変更することはしない)

オブジェクト指向プログラミングとは

  • クラスベースでコードを書く
  • データの状態と処理をクラスにまとめる
ShionShion

命名チェック

  • 関数(or変数)の内容を正確に表したものか
  • 明確か
  • 簡潔か(簡潔すぎてもダメだから塩梅がむずいけど)
  • 追加の情報を与えない単語が含まれていないか
    • XXXData
    • XXXInfo

変数編

真偽値

  • isXXX
  • 過去分詞系(completed, finished)

定数

  • 参照として用いる定数ならアッパースネークケース(THIS_IS_CONST)
  • オブジェクトのインスタンスや値を格納しておくためのオブジェクトはキャメルケース(thisIsConst)

関数編

真偽地を返す関数名

  • isXXX
  • hasXXX
  • canXXX
  • needsXXX
  • shouldXXX
  • existsXXX (exist は自動詞なのでisはつけない)

おまけ

基本的に略語は避けた方がいいと思っているが、業界的にみんなが知っているような略語は使用可。

  • a11y (Accessibility)
  • i18n (internationalization)
ShionShion

コメントについて

前提として、コメントは書かないで良いようにコードを書いていく。とはいってもコメントが必要になる場面はどうしても訪れる。その際は、「このコメントは読み手に付加情報を与える有益なコメントになっているか?」と自問しつつコメントするようにする。

コメントが必要になるのは次のようなケース。

  • コードでうまく表現できない場合に、それを補う
  • 読み手に意図や考えを伝える
  • 覚え書きや注意を促す

ちなみに、コメントはアノテーションをつけるとわかりやすい。

TODO:	あとで追加、修正するべき機能がある。
FIXME:	既知の不具合があるコード。修正が必要。
HACK:	あまりきれいじゃないコード。リファクタリングが必要。
XXX:	危険!動くけどなぜうごくかわからない。
REVIEW:	意図した通りに動くか、見直す必要がある。
OPTIMIZE:	無駄が多く、ボトルネックになっている。
CHANGED:	コードをどのように変更したか。
NOTE:	なぜ、こうなったという情報を残す。
WARNING:	注意が必要。

参考:https://qiita.com/taka-kawa/items/673716d77795c937d422

ShionShion

変数について

マジックナンバーやマジックキャラクタは避ける

文字通り。定数でおくと良い。

変数名と異なる情報は入れない

複数意味が含まれる場合変数を分けることなどを検討する。

分割代入で値を明示する

before

const skills = ["JavaScript", "TypeScript", "Ruby", "Go"];
const firstSkill = skills.shift();
const otherSkills = skills;

after

const skills = ["JavaScript", "TypeScript", "Ruby", "Go"];
const [firstSkill, ...otherSkills] = skills;

変数の寿命を短くする

変数の寿命とは、変数を宣言してから使用するまでの行数のこと。
寿命が長いとコードが追いづらくなる。
Bad

const currentUser = fetchLoginUser();
// ==========================
// 10行の処理
//===========================
if (currentUser.age >= 20) {
  // 20歳以上のユーザーに対する処理
}

Good

// ===================
// 10行の処理
// ===================
const currentUser = fetchLoginUser();

if (currentUser.age >= 20) {
  // 20歳以上のユーザーに対する処理
}

なるべく小さなスコープで変数を定義する

ある変数が関数内でしか参照していないなら、それは関数内で定義するべきである。
なぜならスコープが大きくなればなるほど見なければならない範囲が大きくなるからである。

ShionShion

関数

なるべく小さく、1つの責務に

原則として関数はなるべく小さくし、一つの責務のみ行うことに注意する。
関数を書く際は「この関数の責務は何か?」「複数の責務を持っていないか?」を自問自答すると良い。

DRY原則

さらにDRY原則を意識するとさらに良い。
ただし、DRY原則を適用しない方が良いケースも存在する。
DRY前

// 税込み金額
function getTotalWithTAX(amount) {
  const TAX = 0.1;
  return amount + amount * TAX;
}

// 支払い違反金
function getTotalWithPenalties(amount) {
  const PENALTY = 0.1;
  return amount + amount * PENALTY;
}

DRY適用後

function getTotalWithExtraPercentage(amount, extraPercentage) {
  return amount + amount * extraPercentage;
}
// 税込み金額
getTotalWithExtraPercentage(1000, 0.1);
// 支払い違反金
getTotalWithExtraPercentage(1000, 0.1);

一見良さそうに見えるが、支払い違反金の計算方法が変わった場合(例えばパーセントの他に金額自体を追加することが可能になったなど)にこの関数は次のように変更する必要が出てくる。

function getTotalWithExtraPercentage(amount, extra, isPercent = true) {
  let sum;
  if (isPercent) {
    sum = amount + amount * extra;
  } else {
    sum = amount + extra;
  }
  return sum;
}

ただ、ここで注意したいのが税込み金額の計算はsum = amount + amount * extraのみで行われるため、else ブロックの方は使用されない。そのため、税込み金額の計算としては else ブロック内のコードは冗長なものになる。
このようにして、関数が目的毎に分けられていない場合には、無駄な条件分岐等が追加され見づらいコードになってしまう(スパゲッティコード)。

上記を踏まえ、DRY を適用させる前に、一度目的が同じかどうかを考え、目的が同じの場合のみ適用するように注意するようにするとよい。

関数にフラグは渡さない

条件分岐は関数の処理を複雑にするため、なるべく関数にフラグを渡す記述は避けるようにする。
Bad

async function getUser({ userId, isAdmin = false, isAllUser = false }) {
  // 全てのユーザーを取得
  if (isAllUser) {
    const response = await fetch("/users");
    const result = await response.json();
    return result;
  }
  if (isAdmin) {
    // 管理者を取得
    const body = {
      userId,
      secretKey: process.env.SECRET_KEY,
    };
    const response = await fetch("/admin-users", {
      method: "POST",
      body: JSON.stringify(body),
    });
    const result = await response.json();
    return result;
  } else {
    // 一般ユーザーを取得
    const body = {
      userId,
    };
    const response = await fetch("/users", {
      method: "POST",
      body: JSON.stringify(body),
    });
    const result = await response.json();
    return result;
  }
}

Good

async function getAdminById(userId) {
  const body = {
    userId,
    secretKey: process.env.SECRET_KEY,
  };
  const response = await fetch("/admin-users", {
    body: JSON.stringify(body),
  });
  const result = await response.json();
  return result;
}

async function getUserById(userId) {
  const body = {
    userId,
  };
  const response = await fetch("/users", {
    body: JSON.stringify(body),
  });
  const result = await response.json();
  return result;
}

async function getAllUsers() {
  const response = await fetch("/users");
  const result = await response.json();
  return result;
}

// 呼び出し側コード
if (isAdmin) {
  getAdminById(userId);
  // 何らかの処理
} else {
  getUserById(userId);
  // 何らかの処理
}

getAllUsers();

関数は使用順に上から下に書く

参照元の関数の下に書くことによって、上から下にコードが読めるようになる。
使用順に上から下に書くことで、ファイル内をあちこち行き来しなくて良いので可読性が向上する。
ちなみに縦方向の記述を整えることを垂直フォーマットと呼ぶ。

参照透過性を保つ

参照透過性とは決まった入力(引数)に対して必ず決まった出力(戻り値)を返す性質のことを指す。
Bad

let b = 1;
function sum(a) {
  return a + b;
}
const result = sum(2);

console.log(result);

この場合には関数 sum の中から外部スコープの変数 b を参照している。そのため、変数 b の値が変わると、関数 sum は同じ引数を渡して実行しても異なる実行結果が返ってくる。これは参照透過ではない。

Good

let b = 1;
function sum(a, c) {
  return a + c;
}
const result = sum(2, b);

console.log(result);

上記のように、値が変化するような変数は引数に渡し、戻り値で返すように書くことで参照透過性を持つ関数を書くことができる。

副作用を持つ関数はなるべく限定的にする

関数外の値や状態を更新するような処理は副作用という。

副作用には以下のような操作が挙げられます。

  • 引数で渡ってきたオブジェクトの中身を書き換える
  • 外部スコープの変数の値を変える
  • コンソールへのログ出力
  • DOM 操作
  • サーバーとの通信
  • タイマー処理
  • ランダムな値の生成

関数が副作用を持つということは、その関数の実行が戻り値以外にも影響を与えていることを意味する。そのため、副作用をもつ関数がそこらしかに存在するとコードが追いずらくなってしう。

Bad

async function sendPostHandler() {
  const inputTitle = getUserInputTitle(); // ユーザーからの入力を取得(副作用)
  const inputBody = getUserInputBody(); // ユーザーからの入力を取得(副作用)

  // POST送信する際のデータを定義
  const sendData = {
    title: inputTitle,
    body: inputBody,
  };

  // 入力値のチェック(副作用でない)
  if (!validateTitle(sendData.title)) return;
  if (!validateBody(sendData.body)) return;

  // サーバーへのリクエスト(副作用)
  const response = await fetch("/blog-post", {
    method: "POST",
    body: JSON.stringify(sendData),
  });

  const result = await response.json();
}

Good
バリデーションロジックを抽出し別関数にすることで副作用のない関数を作成した。これによりテストもしやすく安定する関数を定義できたことになる。

async function formSubmitHandler() {
  const sendData = getSendData();

  if (!validateForm(sendData)) return false;

  const result = await postBlog(sendData);
  return result;
}

function getSendData() {
  const inputTitle = getUserInputTitle();
  const inputBody = getUserInputBody();

  return {
    title: inputTitle,
    body: inputBody,
  };
}

// 参照透過性&副作用を保持できる関数を抽出できた!
function validateForm(sendData) {
  // 入力値のチェック(副作用でない)
  if (!validateTitle(sendData.title)) return false;
  if (!validateBody(sendData.body)) return false;
  return true;
}

async function postBlog(sendData) {
  const response = await fetch("/blog-post", {
    method: "POST",
    body: JSON.stringify(sendData),
  });
  const result = await response.json();

  // HTTPステータスコードが200番代以外の場合はエラーを発生させる
  if (result.status < 200 && 300 <= result.status) {
    throw new Error(result.message);
  }

  return result;
}

関数は純粋関数としてなるべく定義する

上記で見てきた参照透過性の保持副作用のない関数のことを純粋関数と呼ぶ。
関数はなるべく純粋関数として定義し、副作用が発生する操作はなるべくひとまとまりにすることによって、純粋関数で記述した処理の動作安定性を向上させることができる。

純粋関数であることで次のようなメリットがある。

  1. 外部の状態に依存しないので、リファクタリングや拡張がしやすい
  2. スコープ外の値に影響を及ぼさないため、デグレードが発生しづらい
  3. テストコードが記述できる(特定の入力に対して特定の出力が行われるかテスト可能となる)
ShionShion

繰り返し処理

for...of や for...in を使用する

for, whileよりも for...of や for...in を使用した方が可読性が上がる。ただし for文よりもパフォーマンスは劣るため注意。

  • for...of は配列要素を取得
  • for...in は配列インデックスを取得

高階関数を使う

foreach, map, reduce などの高階関数を使用することで、for や while で書くよりも無駄な変数(カウンタ変数や配列の初期化など)が減り処理が分離される。

ShionShion

条件分岐

最も一般的な条件から評価する

if 文の条件式は最も一般的な評価(最頻値)から記述する。
なぜなら、頻繁に行われる処理を前に記述することで読み手の負担が軽減されるため。

Bad
const user = getCurrentUser();
if (isPremium(user)) {
  // プレミアムアカウントの処理
} else if (isAdmin(user)) {
  // 管理者アカウントの処理
} else if (isFree(user)) {
  // 通常アカウントの処理
} else {
  throw new Error("ユーザーの種類が不明です");
}

isFree(user)を最初に評価することで読む範囲が限定され、読む負担が減る。

Good
const user = getCurrentUser();
if (isFree(user)) {
  // user が通常ユーザーだった場合、この部分を読むだけでよい。
  // 通常アカウントの処理
} else if (isPremium(user)) {
  // プレミアムアカウントの処理
} else if (isAdmin(user)) {
  // 管理者アカウントの処理
} else {
  throw new Error("ユーザーの種類が不明です");
}

境界条件のカプセル化

境界条件に使用している値を適切に変数で置換することによって、コードの見通しを良くする。

Bad
const squareNum = 1;

// 一つの四角形を構成する点の数
if (squareNum * 4 <= 100) {
  console.log(`四角形の頂点の数は100以下です。`);
} else {
  console.log(`四角形の頂点の数は100より大きいです。`);
}
Good
const squareNum = 1;

// 一つの四角形を構成する点の数
const dotsPerSquare = squareNum * 4;
if (dotsPerSquare <= 100) {
  console.log(`四角形の頂点の数は100以下です。`);
} else {
  console.log(`四角形の頂点の数は100より大きいです。`);
}

switch 文よりも連想配列(オブジェクト)を利用する

同じような処理を switch 文で書くと冗長な記述が増えてしまうため、連想配列でまとめられないか検討する。

Bad
function getEmailContent(fullname) {
  return `${fullname} 様、こんにちは。○○サービスにようこそ。`;
}

function sendInviteEmail(loginEmail) {
  let emailContent;

  switch (loginEmail) {
    case "js-taro@example.com":
      emailContent = getEmailContent("JS タロウ");
      sendEmail("js-taro@example.com", emailContent);
      break;

    case "python-taro@example.com":
      emailContent = getEmailContent("PYTHON タロウ");
      sendEmail("python-taro@example.com", emailContent);
      break;

    case "java-taro@example.com":
      emailContent = getEmailContent("JAVA タロウ");
      sendEmail("java-taro@example.com", emailContent);
  }
}

sendInviteEmail("js-taro@example.com");

オブジェクトなどの連想配列(キーと値が対で格納される配列)を使うと、switch 文による条件分岐をコード内から取り除くことができる。

Good
function getEmailContent(fullname) {
  return `${fullname} 様、こんにちは。○○サービスにようこそ。`;
}

const EMAIL_LIST = {
  "js-taro@example.com": "JS タロウ",
  "python-taro@example.com": "PYTHON タロウ",
  "java-taro@example.com": "JAVA タロウ",
};

function sendInviteEmail(loginEmail) {
  const fullname = EMAIL_LIST[loginEmail];
  let emailContent = getEmailContent(fullname);
  sendEmail(loginEmail, emailContent);
}

早期 return を使う

早期 return で次のようなメリットがあるためできるだけ早期 return を適用する。

  • 読みやすくなる
  • ネストが浅くなる
Bad
function isActiveUser(user) {
  if (user != null) {
    if (
      user.startDate <= today &&
      (user.endDate == null || today <= user.endDate)
    ) {
      if (user.stopped) {
        return false;
      } else {
        return true;
      }
    } else {
      return false;
    }
  } else {
    return false;
  }
}
Good
function isActiveUser(user) {
  if (user === null) return false;
  if (user.startDate > today) return false;
  if (!(user.endDate == null || today <= user.endDate)) return false;
  if (user.stopped) return false;
  return true;
}

if 文よりもコールバック、クラスを使用する

条件分岐が発生すればするほど、コードの実行は追いづらくなり、考慮しなければならいケースが増えるため、if 文や switch 文はなるべくプログラム中に書かないようにする。

Bad
function updateUsername(data, db) {
  /* ユーザー名の更新処理 */
}

function updatePassword(data, db) {
  /* パスワードの更新処理 */
}

function executeQuery(type, data) {
  const db = getDBConnection();
  db.startTransaction();

  if (type === "username") {
    updateUsername(data, db);
  } else if (type === "password") {
    updatePassword(data, db);
  }

  db.endTransaction();
}

// ユーザー名の更新
executeQuery("username", { userId: 1, userName: "Tom" });

// ユーザーパスワードの更新
executeQuery("password", { userId: 1, password: "fewatewaq" });

executeQuery から条件分岐をなくしたため、今後executeQueryの分岐を増やすような修正が不要になる。なるべく修正が不要な関数を実装するようにする。

Good
function updateUsername(data, db) {
  /* ユーザー名の更新処理 */
}

function updatePassword(data, db) {
  /* パスワードの更新処理 */
}

function executeQuery(updateQuery, data) {
  const db = getDBConnection();
  db.startTransaction();

  updateQuery(data, db);

  db.endTransaction();
}

// ユーザー名の更新
executeQuery(updateUsername, { userId: 1, userName: "Tom" });

// ユーザーパスワードの更新
executeQuery(updatePassword, { userId: 1, password: "fewatewaq" });
ShionShion

クラス

  • クラスの情報は出来る限り隠蔽する
    • クラスを作成する目的は「データ保持」と「操作」の2種類ある
    • データ保持の目的で作成したクラスはあまり隠蔽する必要はない
    • 操作(メソッドを持つ)の目的で作成したクラスは基本的に隠蔽する
  • 使用側がクラス内部について知らなくても使えるようにする
  • クラスはなるべく小さく作る
  • クラス継承とコンポジションの違いを知る
  • 凝集度と結合度を意識する

クラス継承とコンポジション

  • クラス継承
    • あるクラスが他クラスの特性を引き継いで、新たなクラスとして定義されること
    • is-aの関係を持つ
  • コンポジション
    • あるクラスの機能を持つクラスのこと
    • 特定のクラスの機能を自分が作るクラスにも持たせたい場合に、継承を使わずプロパティとしてそのクラスを持つこと
    • has-aの関係を持つ
コンポジション例
class Television {
  #power = false;
  #switch = new Switch();

  toggleSwitch() {
    this.#power = this.#switch.toggle(this.#power);
    console.log(`power: ${this.#power ? "on" : "off"}`);
  }
}

class Switch {
  toggle(power) {
    return !power;
  }
}

凝集度を高める

  • 凝集度とは、モジュール内におけるデータ(情報)とロジック(機能)の関係性の強さを表す指標のこと
  • データを保持するクラスと、そのデータを使って計算するロジックが局所的に集まっている状態を高凝集、分散している状態を低凝集という
  • 関連する機能や情報がモジュールとして閉じた状態でまとめられている( = 高凝集)の設計になっていれば、仕様変更で生じる修正の影響範囲は小さくなり、保守性の高いコードとなる
低凝集
class Product {
  price = 0;
}

class Shop {
  getTaxFreePrice() {
    // 税抜き金額の算出
  }

  getTaxIncludedPrice() {
    // 税込み金額の算出
  }
}
class OnlineShop {
  getTaxFreePrice() {
    // 税抜き金額の算出
  }

  getTaxIncludedPrice() {
    // 税込み金額の算出
  }
}
高凝集
class Product {
  #price = 0;

  getTaxFreePrice() {
    // 税抜き金額の算出
  }

  getTaxIncludedPrice() {
    // 税込み金額の算出
  }
}

結合度を疎にする

  • 結合度とは、モジュール間のデータ参照や接続の依存度合いを表す指標
  • モジュール間の関係性が高く、一方の変更が相手へ与える影響が大きい状態を密結合、互いに独立していて変更や修正の自由度が高い状態を疎結合と言う
  • クラス同士の結びつきを弱める疎結合を目指すことでモジュール同士の独立性を高め、再利用しやすく、見通しのよいコードとなる

下記の例ではProductクラスにプロパティを追加(discountPercent)した時、Orderクラスにも修正を加える必要がある。

密結合
class Product {
  price = 0;
  TAX = 0.1;
  discountPercent = 0.2;

  constructor(price) {
    this.price = price;
  }
}

class Order {
  getTaxIncludedTotalPrice() {
    const product = new Product(3000);
    return product.price * (1 + product.TAX) * (1 - product.discountPercent); // Productの変更に伴う変更が発生->密結合!
  }
}

合計を計算するロジックをOrderクラスに持たせるのではなく、Productクラスに持たせることで修正箇所がProductのみに限定され、疎結合になる。

疎結合
class Product {
  #price = 0;
  #TAX = 0.1;
  #discountPercent = 0.2;

  constructor(price) {
    this.#price = price;
  }

  getTaxIncludedPrice() {
    return this.#price * (1 + this.#TAX) * (1 - this.#discountPercent);
  }
}

class Order {
  getTaxIncludedTotalPrice() {
    const product = new Product(3000);
    return product.getTaxIncludedPrice();
  }
}
ShionShion

SOLID 原則

  • 単一責任の原則
  • オープン・クローズドの原則
  • リスコフの置換原則
  • インターフェース分離の原則
  • 依存性逆転の原則

単一責任の原則

概要

  • クラスは単一の責任(責務)のみを負うべきである」というルール。
  • クラスに責任が多いと、クラスの機能を変更するときに、他のコードに与える影響箇所が大きくなるため、クラスに責任を多く持たせないようにする

目的

  • 改修時の影響箇所の限定化
  • 一つのクラスの肥大化防止
Bad
class LoginButton {
  onClick() {
    // ログインボタンを押した際の処理
    // 中で login() が呼ばれると仮定
  }

  render() {
    // ログインボタンの表示
  }

  login() {
    // ログイン機能
  }
}
Good
class LoginButton {
  onClick() {
    // ログインボタンを押した際の処理
    // 中で login() が呼ばれると仮定
  }

  render() {
    // ログインボタンの表示
  }
}

class Auth {
  login() {
    // ログイン機能
  }

  logout() {}
}

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

  • クラスは拡張にはオープンで、修正にはクローズドであるべきである」というルール
  • 要は、追加は容易にできて修正は他の箇所に影響しないようにするべきであるという考え方

以下の例だと、printEmployeeEmployeeと密結合になっている。言い換えると、printEmployeeEmployeeに強く依存している。例えばEmployeeにプロパティ名の修正が入った場合、printEmployeeの方も修正が必要になる。これはオープン・クローズドの原則に違反している。

Bad
function printEmployee(employee) {
  console.log(employee.description);
  employee.members.forEach((name) => {
    console.log(name);
  });
}

class Employee {
  constructor(description, members) {
    this.description = description;
    this.members = members;
  }
}

const employee = new Employee("従業員情報", ["Taro", "Jiro", "Saburo"]);

printEmployee(employee);

printEmployeeEmployeeのプロパティを直接参照するのではなくEmployeeクラスのメソッドを呼び出してあげるようにする。これでEmployeeクラスの修正はEmployeeクラス内に限定できる。

Good
function printEmployee(employee) {
  employee.printDescription();
  employee.printNames();
}

class Employee {
  constructor(description, members) {
    this.description = description;
    this.members = members;
  }

  printDescription() {
    console.log(this.description);
  }

  printMembers() {
    this.members.forEach((member) => {
      console.log(member);
    });
  }
}

const employee = new Employee("従業員情報", ["Taro", "Jiro", "Saburo"]);
printEmployee(employee);

リスコフの置換原則

  • 「S が T の派生系(S が T を継承している状態)であれば、プログラム内で T 型のオブジェクトが使用されている箇所は全て S 型のオブジェクトで置換可能であるべきである」というルール
  • 言い換えると、派生クラスは基底クラスと同じ振る舞いをするべきであるということ
  • 具体的に言うと、メソッドの戻り値や副作用なども T で決まっている場合、T を継承している S でも必ず同じ振る舞いをするべきという考え方

以下の例だと、HumanクラスのgetAgeは返り値があるのに対し、StudentクラスのgetAgeはコンソール出力しているだけで返り値が存在しない。これはリスコフの置換原則に違反している。

Bad
class Human {
  age = 20;
  getAge() {
    return this.age;
  }
}

class Student extends Human {
  getAge() {
    console.log(`${this.age}歳です。`);
  }
}
Good
class Human {
  age = 20;
  getAge() {
    return this.age;
  }
}

class Student extends Human {
  getAge() {
    console.log(`${this.age}歳です`);
    return this.age;
  }
}

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

  • 「インターフェースの利用者に対して、インターフェースを分離するべきである」というルール
  • 言い換えると、インターフェースの利用者に必要なものだけを提供するべきである。 ということ

不要なものまで提供されてしまっている。

Bad
class Creature {
  eat() {}
  run() {}
  fly() {}
  swim() {}
}

const dog = new Creature();
dog.eat();
dog.run();

const swallow = new Creature();
swallow.eat();
swallow.fly();

const shark = new Creature();
shark.eat();
shark.swim();

必要なものだけを提供するようにインターフェースを分離する。

Good
class Creature {
  eat() {}
}

class Animal extends Creature {
  run() {}
}
class Bird extends Creature {
  fly() {}
}
class Fish extends Creature {
  swim() {}
}

const dog = new Animal();
dog.eat();
dog.run();

const swallow = new Bird();
swallow.eat();
swallow.fly();

const shark = new Fish();
shark.eat();
shark.swim();

依存関係逆転の原則

  • 依存関係とは、A というクラスが B というクラスを利用している場合、A は B に依存しているということです。
  • その依存関係が抽象クラスと具象クラスのみにあり、具象クラス同士に依存関係がない状態が最も柔軟性に優れていると言われている
  • その考えから、具象クラス同士での利用が必要になった場合、お互いに直接依存するのではなく共有された抽象クラスを抜き出しそこに依存させるべきである、というのが依存関係逆転の原則

良い依存とは?

  • 依存関係は一方向になるようにする(双方向依存はしない)
  • 依存関係は巡回しないようにする
  • 上位のモジュールは下位のモジュールに依存してはいけない
  • 抽象は実装に依存してはいけない、実装は抽象に依存するべきである

Paymentという抽象クラスが、Paypalという具象クラスに依存してしまっている。
上位のモジュール(抽象クラス)は下位のモジュール(具象クラス)に依存しているので、依存性逆転の原則に違反している。

Bad
class Payment {
  // 2023/12/08 初期化処理追加
  #strategy = null;

  constructor(strategy) {
    this.#strategy = strategy;
  }

  confirmOrder(billingInfo, orderInfo) {
    // 具象クラス(下位のモジュールの実装)に依存
    if (this.#strategy instanceof Paypal) {
      this.#strategy.payByPaypal(billingInfo, orderInfo);
    } else if (this.#strategy instanceof CreditCard) {
      this.#strategy.confirmOrder(billingInfo, orderInfo);
    }
  }

  cancel(id) {
    // 具象クラス(下位のモジュールの実装)に依存
    if (this.#strategy instanceof Paypal) {
      this.#strategy.cancel(id);
    } else if (this.#strategy instanceof CreditCard) {
      this.#strategy.refund(id);
    }
  }
}

class Paypal {
  payByPaypal(billingInfo, orderInfo) {}
  cancel(id) {}
}

class CreditCard {
  confirmOrder(billingInfo, orderInfo) {}
  refund(id) {}
}
Good
class Payment {
  // 2023/12/08 初期化処理追加
  #strategy = null;

  constructor(strategy) {
    this.#strategy = strategy;
  }

  confirmOrder(billingInfo, orderInfo) {
    // 具象クラス(下位のモジュールの実装)に依存していない!
    this.#strategy.confirmOrder(billingInfo, orderInfo);
  }

  cancel(id) {
    // 2023/12/08 メソッド名修正
    // 具象クラス(下位のモジュールの実装)に依存していない!
    this.#strategy.cancel(id);
  }
}

class PaymentMethod {
  confirmOrder(billingInfo, orderInfo) {
    throw new Error("confirmOrderを実装してください。");
  }
  cancel(id) {
    throw new Error("cancelを実装してください。");
  }
}

class Paypal extends PaymentMethod {
  // 具象クラス(下位のモジュールの実装)を抽象クラス(上位のモジュールの実装)に寄せる!
  confirmOrder(billingInfo, orderInfo) {}
  // 具象クラス(下位のモジュールの実装)を抽象クラス(上位のモジュールの実装)に寄せる!
  cancel(id) {}
}

class CreditCard extends PaymentMethod {
  confirmOrder(billingInfo, orderInfo) {}
  cancel(id) {}
}
このスクラップは3ヶ月前にクローズされました