2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【良いコードを書くための Tips】大きなコード作成編

Last updated at Posted at 2022-02-26

概要

の続きです。TypeScriptで記述しています。

目次

  1. 導入
  2. 命名規則
  3. より綺麗にする
  4. ロジック
  5. 大きなコード作成(<-ここ)
  6. 最後に(<-ここ)

無関係の問題を抽出

下位問題を抽出し、小さなメソッドにすることで以下のメリットがある

  • メソッドが小さくなるため可読性が上がる
  • 単体でテスト可能
  • 他の関数でも利用可

下位問題の抽出:例題 1

与えられた地点から最も近い場所を見つける」 findClosestLocation メソッドについて考える

// 与えられた軽度緯度に最も近い"placeList"の要素を返す
const findClosestLocation = (lat: number, lng: number, placeList: Place[]) => {
  let closestDist = Number.MAX_VALUE;
  let closedPlace;
  placeList.forEach((place: Place) => {
    const latRad = radians(lat);
    const lngRad = raians(lng);
    const lat2Rad = radians(place.lat);
    const lng2Rad = raians(place.lng);
    const dist = Math.acos(
      Math.sin(latRad) * Math.sin(lat2Rad) +
        Math.cos(latRad) * Math.cos(lat2Rad) * Math.cos(lng2Rad - lngRad)
    );
    // 算出した距離が他の一番近い距離より近ければ更新
    if (dist < closestDist) {
      closestPlace = place;
      closestDist = dist;
    }
  });
  return closestPlace;
};

2つの地点の球面距離を算出する」コードが下位問題

// 与えられた軽度緯度に最も近い"placeList"の要素を返す
const findClosestLocation = (lat: number, lng: number, placeList: Place[]) => {
  let closestDist = Number.MAX_VALUE;
  let closedPlace;
  placeList.forEach((place: Place) => {
    // ここから
    const latRad = radians(lat);
    const lngRad = raians(lng);
    const lat2Rad = radians(place.lat);
    const lng2Rad = raians(place.lng);
    const dist = Math.acos(
      Math.sin(latRad) * Math.sin(lat2Rad) +
        Math.cos(latRad) * Math.cos(lat2Rad) * Math.cos(lng2Rad - lngRad)
    );
    // ここまで
    if (dist < closestDist) {
      closestPlace = place;
      closestDist = dist;
    }
  });
  return closestplace;
};

2つの地点の球面距離を算出する」コードを関数に抽出

const sphericalDistance = (
  lat1: number,
  lng1: number,
  lat2: number,
  lng2: number
) => {
  const lat1Rad = radians(lat1);
  const lng1Rad = raians(lng1);
  const lat2Rad = radians(lat3);
  const lng2Rad = raians(lng3);
  const dist = Math.acos(
    Math.sin(lat1Rad) * Math.sin(lat2Rad) +
      Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.cos(lng2Rad - lng1Rad)
  );
  return dist;
};

元のコードより簡潔になった

// 与えられた軽度緯度に最も近い"placeList"の要素を返す
const findClosestLocation = (lat: number, lng: number, placeList: Place[]) => {
  let closestDist = Number.MAX_VALUE;
  let closedPlace;
  placeList.forEach((place: Place) => {
    const dist = sphericalDistance(lat, lng, place.lat, place.lng);
    if (dist < closestDist) {
      closestPlace = place;
      closestDist = dist;
    }
  });
  return closestplace;
};

下位問題の抽出:例題 2

「API よりユーザ情報のレスポンス取得」する fetchUser を考える

async function fetchUser(url: Url) {
  // { resisterData: "date 20211221 userId 8898" }
  const result = await axios.get(url);
  const dataList = result.data.split(" ");
  // "{resisterDate: 20211221,userId: 8898,}"に整形
  let str = "{\n";
  dataList.forEach((data: string, index: number) => {
    str += ` ${data}`;
    if (index % 2) {
      str += ",\n";
    }
    if (!(index % 2)) {
      str += ":";
    }
  });
  str += "}";
  return str;
}

「コードを整形する」コードが下位問題

async function fetchUser(url: Url) {
  // { resisterData: "date 20211221 userId 8898" }
  const result = await axios.get(url);
 // ここから
  const dataList = result.data.split(" ");
  // "{resisterDate: 20211221,userId: 8898,}"に整形
  let str = "{\n";
  dataList.forEach((data: string, index: number) => {
    str += ` ${data}`;
    if (index % 2) {
      str += ",\n";
    }
    if (!(index % 2)) {
      str += ":";
    }
  });
  str += "}";
  // ここまで
  return str;
}

「コードを整形する」コードを関数に抽出

function formatPretty(result) {
  const dataList = result.data.split(" ");
  let str = "{\n";
  dataList.forEach((data: string, index: number) => {
    str += ` ${data}`;
    if (index % 2) {
      str += ",\n";
    }
    if (!(index % 2)) {
      str += ":";
    }
  });
  str += "}";
  return str;
}

元のコードはより簡潔に表現できるようになった

async function fetchUser(url: Url) {
  const result = await axios.get(url);
  return formatPretty(result);
}

タスクを小さくする

一度に複数のことをすると理解しにくいため、1 つにするための方法をみる

以下の例を考える

投票用ウィジェットを考える。ユーザは「賛成」、「反対」、「無投票」の 3 つの選べる。
選択したら、以下の関数が呼ばれて合計投票数を計算することを考える。

  • 無投票 - total: 4
  • 賛成 - total: 5
  • 反対 - total: 3
function voteChanged(oldVote: string, newVote: string) {
  let score = getScore();
  if(newVote !== oldVote) {
    if(newVote === 'Up'){
      score += (oldVote === 'Down' ? 2 : 1)
    } else if(newVote === 'Down') {
      score -= (oldVote ===  'Up' ? 2 : 1)
    } else if(newVote === '') {
      score += (oldVote === 'Up' ? -1 : 1)
    }
  }
  setScore(score);
}

タスクを分解する

  1. oldVotenewVoteを解析
  2. scoreを更新
function voteChanged(oldVote: string, newVote: string) {
  let score = getScore();
  if(newVote !== oldVote) {
    if(newVote === 'Up'){
      score += (oldVote === 'Down' ? 2 : 1)
    } else if(newVote === 'Down') {
      score -= (oldVote ===  'Up' ? 2 : 1)
    } else if(newVote === '') {
      score += (oldVote === 'Up' ? -1 : 1)
    }
  }
  setScore(score);
}

  1. oldVotenewVoteを解析 について考える

解析をして対応する数値を返す関数を作成する

// voteの値を解析し、適切な値を返す
function parseVoteValue(vote: string) {
  if (vote === "Up") {
    return 1;
  }
  if (vote === "Down") {
    return -1;
  }
  return 0;
}

元のコードをparseVoteValueを利用し手書き換える

元のコードは 2. scoreを更新 を担う

function voteChanged(oldVote: string, newVote: string) {
  let score = getScore();

  // 前の値を削除
  score -= parseVoteValue(oldVote);
  // 新しい値を追加
  socre += parseVoteValue(newVote);

  setScore(score);
}

オブジェクトから値を抽出

locationInformationのオブジェクトから「prefecrue(県), country(国)」の値を返す関数getPlaceを考える

ルール

  1. 県を選ぶ時、値がなければ市 → 区町の順番で選ぶ
  2. 県、市、区町がない場合、なんでもない場所をデフォルトとして代入
  3. 国がない場合、「地球」をデフォルトとして代入
type LocationInformation = {
  prefecrure: string; //"大阪府";
  city: string; //"大阪市";
  town: string; //"北区";
  country: string; //"日本";
};

悪い例

function getPlace(location: LocationInformation) {
  let place = location.prefecture;
  if (!place) {
    place = location.city;
  }
  if (!place) {
    place = location.town;
  }
  if (!place) {
    place = "どこでもない場所";
  }
  if (location.country) {
    return place + "," + location.country;
  }
  return place + ", 地球";
}

タスクを分解

  1. location から値を抽出
  2. 県の優先准尉を調べ、なければデフォルト値を代入
  3. 国を取得し、なければデフォルト値を代入
  4. 値を更新

このように処理の順序を文字で起こすことで整理できる


1.location から値を抽出

// 値を抽出
const country = location.country;
const prefecture = location.prefecture;
const city = location.city;
const town = location.town;

2.県の優先准尉を調べ、なければデフォルト値を代入

const firstHalf = prefecture || city || town || "なんでもない場所";

3.国を取得し、なければデフォルト値を代入

const secondHalf = country || "地球";

優先順位を求めるタスクは短絡評価を使うとすっきり!


4.値を更新

const place = firstHalf + secondHalf;

まとめる

function getPlace(location: LocationInformation) {
  //  location から値を抽出
  const country = location.country;
  const prefecture = location.prefecture;
  const city = location.city;
  const town = location.town;

  //  県の優先准尉を調べ、なければデフォルト値を代入
  const firstHalf = prefecture || city || town || "なんでもない場所";

  //  国を取得し、なければデフォルト値を代入
  const secondHalf = country || "地球";

  // 値を更新
  const place = firstHalf + secondHalf;

  return place;
}

  • ここで国が日本の場合は、国のところは区町を入れるとの仕様変更があった
  • 容易に対応できることがわかる
function getPlace(location: LocationInformation) {
  //  location から値を抽出
  const country = location.country;
  const prefecture = location.prefecture;
  const city = location.city;
  const town = location.town;
  if (country === "日本") {
    //  県の優先順位を調べ、なければデフォルト値を代入
    const firstHalf = prefecture || city || "なんでもない場所";
    //  国を取得し、なければデフォルト値を代入
    const secondHalf = town || country;
    // 値を更新
    const place = firstHalf + secondHalf;
    return place;
  }
  //  県の優先順位を調べ、なければデフォルト値を代入
  const firstHalf = prefecture || city || town || "なんでもない場所";
  //  国を取得し、なければデフォルト値を代入
  const secondHalf = country || "地球";
  // 値を更新
  const place = firstHalf + secondHalf;
  return place;
}

短いコードを書く

コードを悪化ないことを知り、小さく保つことで、可読性があり、シンプルなコードとなる


不要なコメントアウトは含めない

後で使うかともとコメントアウトでコードで残すのは NG

/*  後で使用するかもしれないから残そう! */
// function getUser(url: string) {
//   try {
//     const result = await axios.get(url);
//     return result;
//   } catch {
//     throw new Error("失敗");
//   }
// }

標準メソッド・ライブラリ・オープンソースを利用

作りたい機能がオープンソースにないか調べ、あれば使う

標準メソッド

  • 配列
    • forEach
    • map
    • include
    • length
  • オブジェクト
    • Object.assign
    • Object.create
    • Object.defineProperty()

標準ライブラリ

  • Math
  • Date

オープンソース

  • Moment.js
    • 時間を扱うライブラリ
  • date-fns
    • Moment.js をさらに使いやすくしたライブラリ
  • is.js
    • フラグ等を扱うライブラリ
  • underscore.js
    • 大量のデータ処理をする際に、よく使うロジックをとても簡単に書くことができるライブラリ
  • highcharts
    • 図表が作れるライブラリ
  • Flatpickr
    • 日時入力カレンダーライブラリ

オープンソースの利用

Moment.js

2012/10/20 15:30:50 という日時の文字列から 2012 年 10 月 20 日 15 時 30 分のような形式で出力したい

// bad標準ライブらりの場合
const d = new Date("2012/10/20 15:30:50");
const formatDate =
  d.getFullYear() +
  "" +
  (d.getMonth() + 1) +
  "" +
  d.getDate() +
  "" +
  d.getHours() +
  "" +
  d.getMinutes() +
  "";
// good (Moment.jfを利用した場合)
const m = moment("2012/10/20 15:30:50");
const formatDate = m.format("YYYY年MM月DD日 HH時mm分");

まとめ

  • 複雑なコードの場合、タスクを文字で列挙することが大事
  • コードはクラスメソッド段落のいずれかには分けることができる

最後に

自然に読みやすいコードを書けるようになるための 3 ステップ

  • 実際にやる
  • 当たり前にする
  • コードで伝える

実際にやる

実際にやるとぶつかる?

自分の書いているコードにどのように適用したらいいのかがわからない・・・

  • 解決策
    • この資料を見直す
    • MR する前に全体を俯瞰する
    • 第三者にレビューしてもらう

当たり前にする

当たり前にするには?

自分だけきれいに書いても他の人が汚いコードだと、そのプロジェクトのコードは変わらない

  • どうする?

他の人のコードを積極的にレビューし、新しく入ってくるコードはすべてきれいにすることが大事


コードで伝える

実際にレビューする際はコード例を書こう

  • 何がいいのか?
    • 言葉で言われるよりより実感がわき、理解しやすい
    • 人に伝えるには自然とわかりやすいコードを書く
  • レビューを意識してコミット
  • フォーマットと修正を 1 つのコミットにまとめない
    1 つのことを 1 つだけ

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?