リファクタリングとは?
リファクタリングとは、プログラムの動作を変えずに、コードを読みやすく、理解しやすくするために「整理・改善」することを指します。
過去に自分で書いたプログラムを後から見ると何が書いてあるかサッパリ分からない…といった経験、ありませんか?自分の書いたプログラムでも、そうなってしまうならば、他人が作成したプログラムを読み解くのは、さらに困難になる事が予想されます。
機能を追加したいのに、ソースが複雑すぎて理解できず追加できない…それは非常に辛いことです。
そんな問題をなくすため、プログラムを何が書いてあるかを読みやすく分かりやすくして誰でも理解でき、かつ、機能追加も簡単にできるように修正しよう!というのが、リファクタリングの目的になります。
とまぁ…説明はこれぐらいにして、実際に試してみましょう!とりあえずChatGPT o1-previewにソースを入力し、リファクタリング可能かどうかを検証していきたいと思います!
リファクタリング前のソースコード
検証のために「あまり良くないと思われるプログラムのソースコード」を用意してみました!
内容としては、ブラウザで遊ぶ「インベーダーシューティングゲーム」です!面白いかどうかは置いといて、バグなく動作します。
実際に動かして遊ぶ分には全く問題ないのですが、プログラミングの作りが良くない…という例になりますね!
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>究極のインベーダーゲーム</title>
<style>
/* Canvasを少し装飾、デザイン性も大事 */
#gameCanvas {
background-color: #333;
border: 5px solid #fefefe; /* オシャレなボーダー */
display: block;
margin: auto;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="400" height="600"></canvas>
<script>
// 定数宣言、あらゆる値を定数化して最高の管理
const CANVAS_WIDTH = 400;
const CANVAS_HEIGHT = 600;
const PLAYER_WIDTH = 30;
const PLAYER_HEIGHT = 30;
const PLAYER_SPEED = 5;
const PLAYER_COLOR = "cyan";
const BULLET_WIDTH = 4;
const BULLET_HEIGHT = 10;
const BULLET_SPEED = 5;
const BULLET_COLOR = "yellow";
const ENEMY_COLOR = "red";
const ENEMY_SPEED = 1;
const ENEMY_WIDTH = 30;
const ENEMY_HEIGHT = 30;
const ENEMY_PADDING = 10;
const ENEMY_OFFSET_TOP = 50;
const ENEMY_OFFSET_LEFT = 20;
const ENEMY_ROW_COUNT = 3;
const ENEMY_COLUMN_COUNT = 8;
const GAME_OVER_TEXT = "Game Over!";
const RANDOM_MOVEMENT_PROBABILITY = 0.5;
const RANDOM_DROP_PROBABILITY = 0.99;
const BASE_ENEMY_SPEED = 1; // 初期の敵速度
// グローバル変数たち、スコープを共有することで一貫したデータ管理が可能
let playerPosX = CANVAS_WIDTH / 2 - PLAYER_WIDTH / 2;
let playerPosY = CANVAS_HEIGHT - PLAYER_HEIGHT - 20;
let bullets = [];
let enemies = [];
let gameRunning = true;
let movingRight = true;
let currentEnemySpeed = BASE_ENEMY_SPEED; // 動的に変わる敵速度
let gameClearFlag = false; // すべての敵が倒されたかどうかを管理するフラグ群
let totalEnemies = ENEMY_ROW_COUNT * ENEMY_COLUMN_COUNT; // 総敵数を別途定義しておく
let gameOverFlag = false;
// プレイヤーの移動状態と弾発射の状態を管理するフラグ
let keys = {
left: false,
right: false,
space: false,
};
let lastBulletTime = 0; // 最後に弾を発射した時間
// プレイヤー用ファクトリー関数、わざわざ分割して理解しやすい
function createPlayer() {
return {
posX: playerPosX,
posY: playerPosY,
width: PLAYER_WIDTH,
height: PLAYER_HEIGHT,
speed: PLAYER_SPEED,
color: PLAYER_COLOR
};
}
const player = createPlayer();
// 敵生成関数、インベーダーの数や配置を一括管理
function generateEnemies() {
for (let rowIndex = 0; rowIndex < ENEMY_ROW_COUNT; rowIndex++) {
for (let colIndex = 0; colIndex < ENEMY_COLUMN_COUNT; colIndex++) {
let enemy = createEnemy(
colIndex * (ENEMY_WIDTH + ENEMY_PADDING) + ENEMY_OFFSET_LEFT,
rowIndex * (ENEMY_HEIGHT + ENEMY_PADDING) + ENEMY_OFFSET_TOP
);
enemies.push(enemy);
}
}
}
// 1つの敵オブジェクトを生成、敵ごとに固有のデータを持たせる
function createEnemy(x, y) {
return {
posX: x,
posY: y,
width: ENEMY_WIDTH,
height: ENEMY_HEIGHT,
color: ENEMY_COLOR,
isAlive: true
};
}
// 弾丸を生成する関数、プレイヤーから発射される弾ごとに生成、画面上のミサイルが3つ以下のときのみ発射
function shootBullet() {
if (bullets.length < 3) { // 弾丸が3つ以下の場合のみ発射
bullets.push({
posX: player.posX + player.width / 2 - BULLET_WIDTH / 2, // プレイヤーの中央に合わせて発射
posY: player.posY, // プレイヤーの現在y位置から発射
width: BULLET_WIDTH,
height: BULLET_HEIGHT,
speed: BULLET_SPEED,
color: BULLET_COLOR
});
}
}
// キャンバスの設定と描画用Context取得
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
// ゲームループ関数
function gameLoop() {
clearCanvas();
if (gameRunning) {
movePlayer(); // プレイヤーの移動処理
handleShooting(); // 弾発射の処理
drawPlayer();
drawBullets();
drawEnemies();
moveEnemies();
handleBulletCollisions();
checkGameOver(); // ゲームオーバーの判定
} else {
if (gameOverFlag) {
drawMessage("Game Over"); // ゲームオーバーのメッセージ表示
} else if (gameClearFlag) {
drawMessage("Game Clear!"); // ゲームクリアのメッセージ表示
}
}
requestAnimationFrame(gameLoop);
}
// 画面クリア、真っさらなキャンバスでリセット
function clearCanvas() {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
}
// プレイヤーを描画
function drawPlayer() {
ctx.fillStyle = player.color;
ctx.fillRect(player.posX, player.posY, player.width, player.height);
}
// 敵を描画、敵の配列を順番に描画する
function drawEnemies() {
for (let enemy of enemies) {
if (enemy.isAlive) {
ctx.fillStyle = enemy.color;
ctx.fillRect(enemy.posX, enemy.posY, enemy.width, enemy.height);
}
}
}
// 弾の描画と移動、弾がキャンバス内にあるかの確認と移動処理をまとめる
function drawBullets() {
for (let bullet of bullets) {
ctx.fillStyle = bullet.color;
ctx.fillRect(bullet.posX, bullet.posY, bullet.width, bullet.height);
bullet.posY -= bullet.speed;
}
bullets = bullets.filter(bullet => bullet.posY > 0);
}
// 衝突処理:弾が敵に当たったとき、毎回ゲームクリア判定を呼ぶ
function handleBulletCollisions() {
for (let bullet of bullets) {
for (let enemy of enemies) {
if (enemy.isAlive && checkCollision(bullet, enemy)) {
enemy.isAlive = false;
bullet.posY = -10; // 弾を画面外に移動し消去扱い
updateEnemySpeed(); // 敵が減った後に速度を更新
checkGameClear(); // 敵が全滅したかを確認
}
}
}
}
// 衝突判定関数、範囲が被っているかの判定
function checkCollision(bullet, enemy) {
return (
bullet.posX < enemy.posX + enemy.width &&
bullet.posX + bullet.width > enemy.posX &&
bullet.posY < enemy.posY + enemy.height &&
bullet.posY + bullet.height > enemy.posY
);
}
// 敵の残り数に応じた速度を更新
function updateEnemySpeed() {
const aliveEnemies = enemies.filter(enemy => enemy.isAlive).length;
currentEnemySpeed = BASE_ENEMY_SPEED + (ENEMY_ROW_COUNT * ENEMY_COLUMN_COUNT - aliveEnemies) * 0.2; // 敵が減るごとに速度アップ
}
// 敵の移動ロジック、壁にぶつかったときの方向転換を含む
function moveEnemies() {
let edgeReached = false;
for (let enemy of enemies) {
if (enemy.isAlive) {
enemy.posX += movingRight ? currentEnemySpeed : -currentEnemySpeed;
if (enemy.posX + enemy.width >= CANVAS_WIDTH || enemy.posX <= 0) {
edgeReached = true;
}
}
}
if (edgeReached) {
movingRight = !movingRight;
dropEnemies(); // エッジ到達時に敵を下に移動
}
}
// 弾が敵に当たったときに速度を更新
function handleBulletCollisions() {
for (let bullet of bullets) {
for (let enemy of enemies) {
if (enemy.isAlive && checkCollision(bullet, enemy)) {
enemy.isAlive = false;
bullet.posY = -10; // 弾を画面外に移動し消去扱い
updateEnemySpeed(); // 敵が減った後に速度を更新
}
}
}
}
// 敵を少し下に移動
function dropEnemies() {
for (let enemy of enemies) {
enemy.posY += ENEMY_HEIGHT / 3; // 元の1/3だけ下に移動
}
}
// ゲームクリア判定関数
function checkGameClear() {
const remainingEnemies = enemies.filter(enemy => enemy.isAlive).length;
if (remainingEnemies === 0) { // 全てのインベーダーが倒された場合
gameClearFlag = true;
gameRunning = false;
}
}
// キーが押された時のイベント処理
document.addEventListener("keydown", event => {
if (event.key === "Enter" && (gameOverFlag || gameClearFlag)) {
location.reload(); // ゲームをリロードして再スタート
}
switch (event.key) {
case "ArrowLeft":
keys.left = true;
break;
case "ArrowRight":
keys.right = true;
break;
case " ":
keys.space = true;
break;
}
});
// キーが離された時のイベント処理
document.addEventListener("keyup", event => {
switch (event.key) {
case "ArrowLeft":
keys.left = false;
break;
case "ArrowRight":
keys.right = false;
break;
case " ":
keys.space = false;
break;
}
});
// プレイヤーを移動する関数
function movePlayer() {
if (keys.left && player.posX > 0) {
player.posX -= player.speed;
}
if (keys.right && player.posX + player.width < CANVAS_WIDTH) {
player.posX += player.speed;
}
}
// 弾を発射する関数(押しっぱなしで一定間隔で発射)
function handleShooting() {
const now = Date.now();
if (keys.space && now - lastBulletTime > 300) { // 300ms間隔で発射
shootBullet();
lastBulletTime = now;
}
}
// ゲームクリアの確認用関数
function isGameCleared() {
let deadEnemies = 0; // 倒した敵を数える
for (let i = 0; i < enemies.length; i++) {
if (!enemies[i].isAlive) { // 各敵が生きているかどうか毎回チェック
deadEnemies++;
}
}
if (deadEnemies === totalEnemies) { // すべての敵が倒されたか確認
gameClearFlag = true;
} else {
gameClearFlag = false;
}
return gameClearFlag;
}
// ゲームクリア判定関数
function checkGameClear() {
const remainingEnemies = enemies.filter(enemy => enemy.isAlive).length;
if (remainingEnemies === 0) { // 全てのインベーダーが倒された場合
gameClearFlag = true;
gameRunning = false;
}
}
// 衝突処理:弾が敵に当たったとき、毎回ゲームクリア判定を呼ぶ
function handleBulletCollisions() {
for (let bullet of bullets) {
for (let enemy of enemies) {
if (enemy.isAlive && checkCollision(bullet, enemy)) {
enemy.isAlive = false;
bullet.posY = -10; // 弾を画面外に移動し消去扱い
updateEnemySpeed(); // 敵が減った後に速度を更新
checkGameClear(); // 毎回全敵の生存状態を確認し、クリア判定
}
}
}
}
// メッセージの描画関数(ゲームクリアとゲームオーバー用)
function drawMessage(message) {
ctx.fillStyle = "white";
ctx.font = "40px Arial";
ctx.textAlign = "center";
ctx.fillText(message, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2);
ctx.font = "20px Arial";
ctx.fillText("Press Enter to Restart", CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 40);
}
// ゲームオーバー判定関数
function checkGameOver() {
for (let enemy of enemies) {
if (enemy.isAlive && enemy.posY + enemy.height >= player.posY) {
gameOverFlag = true;
gameRunning = false;
}
}
}
generateEnemies(); // 敵を生成して配置
gameLoop(); // メインループ開始
</script>
</body>
</html>
ソース、長いですね!!
いちいちコメントが面白いですw
こういうのを見ると、プログラミングのコメントを全て「サムライ風に変えるAI」とか、やりたくなりますねw
…話が脱線しました。戻しましょう!
ちなみに、ゲーム中の画面はこんな感じです。
インベーダーゲームはよくあるプログラムなので、これぐらいのレベルなら、ChatGPTを利用すれば、(そこそこ面白くする調整も含めて)30分もあれば作成できそうです!
このプログラムをGPT-4oでレビュー
リファクタリングする前に本当に良くないプログラムとして判定されるか?ChatGPTに聞いてみましょう!
プロンプトとしては「下記プログラムのレビューをしてください」という非常にシンプルなものです。
私としては、非常に良くないプログラムを作ったつもりなのですが、レビュー結果は…
プログラム全体の構造がわかりやすく、要素ごとに関数が分けられており、機能ごとの責任が明確に分離されています。また、ゲームの管理や操作に必要な変数や定数の使用も適切で、可読性が高いです。
…らしいです。
えっと…コメントで騙されているのかな?
評価ポイントをしっかり書いた上で、レビューさせないとダメなのかもしれません。
軽くGPT-4oさんにレビューしてもらって「分かりやすい」と評価されたプログラムが、評価や修正ポイントを適切にプロンプトで指示した「o1-prevew」では、どれぐらい良くない部分を探し出して修正してくれるのか?
楽しみになってきました!
o1-prevewでリファクタリング!
さて、先程のプログラムをChatGPT o1-prevewさんにリファクタリングしてもらいましょう!
プロンプトはこんな感じです。
#### 【リファクタリング前のソースコード】を元に、下記のステップ1~ステップ6の手順でリファクタリングし、【リファクタリングされたソースコード】を出力して下さい。
### ステップ 1: コードベースの完全な静的解析
- **インプット**: リファクタリング前のソースコード
- **説明**: 静的解析を用いてコード全体の依存関係やデータフローを把握し、特に共通化や定数化が適切に使われているかチェックします。
- **追加チェック**: 共通化や定数化が適切かを確認し、機能に直結しない抽象化や定数化が複雑化の原因になっていないか確認します。
- **レポート項目**:
1. **依存関係マップ**: クラス、モジュール間の依存関係図。循環依存や双方向依存の箇所を明示。
2. **凝集度・結合度の評価**: 各モジュールやクラスの凝集度と結合度の分析結果。
3. **データフロー図**: データの流れと変更箇所のマッピング。
4. **共通化・定数化の適正評価**: 既存の共通化や定数化が冗長な抽象化になっていないかの評価。
- **アウトプット**: 構造・依存関係・データフローの解析結果
---
### ステップ 2: 凝集度と結合度に基づく機能分割
- **インプット**: 構造・依存関係・データフローの解析結果
- **説明**: 凝集度を高め、結合度を下げるようにモジュールやクラスを再構築。共通化・定数化が無意味な複雑化を引き起こしていないかをチェックします。
- **追加チェック**: 再構築によって共通化が増える場合、各モジュールの役割に対し適切かを検討し、抽象化が簡潔な構造に寄与しているかを確認。
- **レポート項目**:
1. **機能分割マップ**: 凝集度を高めるために再構築されたモジュール・クラスのリスト。
2. **依存関係の最適化案**: 依存関係削減の具体的な対策案。
3. **責務の再割当表**: 各クラス・モジュールの単一責務に収まるよう再定義。
4. **無駄な抽象化の排除結果**: 無意味な共通化や抽象化を削減した結果。
- **アウトプット**: 再構築されたモジュール・クラスの構造設計
---
### ステップ 3: 循環的複雑度を抑えた分岐とロジックの再編成
- **インプット**: 再構築されたモジュール・クラスの構造設計
- **説明**: 分岐やループの構造を単純化し、無駄な定数化が複雑化を招いていないか確認します。
- **追加チェック**: 分岐が不要な場合は除外し、定数化が論理的なメリットを生むかを検討します。
- **レポート項目**:
1. **複雑度分析結果**: 循環的複雑度が高い箇所とその再編成の対象。
2. **分岐・ループの再編成案**: シンプルなロジックに再構成した方法の詳細。
3. **早期リターン戦略の適用箇所**: 不要なケースを除外することで複雑度が減少した箇所。
4. **無駄な定数化の見直し結果**: 必要のない定数化や共通化を省いた結果。
- **アウトプット**: 簡素化された分岐とロジックの設計
---
### ステップ 4: 冗長性と重複の削減
- **インプット**: 簡素化された分岐とロジックの設計
- **説明**: 重複コードを合理的に抽出し、意味のない共通化を避けつつ冗長性を削減します。
- **追加チェック**: 抽象化や共通化が拡張性や保守性を損なっていないか検証します。
- **レポート項目**:
1. **重複コードの抽出結果**: 必要な場合にのみ共通化されたメソッドやクラス。
2. **冗長性の削減戦略**: 冗長性削減のための合理的な共通化とその意図。
3. **共通化の適正評価結果**: 過剰な共通化や定数化を省いた内容。
- **アウトプット**: 冗長性の排除と最適化されたコード設計
---
### ステップ 5: デザインパターンを適用して構造を最適化
- **インプット**: 冗長性の排除と最適化されたコード設計
- **説明**: 必要なデザインパターンのみ適用し、過度な抽象化を排除して構造を簡潔に保ちます。
- **追加チェック**: デザインパターンが適切で、無意味な複雑化や依存関係を増やしていないか確認。
- **レポート項目**:
1. **導入したデザインパターンの一覧**: 必要に応じて適用されたデザインパターン。
2. **依存関係の抽象化案**: 抽象化によって依存関係がどのように改善されたかの詳細。
3. **役割の再割り当て結果**: 最小限のパターン適用により再編成された役割。
- **アウトプット**: デザインパターン適用後のコード構造
---
### ステップ 6: メトリクスを用いた改善効果の定量評価
- **インプット**: デザインパターン適用後のコード構造
- **説明**: 凝集度、結合度、循環的複雑度を再評価し、過剰な共通化・定数化が構造に負担をかけていないか確認。
- **追加チェック**: 共通化や定数化によって複雑さが増加していないか、定量評価の観点から分析。
- **レポート項目**:
1. **メトリクスの評価結果(Before/After)**: 凝集度・結合度・循環的複雑度の評価比較。
2. **再構築の効果分析**: リファクタリングで不要な共通化が削減され、コード品質が向上したかの定量評価。
3. **総合評価**: 適切な共通化レベルが保たれ、簡潔さを維持できたかの分析結果。
- **アウトプット**: リファクタリングされたソースコード
【リファクタリング前のソースコード】
このプロンプトの下に先ほどのソースを貼り付ける感じですね!
解析レポートを出力させるように指示していますので、ステップごとに、それを見ていきましょう!
ステップ 1: コードベースの完全な静的解析
### 1. 依存関係マップ
- **グローバル変数依存**: 多数のグローバル変数(`playerPosX`, `playerPosY`, `bullets`, `enemies`, `gameRunning`など)が存在し、複数の関数で共有されている。これにより、各関数がこれらの変数に依存している。
- **関数間の依存関係**:
- `gameLoop`関数は多数の描画・更新関数(`movePlayer`, `handleShooting`, `drawPlayer`, `drawBullets`, `drawEnemies`, `moveEnemies`, `handleBulletCollisions`, `checkGameOver`)に依存。
- `handleBulletCollisions`は`checkCollision`, `updateEnemySpeed`, `checkGameClear`に依存。
- `document.addEventListener`で定義された匿名関数は、`keys`オブジェクトおよび`location.reload`に依存。
- **循環依存や双方向依存**: 特に明確な循環依存は見られないが、グローバル変数の多用により関数間の依存が強い。
依存度を真面目に解析しているようです!
### 2. 凝集度・結合度の評価
- **凝集度**:
- 多くの関数が単一の責務を持っているが、一部の関数(例: `handleBulletCollisions`)は複数の責務(衝突判定、ゲームクリア判定、速度更新)を持っており、凝集度が低い。
- **結合度**:
- グローバル変数の共有により結合度が高い。特に、`bullets`, `enemies`, `player`などを複数の関数で直接操作している。
凝集度・結合度の評価も妥当そうですね!(たぶん)
### 3. データフロー図
- **プレイヤー入力**: キーボードイベントで`keys`オブジェクトが更新され、`movePlayer`と`handleShooting`で使用。
- **ゲームループ**: `gameLoop`が各種更新・描画関数を呼び出し、キャンバスを更新。
- **弾の生成と移動**: `handleShooting`で弾が生成され、`drawBullets`で移動と描画。
- **敵の生成と移動**: `generateEnemies`で初期生成、`moveEnemies`で移動。
- **衝突判定**: `handleBulletCollisions`で弾と敵の衝突を判定。
データの流れの見える化が出来るのは嬉しいものです!
### 4. 共通化・定数化の適正評価
- **過度な定数化**: すべての数値を定数化しており、コードの可読性を下げている部分がある。
- **無意味な共通化**: `isGameCleared`関数が存在するが、実際には`checkGameClear`で同様の処理を行っており、重複している。
マジックナンバーを恐れるあまり、過度な定数化をしてしまうのは良くないですね!
ステップ 2: 凝集度と結合度に基づく機能分割
### 1. 機能分割マップ
- **クラス化**:
- `Player`クラス
- `Bullet`クラス
- `Enemy`クラス
- **モジュール化**:
- 入力管理モジュール(キーボードイベント)
- 描画モジュール(Canvasの描画関連)
- ゲームロジックモジュール(ゲームループ、状態管理)
このようにまとめてもらえると、凄くわかりやすいです!!
### 2. 依存関係の最適化案
- **グローバル変数の削減**: クラスやモジュール内のプロパティとして管理し、グローバルスコープを汚染しない。
- **データのカプセル化**: 各クラスが自分のデータを持ち、外部から直接アクセスさせない。
インベーダーゲームのようなプログラムにはデータのカプセル化が非常に有効ですよね!
### 3. 責務の再割当表
- **`Player`クラス**: プレイヤーの位置、移動、描画を管理。
- **`Bullet`クラス**: 弾の位置、移動、描画、衝突判定を管理。
- **`Enemy`クラス**: 敵の位置、移動、描画、状態を管理。
- **ゲーム管理クラス**: ゲーム全体の状態、ループ、勝敗判定を管理。
ふむふむ。そのように責務を割り当てるのですね!分かりやすい!!
### 4. 無駄な抽象化の排除結果
- **`isGameCleared`関数の削除**: 重複しているため削除し、`checkGameClear`に機能を統一。
- **過度な定数化の見直し**: 一度しか使わない定数や明らかに理解しやすい値は、直接コード内で使用。
「過度な定数化の見直し」は絶対やって欲しいところでした!嬉しい!
ステップ 3: 循環的複雑度を抑えた分岐とロジックの再編成
### 1. 複雑度分析結果
- **`handleBulletCollisions`関数**: ネストが深く、複数の責務を持っている。
- **`isGameCleared`関数**: 不要なループと条件分岐があり、複雑度が高い。
まずは分析、複数の責務を持った処理を漏らさず分析!
### 2. 分岐・ループの再編成案
- **早期リターンの採用**: 不要なネストを減らし、条件を満たさない場合は早めに処理を終了。
- **関数の単一責務化**: 衝突判定とゲームクリア判定を別々の関数に分割。
複雑になっているものを、どのように修正するか?案出しです。良い感じです!
### 3. 早期リターン戦略の適用箇所
- **`handleBulletCollisions`**: 衝突がない場合は次のループへ。
- **`checkGameOver`**: 敵がプレイヤーの位置に到達した時点で即座にゲームオーバー処理。
再構成案を元にリファクタリングの適用箇所を見つけていきます!
### 4. 無駄な定数化の見直し結果
- 一度しか使われない定数や明確な値は、直接コード内で使用してコードを簡潔化。
これは、ステップ2でも出てきましたね!
ステップ 4: 冗長性と重複の削減
### 1. 重複コードの抽出結果
- **`draw`メソッド**: `Player`, `Bullet`, `Enemy`クラスに共通する描画処理を統一。
- **`move`メソッド**: 移動に関する処理を各クラス内に統一。
同じような処理はひとつにまとめたいものです!
### 2. 冗長性の削減戦略
- **クラスの導入**: オブジェクト指向的に設計し、共通のプロパティやメソッドを持たせる。
- **モジュールの分割**: ゲームロジックと描画ロジックを分離。
それです!インベーダーゲームはオブジェクト指向と非常に相性が良いと思ってました!!
### 3. 共通化の適正評価結果
- **過剰な共通化の排除**: 無意味な関数や使われていないコードを削除。
- **適切な抽象化**: 必要な部分のみ共通化し、各クラスの責務を明確化。
過剰な共通化を排除するという考え方、私は大好きです!!
ステップ 5: デザインパターンを適用して構造を最適化
### 1. 導入したデザインパターンの一覧
- **モジュールパターン**: ゲーム全体のグローバル変数を避けるために適用。
- **ファクトリパターン**: `Bullet`や`Enemy`の生成を統一。
同じものを複製させるのはやはり、ファクトリパターンですよね!
### 2. 依存関係の抽象化案
- **イベント駆動**: キーボード入力などのイベント処理を抽象化。
- **状態管理**: ゲームの状態を一元管理する仕組みを導入。
ゲームにおける「状態管理」って非常に面倒なので、整理してもらえるのは有難いです!
### 3. 役割の再割り当て結果
- **`Game`クラス**: ゲーム全体の状態とループを管理。
- **各エンティティクラス**: 自身の状態と動作を管理。
解析情報を元に、ソース全体に対してリファクタリングを始めた感じですね!
ステップ 6: メトリクスを用いた改善効果の定量評価
### 1. メトリクスの評価結果(Before/After)
- **凝集度**: 各クラスが単一の責務を持つようになり、凝集度が向上。
- **結合度**: グローバル変数の削減により、結合度が低下。
- **循環的複雑度**: 複雑なネストや不要な条件分岐が削減され、複雑度が低下。
@Mさん 、@和田 颯馬 / somawada さんに教えていただいた、凝集度、結合度、循環的複雑度をプロンプトに与えてソースコードの主な評価にしました!いい感じで評価されているようです!
### 2. 再構築の効果分析
- **コード行数の減少**: 冗長なコードの削減により、全体のコード行数が減少。
- **可読性の向上**: クラス化と関数の責務分離により、コードの可読性が向上。
- **保守性の向上**: 各コンポーネントが独立しているため、変更の影響範囲が限定的。
「可読性」と「保守性」は両立させなくてはなりません!
### 3. 総合評価
- **適切な共通化レベルが保たれ、コードが簡潔かつ理解しやすくなった。**
- **過度な抽象化や定数化が排除され、必要な部分のみが最適化された。**
どうやらリファクタリングが終わったようです。結果が楽しみですね!!
リファクタリングされたソースコード
出力結果は下記になりました!!
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>究極のインベーダーゲーム</title>
<style>
/* Canvasのスタイル設定 */
#gameCanvas {
background-color: #333;
border: 5px solid #fefefe;
display: block;
margin: auto;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="400" height="600"></canvas>
<script>
// キャンバスと描画コンテキストを取得
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
// エンティティの基本クラス(プレイヤー、敵、弾丸の共通機能)
class Entity {
constructor(x, y, width, height, color) {
this.x = x; // X座標
this.y = y; // Y座標
this.width = width; // 幅
this.height = height; // 高さ
this.color = color; // 色
}
// エンティティを描画
draw() {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
// プレイヤークラス
class Player extends Entity {
constructor() {
// 親クラスのコンストラクタを呼び出し、初期位置とサイズ、色を設定
super(canvas.width / 2 - 15, canvas.height - 50, 30, 30, 'cyan');
this.speed = 5; // 移動速度
}
// プレイヤーの移動処理
move(keys) {
if (keys.left && this.x > 0) {
this.x -= this.speed; // 左に移動
}
if (keys.right && this.x + this.width < canvas.width) {
this.x += this.speed; // 右に移動
}
}
}
// 弾丸クラス
class Bullet extends Entity {
constructor(x, y) {
super(x, y, 4, 10, 'yellow');
this.speed = 5; // 移動速度
}
// 弾丸の位置を更新
update() {
this.y -= this.speed; // 上に移動
}
// 弾丸が画面外に出たかを判定
offScreen() {
return this.y + this.height < 0;
}
}
// 敵クラス
class Enemy extends Entity {
constructor(x, y) {
super(x, y, 30, 30, 'red');
this.isAlive = true; // 生存状態
}
}
// ゲーム全体を管理するクラス
class Game {
constructor() {
this.player = new Player(); // プレイヤーのインスタンス
this.bullets = []; // 弾丸の配列
this.enemies = []; // 敵の配列
this.keys = { left: false, right: false, space: false }; // キーの状態
this.lastBulletTime = 0; // 最後に弾を発射した時間
this.enemyDirection = 1; // 敵の移動方向(1:右、-1:左)
this.enemySpeed = 1; // 敵の移動速度
this.gameOver = false; // ゲームオーバーフラグ
this.gameClear = false; // ゲームクリアフラグ
this.init(); // 初期化処理
}
// ゲームの初期化
init() {
this.createEnemies(); // 敵を生成
this.bindEvents(); // キーイベントをバインド
this.gameLoop(); // ゲームループを開始
}
// 敵を生成する
createEnemies() {
const rows = 3; // 行数
const cols = 8; // 列数
const padding = 10; // 敵間の余白
const offsetTop = 50; // 上部の余白
const offsetLeft = 20; // 左側の余白
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
let x = col * (30 + padding) + offsetLeft;
let y = row * (30 + padding) + offsetTop;
this.enemies.push(new Enemy(x, y)); // 敵を配列に追加
}
}
}
// キーイベントをバインド
bindEvents() {
document.addEventListener("keydown", event => {
if (event.key === "Enter" && (this.gameOver || this.gameClear)) {
location.reload(); // Enterキーで再スタート
}
switch (event.key) {
case "ArrowLeft":
this.keys.left = true; // 左キーが押された
break;
case "ArrowRight":
this.keys.right = true; // 右キーが押された
break;
case " ":
this.keys.space = true; // スペースキーが押された
break;
}
});
document.addEventListener("keyup", event => {
switch (event.key) {
case "ArrowLeft":
this.keys.left = false; // 左キーが離された
break;
case "ArrowRight":
this.keys.right = false; // 右キーが離された
break;
case " ":
this.keys.space = false; // スペースキーが離された
break;
}
});
}
// ゲームループ
gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 画面をクリア
if (!this.gameOver && !this.gameClear) {
this.player.move(this.keys); // プレイヤーの移動
this.handleShooting(); // 弾の発射処理
this.updateBullets(); // 弾の位置を更新
this.moveEnemies(); // 敵の移動
this.checkCollisions(); // 衝突判定
this.checkGameClear(); // ゲームクリアの判定
this.checkGameOver(); // ゲームオーバーの判定
this.player.draw(); // プレイヤーを描画
this.enemies.forEach(enemy => {
if (enemy.isAlive) {
enemy.draw(); // 生存している敵を描画
}
});
this.bullets.forEach(bullet => bullet.draw()); // 弾を描画
} else {
this.drawMessage(this.gameOver ? "Game Over" : "Game Clear!"); // 終了メッセージ
}
requestAnimationFrame(() => this.gameLoop()); // 次のフレームをリクエスト
}
// 弾の発射処理
handleShooting() {
const now = Date.now();
if (this.keys.space && now - this.lastBulletTime > 300) {
// 弾を生成し、プレイヤーの中央から発射
this.bullets.push(new Bullet(this.player.x + this.player.width / 2 - 2, this.player.y));
this.lastBulletTime = now; // 最後に発射した時間を更新
}
}
// 弾の位置を更新
updateBullets() {
this.bullets.forEach((bullet, index) => {
bullet.update(); // 弾を移動
if (bullet.offScreen()) {
this.bullets.splice(index, 1); // 画面外の弾を削除
}
});
}
// 敵の移動
moveEnemies() {
let edgeReached = false; // 画面端に到達したかどうか
this.enemies.forEach(enemy => {
if (enemy.isAlive) {
enemy.x += this.enemySpeed * this.enemyDirection; // 敵を移動
if (enemy.x + enemy.width >= canvas.width || enemy.x <= 0) {
edgeReached = true; // 端に到達
}
}
});
if (edgeReached) {
this.enemyDirection *= -1; // 移動方向を反転
this.enemies.forEach(enemy => {
enemy.y += enemy.height / 3; // 敵を下に移動
});
}
}
// 衝突判定
checkCollisions() {
this.bullets.forEach((bullet, bIndex) => {
this.enemies.forEach((enemy, eIndex) => {
if (enemy.isAlive && this.isColliding(bullet, enemy)) {
enemy.isAlive = false; // 敵を倒す
this.bullets.splice(bIndex, 1); // 弾を削除
this.updateEnemySpeed(); // 敵の速度を更新
}
});
});
}
// オブジェクト同士の衝突判定
isColliding(obj1, obj2) {
return (
obj1.x < obj2.x + obj2.width &&
obj1.x + obj1.width > obj2.x &&
obj1.y < obj2.y + obj2.height &&
obj1.y + obj1.height > obj2.y
);
}
// 敵の速度を更新(敵が減るごとに速度アップ)
updateEnemySpeed() {
const aliveEnemies = this.enemies.filter(enemy => enemy.isAlive).length;
this.enemySpeed = 1 + (24 - aliveEnemies) * 0.2;
}
// ゲームクリアの判定
checkGameClear() {
if (this.enemies.every(enemy => !enemy.isAlive)) {
this.gameClear = true; // 全ての敵を倒した
}
}
// ゲームオーバーの判定
checkGameOver() {
this.enemies.forEach(enemy => {
if (enemy.isAlive && enemy.y + enemy.height >= this.player.y) {
this.gameOver = true; // 敵がプレイヤーの位置まで到達
}
});
}
// 終了時のメッセージを表示
drawMessage(message) {
ctx.fillStyle = "white";
ctx.font = "40px Arial";
ctx.textAlign = "center";
ctx.fillText(message, canvas.width / 2, canvas.height / 2);
ctx.font = "20px Arial";
ctx.fillText("Press Enter to Restart", canvas.width / 2, canvas.height / 2 + 40);
}
}
// ゲームを開始
const game = new Game();
</script>
</body>
</html>
素晴らしいです!!!
そんなに真面目には見れていませんが、ぱっと見だけでも圧倒的に良いソースコードになったと感じられますね!!
動かしてみましょう!!
おぉ!!
プログラムソースは完全に作り直しのレベルでしたが、全く同じように動いています!
凄い!!
最後にまとめ
今回のリファクタリング対象は、インベーダーシューティングゲームという非常にありがちな題材で行いました。それもあったのか、非常に素晴らしい結果となりました。
ただ、もっと複雑なプログラムになると上手くいくかは…試していないので分かりません。皆さん、ぜひ試してみて下さい!!
このような感じでChatGPTを利用してリファクタリングすることは非常に有用ですが、何が行われているかを、しっかり理解することも非常に重要です。
「正しく動かなくなった?でも、ChatGPTで全自動で作ったから、どうすれば良いか分からない…」
…って、ならないよう、しっかり理解していきましょう!その理解を深めるために ChatGPTに解説してもらうのはアリです!!
最後に…プロンプトとソースファイルを添付いたします!ご活用下さい!