10年以上バグに向き合ってきたエンジニアのコーディングで気を付けていること
はじめに
はじめまして。うっちーと申します。
元ゲームプログラマーのエンジニアです。
エンジニア歴はまだ1年半と未熟者ですが、プログラミングの経験は10年以上となりました。
まだまだ学ぶことが多く、生涯学習とはこのことだなと痛感しています!
ゲームプログラマーだった時は、1人で100件ほどのバグを抱え込んだこともあり
親近感を覚えるほどバグに悩まされ、バグと密接なプログラマーライフを送っていました。
今回は常にバグと向き合ってきた私が日頃考慮しているコーディングを記事にしたいと思います。
「こんなこと、もうわかってるよ!」
と言われがちな内容になっていますが
是非、参考にしていただければと思います。
マジックナンバーは使わない
皆さん数値は好きですか?私は大好きです。
プログラムを制御するには必ず数値がつきものです。
画像の表示位置や、細かい計算式を利用するとき
+100や、*0.8などの数値を使用する事があります。
function calculatePrice(price: number, userType: number): number {
// userTypeの1や2が何を指しているのかわからない。
if (userType === 1) {
// なぜ0.8を掛け合わせているのかわからない。
return price * 0.8;
} else if (userType === 2) {
return price * 0.5;
}
return price;
}
const finalPrice = calculatePrice(1000, 1);
コード内に名前や意味の定義のない数値を
マジックナンバー
と呼びます。
マジックナンバーという名前の由来
コードの中に突如として現れ、あたかも魔法のように根拠不明のまま特定のプログラムの挙動を制御してしまう数値 なのでマジックナンバーと呼ぶようです
マジックナンバーはプログラムの可読性と修正を困難とします。
またエラーに気づけないというデメリットも存在します。
-
可読性の低下
1 や 0.8 という数値が何を意味するのか推察しなければならない。
(1=プレミアム会員? 0.8 = 20%オフ?) -
修正を困難にする
もし0.8が20%オフという定義であった場合
プログラムの全ての0.8を探し、修正する必要がある。
また、その0.8が本当に20%オフの意味をもつのか確認する必要が発生する。 -
エラーに気づけない
指定するべき値を間違って記述してもエラーにならないため
バグの発見が遅れてしまう。magic_number_sample.ts// 1が正しい指定するべき値 if( userType === 11 ){ // 処理内容 }
マジックナンバーの改善案
enum UserStatus {
Premium = 1,
VIP = 2,
}
const PREMIUM_DISCOUNT_RATE = 0.8;
const VIP_DISCOUNT_RATE = 0.5;
function calculatePrice(price: number, status: UserStatus): number {
if (status === UserStatus.Premium) {
return price * PREMIUM_DISCOUNT_RATE;
} else if (status === UserStatus.VIP) {
return price * VIP_DISCOUNT_RATE;
}
return price;
}
数値に意味を持たせるのには、コストが発生します。
忙しい時はそのコストを払う時間が惜しい時もあると思いますが…
数値には意味を持たせるようにし、可読性とメンテナンス性を常に意識したいですね
リファクタリングを大切にする
皆さんリファクタリングは行っているでしょうか。
コーディングをする上で開発後にしたい、行動の1つです。
なるほど…なかなか面倒ですよね。
「機能としてはできているからいいじゃん!」
と言いたくなる気持ちもわかりますが
以下のメリットがあります。
-
技術的負債を防ぐ
急いで修正した内容や、場当たり的な対応(マジックナンバーなど)を放置すると
少しずつコードが複雑怪奇になっていき、だんだん誰も触れない「スパゲティコード」になってしまう。スパゲティコードとは
プログラムの構造が複雑に絡み合いどこを触るとどこに影響が
出るか分からない「収拾がつかない状態」のコードのこと。
絡まってるパスタの麺を1本取ろうと思っても、
他の麺がついてくるように複雑に絡まり合っている様子。 -
「可読性」と「保守性」を保つ
プログラムは一度書いたら終わりではなく、何度も読み返され、修正される- 可読性:他人がコードを見た時、何をしているかがわかりやすい状態
- 保守性:修正が必要な時、影響が少なく修正が可能な状態
リファクタリングを行うことで「解読」に使う時間を短縮し
「開発」に時間を使うことができるようになる。 -
潜在的バグの発見に繋がる
コードを整理する過程で潜在的なバグに気づくことも。
また、構造がシンプルになることでユニットテスト(自動テスト)が書きやすく
品質が向上する。
例えば、以下のようなコードがあるとします
function getPrice(p: number, t: number): number {
// if, else ifが続いており、見づらい。
// 1, 2, 0.9, 0.7 などの意味がコードから読み取れない
if (t === 1) {
return p * 0.9;
} else if (t === 2) {
return p * 0.7;
} else if (t === 3) {
return 0;
}
return p;
}
このコードの問題点は以下のとおりです。
- pやtなど、変数名から用途が読み取れない
- tがnumber型のため、型の安全性が低い(1〜3以外の値が渡される可能性がある)
- 条件分岐が続きネストが多くなり見づらい
- 0.9 や 0.7などのマジックナンバーが存在する。
これを、リファクタリングしてみます。
// 会員ランクを定義
type UserRank = 'Standard' | 'Premium' | 'VIP' | 'Staff';
// 割引率を定数化(マジックナンバーの排除)
const DISCOUNT_RATES: Record<UserRank, number> = {
Standard: 1.0,
Premium: 0.9,
VIP: 0.7,
Staff: 0.0,
};
/**
* ユーザーのランクに応じた最終価格を計算する
*/
function calculateFinalPrice(basePrice: number, rank: UserRank): number {
const rate = DISCOUNT_RATES[rank];
return basePrice * rate;
}
どうでしょうか、calculateFinalPriceメソッド内が整理されました。
- 変数名から変数の用途が汲み取れる
- UserRank型を設け、型の安全性が保たれている
- Recordを利用することで、数値に意味を持たせると同時に条件分岐が不要に。
数値に定義が行われ誰が見てもコード作成者の意図が汲み取れるコードになりました。
リファクタリングのメリットはあるものの、コストは低くはなく
私もなかなかできていません。
その理由としては以下の部分がネックになることが多いと考えています。
- 内部構造を変更して新たにバグが発生するのが怖い
- 納期と工数を守るため時間を割けない
- 「他の人のここを治してバグがでたら…」という不安感
リファクタリングやらなきゃと思いつつも、上記の理由から
「やらない理由」を作ってしまいがちです。
どうすれば「できる」?
リファクタリングの不安を取り除き、実績をつみ、リファクタリングに価値を感じることだと考えています。
リファクタリングは重要な工程ですが
開発の納期や、挙動は既にできているものを修正する不安のために
ついつい放置してしまいそうになります。
ですが、リファクタリングの価値を再確認して
コードの品質を保つのもエンジニアの重要な作業と考えています。
シンプルな書き方を忘れない
誰かが作ったコードを見た時、誰もが1度は経験したことはないでしょうか。
プログラム言語の進化は凄まじく、日々新しい技術が生まれています。
新しいコーディングを覚えることでスマートに、美しく効率的に
コーディングすることができるようになりました。
簡単な例として
「配列の中の偶数の数値のみ抽出し、値を2倍にして結果を出力する」
というコードがあります。
const numbers = [1, 2, 3, 4, 5];
const result = [];
// numbersの中身を処理したい。
for (let i = 0; i < numbers.length; i++) {
const n = numbers[i];
// 偶数か?
if (n % 2 === 0) {
const doubled = n * 2; // 偶数なら2倍にして
result.push(doubled); // 結果をリストに入れる
}
}
この書き方はいわゆるシンプルな書き方です。
コードが長くなってしまうというデメリットはあるものの、
シンプルだからこそ以下のメリットがあります。
この処理を新しい書き方に変更すると…
const numbers = [1, 2, 3, 4, 5];
// 偶数のみをフィルタし、その結果全てに2倍にした配列をresultへ格納。
const result = numbers.filter(n => n % 2 === 0).map(n => n * 2);
1行で同じ処理を作成することができました!
メソッドチェーンを利用し結果を抽出しています。
これはスマートで綺麗な処理ですが、以下の問題点も孕んでいます。
1行でスマートに書けるが、内部で何が起きているかは抽象化されています。
これは時代が進み、言語にもサポート関数が用意され
コーディングの方法も生産性を重視し、関数の中の処理は信頼できるものとして
抽象化されてきていると考えます。
どちらにも、メリット・デメリットがあり、論争が発生しやすい点です。
私の考え方としては、
個人開発のコードは自分のみが読み返すため理解が可能なので
どんどん新しい技術を利用しても良いかと思います。
チーム開発では、他人がコードを確認することを前提にコーディングすることも大切になってきます。
そのため、チームでコーディング規約を作成する必要が発生します。
これからも技術が進化していく中で、生産性を高めつつ可読性・保守性が高まるよう
新しい技術と古い技術のいいとこどりをしていきたいと考えています。
バグは教師という考え方
皆さんはバグが発生してしまった時、どう考えますか?
- バグの数が多くて開発に手が回らない!どうしよう!
- バグの原因がわからない…納期もせまっているし焦ってしまう
- こんな簡単なバグを出してしまうなんて、もうダメだ…
自分が正しいと思って記述したコードがバグを起こした時
ネガティブな思考が頭をよぎります。
嫌でも自分の未熟さに気付かされてしまいます
自分のミスではない関連で発生した内容でも 「もう少し考慮できたのでは」 と
感じてしまうこともあります。
私がゲームプログラマーだった時に
バグ(不具合)は色々教えてくれることに気づきました。
そして、私はバグを教師と考えるようになりました。
リリース前に発生したバグは開発物をさらに良いものにできるチャンスであり
なおかつ 自分を成長させてくれる ものと考えています。
バグが発生した時は、チャンスと捉えてどんどん解消していきましょう!
最後に
私がコーディングで気をつけていることをまとめてみました。
特に最後に紹介したバグを教師とするという考え方は
1人でバグを100件以上抱えてしまった経験からドM的な考えなのかもしれません…
しかし、バグを教師にしたからこそリファクタリングを大切にすることや、シンプルに開発するということの大切さに気づけました。
皆さんも自分のコーディングで大切にすることを見つけてみてください。
それでは、これからもいいコーディングライフを!
Discussion