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


# 概要
https://qiita.com/Yuki-TU/items/80536b2faf60b0a5679f

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

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

# 無関係の問題を抽出
下位問題を抽出し、小さなメソッドにすることで以下のメリットがある
- メソッドが小さくなるため可読性が上がる
- 単体でテスト可能
- 他の関数でも利用可


## 下位問題の抽出：例題 1

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

```ts
// 与えられた軽度緯度に最も近い"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;
};
```

---

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

```ts
// 与えられた軽度緯度に最も近い"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;
};
```

---

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



```ts
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;
};
```

---

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

```ts
// 与えられた軽度緯度に最も近い"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 を考える

```ts
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;
}
```

---


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

```ts
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;
}
```

---



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

```ts
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;
}
```

---

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

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

---

## タスクを小さくする

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

以下の例を考える

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

- 無投票 - total: 4
- 賛成 - total: 5
- 反対 - total: 3

```ts
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. `oldVote`と`newVote`を解析
2. `score`を更新

```ts
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. `oldVote`と`newVote`を解析 について考える

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

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

---


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

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

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

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

  setScore(score);
}
```

---

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

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

ルール

1. 県を選ぶ時、値がなければ市 → 区町の順番で選ぶ
2. 県、市、区町がない場合、なんでもない場所をデフォルトとして代入
3. 国がない場合、「地球」をデフォルトとして代入

```ts
type LocationInformation = {
  prefecrure: string; //"大阪府";
  city: string; //"大阪市";
  town: string; //"北区";
  country: string; //"日本";
};
```

---


悪い例

```ts
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 から値を抽出

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

---


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

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

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

```ts
const secondHalf = country || "地球";
```

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

---


4.値を更新

```ts
const place = firstHalf + secondHalf;
```

---


まとめる

```ts
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;
}
```

---


- ここで国が日本の場合は、国のところは区町を入れるとの仕様変更があった
- 容易に対応できることがわかる


```ts
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

```ts
/*  後で使用するかもしれないから残そう！ */
// 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](https://momentjs.co)
  - 時間を扱うライブラリ
- [date-fns](https://date-fns.org/)
  - Moment.js をさらに使いやすくしたライブラリ
- [is.js](https://is.js.org/)
  - フラグ等を扱うライブラリ
- [underscore.js](https://underscorejs.org/)
  - 大量のデータ処理をする際に、よく使うロジックをとても簡単に書くことができるライブラリ
- [highcharts](https://www.highcharts.com/)
  - 図表が作れるライブラリ
- [Flatpickr](https://flatpickr.js.org/)
  - 日時入力カレンダーライブラリ

---

## オープンソースの利用

Moment.js

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

```ts
// bad標準ライブらりの場合
const d = new Date("2012/10/20 15:30:50");
const formatDate =
  d.getFullYear() +
  "年" +
  (d.getMonth() + 1) +
  "月" +
  d.getDate() +
  "日 " +
  d.getHours() +
  "時" +
  d.getMinutes() +
  "分";
```

```ts
// 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 つだけ**

---