JavaScriptでアプリを作成しました【6】【4択クイズ②】

## 【仕様】
- 開始ボタンを押すと、問題が始まる。
- 回答する度に次の問題へ移行する。
- 10回問題が終わると、正答数が表示される。
- 最初に戻るボタンを押すと、最初の画面に戻る。

## オブジェクト指向とは
『オブジェクト指向』とは、『現実の事柄を実態として捉える考え方』の事。
プログラム設計や実装方法の一つで、プログラマーとしてコードを書いていく上で必須の概念であり、現在はオブジェクト指向を用いてコードを書いていくのが主流。
『オブジェクト指向』の考え方は、機能を部品化し、部品化された機能を組み合わせることで製品を完成させること。

## オブジェクト指向で書く理由
コードを管理しやすくするため。実務ではもっと複雑なコードが絡み合うことになり、自分だけでなく、他の人が同じコードに触れることも多々ある。その場合、必然的に管理しやすいコードが好まれる。誰かとコードを共有するのが必要不可欠な実務において、オブジェクト指向でコードを書くというのは、大きな利点に繋がる。

## オブジェクト指向に必須な用語

#### オブジェクト
- すべての”モノ”や”コト”のこと。
- メソッドとプロパティから構成されたもの。
- クラスによって定義される。

#### クラス
- オブジェクトを定義した設計図。
- 概念の大きな箱。
- 機能をたくさん集めたもの。

#### インスタンス
- クラス(設計図)を基に実体化したもの。
- クラスを基に作り出したオブジェクト。
- インスタンス化しないと、クラスで定義したものは使えない。

#### コンストラクタ
- インスタンスを生成する際に必ず必要なメソッド。
- 値の初期値を設定する。

## クイズアプリ作成
#### クラスとインスタンス生成
##### ① クイズのWebAPIを使用して、URLを定義。

```
const API_URL = 'https://opentdb.com/api.php?amount=10&type=multiple';
```

##### ② クラスを作成する。

```main.js
class Quiz {
　constructor() {
}
```

##### ③ インスタンス生成する。
quizInstanceを生成して、fetchで受け取ったクイズのデータをインスタンスの引数に設置。

```main.js
const fetchQuizData = async (index) => {
　titleElement.textContent = '取得中';
　questionElement.textContent = '少々お待ち下さい';

　const response = await fetch(API_URL);
　const quizData = await response.json();
　const quizInstance = new Quiz(quizData);

　setNextQuiz(quizInstance, index);
};
```

##### ④ 引数のクイズのデータを基に、クラス内の設計をする。

constructorの引数にはquizDataが入っている。⇨ このクラスのデータはquizDataを基に作られていくということ。

#####　※ this._quizzesにどんな値がクイズの情報として保持されているかもconsole.logで確認すること!!。

```main.js
class Quiz {
　constructor(quizData) {
　　this._quizzes = quizData.results;
　}

　getQuizCategory(index) {
　　return this._quizzes[index - 1].category;
　}

　getQuizDifficulty(index) {
　　return this._quizzes[index - 1].difficulty;
　}

　getQuizQuestion(index) {
　　return this._quizzes[index - 1].question;
　}

}

```

console.logで確認したように、クイズデータの中には「カテゴリや難易度、問題、解答」などたくさんの情報が入っている。更にquizData.resultsとすることで配列としてデータが格納されていることが確認できる。

※ 画像を見ると、左上にArrayとあり、このデータは配列であることを表す。

クラスを定義したことによって、クラスの中にはQuizのデータを介して表せる情報が保持されている。　※ 情報とはcategoryやdifficultyなどのことです。

##### ⑤ 関数内での呼び出し

インスタンスを生成できたことによって、アプリ内のどこででもインスタンスを呼び出して使用することが出来るようになるので、インスタンスを必要な箇所で呼び出して実装を仕上げる。

setNextQuiz関数へ処理を繋げる。

```main.js
const setNextQuiz = (quizInstance, index) => {
　makeQuiz(quizInstance, index);
};
```

##### ⑥ 『次へ』が押される度に次の問題へ進む

makeQuiz関数でquizInstanceを用いて、クイズを10回表示する実装を進める。

```main.js
const makeQuiz = (quizInstance, index) => {

　titleElement.innerHTML = `問題 ${index}`;
　genreElement.innerHTML = `【ジャンル】 ${quizInstance.getQuizCategory(index)}`;
　difficultyElement.innerHTML = `【難易度】 ${quizInstance.getQuizDifficulty(index)}`;
　questionElement.innerHTML = `【クイズ】${quizInstance.getQuizQuestion(index)}`;

　const buttonElement = document.createElement('button');
　buttonElement.innerHTML = '次へ';
　answerContainer.appendChild(buttonElement);

　buttonElement.addEventListener('click', () => {
　　index++;
　　answersContainer.removeChild(answersContainer.firstChild);
　　setNextQuiz(quizInstance, index);
　});
};
```

引数として渡ってきているquizInstanceを用いて、ジャンルや難易度の値を取得している。quizInstance.getQuizCategoryのようにすることで、クラスで定義したメソッドを使用することが出来る。

#### クイズが10回終わったら、正答数を表示する
##### ① ボタンを一つ用意して、正答数がカウントされる仕組みを作成する
クイズに関する情報は全てクラスで管理していくので、クイズの正答数やクイズ自体の長さなども引っ張ってこれるように定義する。

##### Quizクラス
追加
・クイズの長さを取得するためのgetNumQuizメソッド
・クイズの正答を取得するためのgetCorrectAnswerメソッド
・クイズの正答数をカウントするためのcountCorrectAnswersNumメソッド
・カウントした正答数を取得するためのgetCorrectAnswersNumメソッド

```main.js
class Quiz {
　constructor(quizData) {
　　this._quizzes = quizData.results;
　　this._correctAnswersNum = 0;
　}

// 省略

　getNumOfQuiz() {
　　return this._quizzes.length;
　}

　getCorrectAnswer(index) {
　　return this._quizzes[index - 1].correct_answer;
　}

　countCorrectAnswersNum(index, answer) {
　　const correctAnswer = this._quizzes[index - 1].correct_answer;
　　if (answer === correctAnswer) {
　　　return this._correctAnswersNum++;
　　}
　}

　getCorrectAnswersNum() {
　　return this._correctAnswersNum;
　　}
　}
}
```

##### makeQuiz関数

追加
・answerを定義
・ボタンのテキストに答えを表示
・正答数をカウントするインスタンスメソッドを定義

```main.js
const makeQuiz = (quizInstance, index) => {
　titleElement.innerHTML = `問題 ${index}`;
　genreElement.innerHTML = `【ジャンル】 ${quizInstance.getQuizCategory(index)}`;
　difficultyElement.innerHTML = `【難易度】 ${quizInstance.getQuizDifficulty(index)}`;
　questionElement.innerHTML = `【クイズ】${quizInstance.getQuizQuestion(index)}`;

　const answer = quizInstance.getCorrectAnswer(index);

　const buttonElement = document.createElement('button');
　buttonElement.innerHTML = answer;
　answersContainer.appendChild(buttonElement);

　buttonElement.addEventListener('click', () => {
　　quizInstance.countCorrectAnswersNum(index, answer);
　　index++;
　　answersContainer.removeChild(answerContainer.firstChild);
　　setNextQuiz(quizInstance, index);
　});
};
```

##### finishQuiz関数

クイズが終わった後の処理を行ってくれる関数を設置します。
・カウントした正答数を表示
・ジャンルや難易度の項目を空白にする
・再チャレンジ用の文面を用意

```main.js
const finishQuiz = (quizInstance) => {
　titleElement.textContent = `あなたの正答数は${quizInstance.getCorrectAnswersNum()}です`
　genreElement.textContent = '';
　difficultyElement.textContent = '';
　questionElement.textContent = '再チャレンジしたい場合は下をクリック';
};
```

##### setNextQuiz関数

あとはクイズの問題数によって処理を切り分けていきます。
・クイズの問題数が10回以下ならmakeQuiz関数へ
・それ以外の場合はfinishQuiz関数へ

```main.js
const setNextQuiz = (quizInstance, index) => {
　if (index <= quizInstance.getNumOfQuiz()) {
　　makeQuiz(quizInstance, index);
　} else {
　　finishQuiz(quizInstance);
　}
};
```
#### 正答数をカウントする
先程使用したmakeQuiz関数内のコードを少し編集してボタンを生成する処理をする。

##### makeQuiz関数

変更点
・配列でanswersを定義
・ボタン要素をリスト表示

```main.js
const makeQuiz = (quizInstance, index) => {

// 省略

// 以下、編集

```main.js
　const answers = [
　　quizInstance.getCorrectAnswer(index),
　　...quizInstance.getIncorrectAnswers(index)
　];

　answers.forEach((answer) => {
　　const answerElement = document.createElement('li');
　　answersContainer.appendChild(answerElement);

　　const buttonElement = document.createElement('button');
　　buttonElement.innerHTML = answer;
　　answerElement.appendChild(buttonElement);

　　buttonElement.addEventListener('click', () => {
　　　quizInstance.countCorrectAnswersNum(index, answer);
　　　index++;
　　　setNextQuiz(quizInstance, index);
　　});
　});
};
```

#### 解答項目をシャッフルする
現状、一番上に正答で、下3つは誤答となっているので、回答ボタンの『配列をシャッフルする処理』をする。
#### shuffleArray関数

```main.js
const shuffleArray = ([...array]) => {
　for (let i = array.length - 1; i >= 0; i--) {
　　const j = Math.floor(Math.random() * (i + 1));
　　[array[i], array[j]] = [array[j], array[i]];
　}
　return array;
};
```

#### buildAnswers関数

このbuildQuiz関数をmakeQuiz関数の中で呼び出す。
元々定義していたanswersを修正する。

```main.js
const buildAnswers = (quizInstance, index) => {
　const answers = [
　　quizInstance.getCorrectAnswer(index),
　　...quizInstance.getIncorrectAnswers(index)
　];
　return shuffleArray(answers);
};
```

#### makeQuiz関数

```main.js
const makeQuiz = (quizInstance, index) => {

// 省略

　const answers = [
　　quizInstance.getCorrectAnswer(index),
   ...quizInstance.getIncorrectAnswers(index)

　　↓↓↓

　const answers = buildAnswers(quizInstance, index);

// 省略

};
```

#### finishQuiz関数
クイズが終わった後にもう一度スタート画面に戻れるようにfinishQuiz関数に変更を加える。➡︎「ホームに戻る」ボタンを押せば、何回でもクイズが出来る。


```main.js
const finishQuiz = (quizInstance) => {

// 省略

　const restartButton = document.createElement('button');
　restartButton.textContent = 'ホームに戻る';
　answersContainer.appendChild(restartButton);
　restartButton.addEventListener('click', () => {
　　location.reload();
　});
};
```
#### アプリケーションが止まらないようにエラーハンドリングを行う
## 完成

下記のコードの部分に、APIとの通信はAPI側に以上があったりネットワークの接続状況などでAPIとの通信に失敗する可能性があるなどの例外が発生する可能性があるので、アプリケーションが止まらないようにエラーハンドリングを行う。

```
const fetchQuizData = async (index) => {
    titleElement.textContent = "取得中";
    questionElement.textContent = "少々お待ち下さい";

    const response = await fetch(API_URL);
    const quizData = await response.json();
    const quizInstance = new Quiz(quizData);

    setNextQuiz(quizInstance, index);
  };
```
## 完成

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>課題④４択クイズアプリ</title>
</head>
<body>
    <div class="container">
        <h1 id="title">ようこそ</h1>
        <p id="genre"></p>
        <p id="difficulty"></p><hr>
        <p id="question">以下のボタンをクリック</p><hr>
        <button id="start-button">開始</button>
    </div>
    <div id="answers"></div>    
    <style>
        li {
          list-style: none;
        }
    </style>
    <script src="main.js"></script>   
</body>
</html>
```
```main.js
{
  //クイズのWebAPIを使用して、URLを定義する
  const API_URL = "https://opentdb.com/api.php?amount=10&type=multiple";
  //クラスを作成する
  //引数のクイズのデータを基に、クラス内の設計をする。
  class Quiz {
    constructor(quizData) {
      this._quizzes = quizData.results;
      this._correctAnswersNum = 0;
    }
    getQuizCategory(index) {
      return this._quizzes[index - 1].category;
    }
    getQuizDifficulty(index) {
      return this._quizzes[index - 1].difficulty;
    }
    //クイズの長さを取得するためのgetNumQuizメソッド
    getNumOfQuiz() {
      return this._quizzes.length;
    }
    getQuizQuestion(index) {
      return this._quizzes[index - 1].question;
    }
    //クイズの正答を取得するためのgetCorrectAnswerメソッド
    getCorrectAnswer(index) {
      return this._quizzes[index - 1].correct_answer;
    }
    getIncorrerctAnswers(index) {
      return this._quizzes[index - 1].incorrect_answers;
    }
    //クイズの正答数をカウントするためのcountCorrectAnswersNumメソッド
    countCorrectAnswersNum(index, answer) {
      const correctAnswer = this._quizzes[index - 1].correct_answer;
      if (answer === correctAnswer) {
        return this._correctAnswersNum++;
      }
    }
    //カウントした正答数を取得するためのgetCorrectAnswersNumメソッド
    getCorrectAnswersNum() {
      return this._correctAnswersNum;
    }
  }

  const titleElement = document.getElementById("title");
  const questionElement = document.getElementById("question");
  const answersContainer = document.getElementById("answers");
  const startButton = document.getElementById("start-button");
  const genreElement = document.getElementById("genre");
  const difficultyElement = document.getElementById("difficulty");

  startButton.addEventListener("click", () => {
    startButton.hidden = true;
    fetchQuizData(1);
  });

  //quizInstanceを生成して、fetchで受け取ったクイズのデータをインスタンスの引数に設置する。
  const fetchQuizData = async (index) => {
    titleElement.textContent = "取得中";
    questionElement.textContent = "少々お待ち下さい";
    //アプリケーションが止まらないようにエラーハンドリング
    try {
      const respnse = await fetch(API_URL);
      quizData = await respnse.json();
      const quizInstance = new Quiz(quizData);
      setNextQuiz(quizInstance, index);
    } catch (error) {
      console.log(error);
    }
  };
  const setNextQuiz = (quizInstance, index) => {
    while (answersContainer.firstChild) {
      answersContainer.removeChild(answersContainer.firstChild);
    }

    if (index <= quizInstance.getNumOfQuiz()) {
      //クイズの問題数が10回以下ならmakeQuiz関数へ
      makeQuiz(quizInstance, index);
    } else {
      //それ以外の場合はfinishQuiz関数へ
      finishQuiz(quizInstance);
    }
  };
  //makeQuiz関数でquizInstanceを用いて、”次へ”が押される度に次の問題へ進むクイズを10回表示する実装をする。
  const makeQuiz = (quizInstance, index) => {
    titleElement.innerHTML = `問題${index}`;
    genreElement.innerHTML = `【ジャンル】 ${quizInstance.getQuizCategory(index)}`;
    difficultyElement.innerHTML = `【難易度】${quizInstance.getQuizDifficulty(index)}`;
    questionElement.innerHTML = `【クイズ】${quizInstance.getQuizQuestion(index)}`;
    //answerを定義
    const answers = buildAnswers(quizInstance, index);

    answers.forEach((answer) => {
      const answerElement = document.createElement("li");
      answersContainer.appendChild(answerElement);
      //ボタンのテキストに答えを表示
      const buttonElement = document.createElement("button");
      buttonElement.innerHTML = answer;
      answersContainer.appendChild(buttonElement);
      //正答数をカウントするインスタンスメソッドを定義
      buttonElement.addEventListener("click", () => {
        quizInstance.countCorrectAnswersNum(index, answer);
        index++;
        setNextQuiz(quizInstance, index);
      });
    });
  };
  const finishQuiz = (quizInstance) => {
    //カウントした正答数を表示
    titleElement.textContent = `あなたの正答数は${quizInstance.getCorrectAnswerNum()}です`;
    //ジャンルや難易度の項目を空白にする
    genreElement.textContent = "";
    difficultyElement.textContent = "";
    //再チャレンジ用の文面を用意
    questionElement.textContent = "再チャレンジしたい場合は下をクリック"; //クイズが終わった後にもう一度スタート画面に戻れるようにする
    const restartButton = document.createElement("button");
    restartButton.textContent = "ホームに戻る";
    answersContainer.appendChild(restartButton);
    restartButton.addEventListener("click", () => {
      location.reload();
    });
  };
  //buildQuiz関数をmakeQuiz関数の中で呼び出す
  const buildAnswers = (quizInstance, index) => {
    const answers = [quizInstance.getCorrectAnswer(index), ...quizInstance.getIncorrerctAnswers(index)];
    return shuffleArray(answers);
  };
  //Answersの配列をシャッフルする処理
  const shuffleArray = ([...array]) => {
    for (let i = array.length - 1; i >= 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
  };
}
```

## 参考サイト
[【JavaScript: クイズアプリ】オブジェクト指向を用いて、クイズを10回表示させる方法](https://ina-yona-mochiko.com/programming/javascript/quiz-object-oriented/)
[【JavaScript課題: クイズアプリ】必要な手順を逆算してアプリを作成していく方法](https://ina-yona-mochiko.com/programming/javascript/quiz-implement/)