クリーンコードとは、高品質で効率的にコーディングする技術のことです。
プログラミングスキルを向上させたい、またはより良い開発者になりたいと考える人にとって、この概念はとても重要になります。
多くの開発者が高品質のコードを書いていないことはよく知られています(自分も含めて)。
これは、十分な研修が行われていないか、もしくは限られた予算のため、リリースを優先した際に発生しがちです。
その結果、プロジェクト開始から半年後には、大きな技術的負債が蓄積されてしまうことがあります。
開発者は、常に高品質のコードを心がけるべきです。
最初にお伝えすると、負債なコードを溜めないためには、以下のような方法が有効です。
- リファクタリング: 定期的にコードのリファクタリングを行い、複雑さを管理し、可読性を高めます
- コードレビュー: 他の開発者によるコードレビューを実施して、問題点を早期に発見し、修正します
- 単体テスト: 単体テストを充実させ、コードの変更が既存の機能に悪影響を与えないようにします
- ドキュメントの整備: コードだけでなく、ドキュメントも適切に更新し、後からコードを理解しやすくします
- プリンシプルとガイドラインの遵守: 開発チームで共有されたプログラミングプリンシプルやスタイルガイドに従って、一貫性のあるコードを書きます
- 技術的負債の認識と管理: 技術的負債を認識し、それを管理するための計画を立て、定期的に見直しを行います
これらの実践を通じて、コードを健全に保ち、将来的な問題の発生を防ぐことができます。
そこで、今回は自らの体験や資料と書籍を元によりリファクタリングとコードレビューの部分を詳細に落とし込んで開発時に発生するミスと改善例を前半/後半として記事にまとめてみました。
品質の高いコードの特徴
品質の高いコードには共通の特徴があります。
- コードに表現力があり、その動作は明確で、誰もがその機能を理解しています
- どのように機能するかが明白であるべきです
- コードは洗練されており、「美しいコード」と言えるでしょう
誰でもコンピュータが理解できるコードを書くことができますが、優れたプログラマーは人間が理解できるコードを書きます。
マーティン・ファウラーによると、自分が書いたばかりのコードを読み返して理解する必要がある場>合、そのコードはおそらく複雑すぎるとのことです。
自分が書いた人にとって複雑なら、後でそのコードを引き継ぐ開発者にとってはさらに理解が難しいでしょう。
技術的負債と品質の放棄
コードが古くなるほど、多くの開発者によって変更される傾向があります。
コードが変更されればされるほど、バグや不具合が含まれる可能性が高まります。
これから技術的な負債が始まっていきます。
しかし、コードが損なわれるほど、後から来る開発者がコードの品質を気にする傾向は低くなります。
簡単な例としては、「古い」コードの保守で、誰もがそのコードを使いたがりません。
「コードが古すぎてバージョンが更新されておらず、多くのリグレッションがあるため、至る所にテープを貼って対処しています。」
この状況を体験した人は数多くいることでしょう。
さらに、コードが汚れているほど、その上に汚いコードを追加することに抵抗が少なくなります。
「どうせ現状のソースコードはもうダメだから...自分も少しぐらい...」という考え方です。
これは「割れ窓理論」という心理的バイアスです。
レガシーコードの管理
レガシーコードとは、古くてメンテナンスされていない、または最新の状態ではなく、古いツールを使用しているコードのことです。
普通の感覚だと、誰も扱いたくないようなコードです。例えば一つのファイルに数千行ぐらいのコードがあり、どのメソッドが今も使用されているかわからないようなコードです。
クリーンコードは、読みやすく健全な基盤のあるコードとそうでないコードの区別をしません。
どこでも品質を保つことが求められます。
守るべきルールは次の通りです。ボーイスカウトには大切なルール
「見つけたときよりも良い状態でコードを残すこと」
ボブおじさんことアンクル・ボブ・マーティンによると、小さなリファクタリングを行うことも含まれています。
関数を分割する、変数の名前を変更する、新しいクラスを作成するなど...
要は、コードにもボーイスカウト精神が必要ということです。
手を加えた後にコードが、少しでもよりクリーンになっていればOKです。
悪いコードからクリーンなコードを作り出すことも、クリーンコードであることの一環です。
リファクタリングと名前の変更
概念や変数をより明確にするため、積極的に名前を変更することをためらわないでください。
小さな改善でも役立ちます。
ただし、名前を変更する際には、単に変更するためではなく、具体的な目的が必要です(そのためにはチームの助けが非常に役立ちます)。
また、ある変数の名前を変更する場合は、作業しているファイルだけでなく、プロジェクト全体で一貫して行う必要があります。
ビジネスの側面を簡単に名前変更するために、IDEを活用してください。
例えば、ある変数名、クラス名やメソッド名がビジネスの用語として不適切であると判断した場合、IDEのリファクタリング機能を使ってその名前を変更すると、その名前が使われているすべての場所が自動的に更新されます。これにより、手動でひとつひとつのファイルを修正するよりも迅速かつ正確に作業を行うことができ、エラーのリスクを減らすことができます。
関数名はその意図を表す
関数の名前はその意図を表すべきです。
例えば、「calculate()
」という名前だけでは、何を計算しているのかが分かりません。
しかし、「getTax(): int
」や「calculateShippingPrice(): float
」といったメソッド名では、その名前からメソッドが何をするのかが明確に理解できます。
「getTax()
」は消費税を取得すること、「calculateShippingPrice()
」は送料を計算することを示しています。
このように、メソッドの名前に具体的な動作や目的を含めることで、コードの可読性が向上し、他の開発者がコードを理解しやすくなります。
サンプルコード
React
とTypeScript
を使用して、関数名がその意図を明確に表す具体的なコード例を以下に示します。
import React from 'react'
interface Product {
price: number
quantity: number
}
interface Props {
products: Product[]
}
const TAX_RATE = 0.1 // 消費税率10%
// 消費税を計算する関数
const calculateTax = (total: number) => {
return total * TAX_RATE
}
// 送料を計算する関数
const calculateShippingCost = (total: number) => {
if (total > 5000) {
return 0 // 合計が5000円以上の場合は送料無料
}
return 500 // それ以外は送料500円
}
// ショッピングカートの合計を計算する関数
const calculateTotal = (products: Product[]) => {
return products.reduce(
(acc, product) => acc + product.price * product.quantity,
0
)
}
const ShoppingCart: React.FC<Props> = ({ products }) => {
const subtotal = calculateTotal(products)
const tax = calculateTax(subtotal)
const shippingCost = calculateShippingCost(subtotal)
const total = subtotal + tax + shippingCost
return (
<div>
<h2>ショッピングカート</h2>
<p>小計: ¥{subtotal.toFixed(2)}</p>
<p>消費税: ¥{tax.toFixed(2)}</p>
<p>送料: ¥{shippingCost.toFixed(2)}</p>
<p>合計: ¥{total.toFixed(2)}</p>
</div>
)
}
export default ShoppingCart
これらの関数名は、それぞれが何を行うのかを明確に示しており、コードの可読性を高めています。
また、TypeScriptの型注釈を使用することで、関数の入出力が期待通りの型であることを保証し、エラーのリスクを減らしています。
クラスと関数の命名
変数の命名は非常に重要ですが、行動に適切な名前を付けるのはとてもとても難しい作業です。
- 変数を命名する際は、本質に迫るよう努めましょう
- 変数の名前と目的を一目で理解できるように表現します
- 名前が少し長くなっても恐れずに
- 例:
HTTP_KO
=>HTTP_INTERNAL_SERVER_ERROR
- 例:
- 同義語を避け、一貫した用語を使用する
- 例:
getAllUsers()
,fetchAllUsers()
,retrieveAllUsers()
- 例:
- 単純な言葉を優先します
- 完璧な名前は存在しません
- その時点で最善を尽くし、リファクタリングで改善します)
- 一文字の変数名は避けてください
- 例:
for (let i = 0; i < arr.length; i++)
はfor (let index = 0; index < usersArray.length; index++)
に変更
- 例:
- 現在のプロジェクトの命名規則を使用。(たとえそれが最適でなくても_
- フロントエンドとバックエンドでエンティティを同じように命名する(例:フロントで
Employee
、バックでUser
とすると混乱します) - コードの品質は、その理解のしやすさ、特に命名によって測定できます
コードの型付け
変数や関数の型ヒントは、理解や全体的な可読性、IDEの自動補完機能にとって非常に重要です。
JavaScriptは、初期段階でコードの型付けを行っていませんでした。後に型付けを導入し、TypeScriptが登場しました。
可能な限りコードに型ヒントを付けましょう。
またextends
を使用することでコードの可読性と品質が向上します。型付けが必ずしもコード品質の同義語ではありませんが、エラーのリスクを限定し、コードをより理解しやすくするのに大いに役立ちます。
extends
はクラスが別のクラスを継承する際に使用され、基底クラスのプロパティやメソッドを引き継ぎます。これにより、コードの重複を避け、より整理された構造を作ることができます。
型付けは、変数や関数の入出力が期待通りの型であることを保証することで、実行時の型関連のエラーを防ぎます。これにより、開発者はコードの意図をより明確にし、他の開発者がそのコードを理解しやすくなります。
定数と設定ファイル
定数要素を保存する方法は2つあります。
- コード全体で要素を使用したい場合は定数を使用します
- 例:
Response.INTERNAL_SERVER_ERROR
- コードの動作が設定に依存する場合は設定ファイルを使用します
- 例:
webservices.name_of_company.users = 'https://...'
設定ファイルを使用すると、環境が変わる際に情報を変更することもできます。
定数はプロジェクト全体でグローバルかつ不変です。
定数の使用例
定数を管理するためのファイルを作成します。例えば、HTTPステータスコードを定数として持つことができます。
export const HTTP_STATUS = {
INTERNAL_SERVER_ERROR: 500,
NOT_FOUND: 404,
OK: 200
};
この定数はアプリケーション全体でインポートして使用することができます。
設定ファイルの使用例
設定ファイルは、環境に依存する値を外部から注入できるようにします。例えば、開発環境と本番環境で異なるAPIエンドポイントを使用する場合があります。
config.json
ファイルを作成し、異なる環境の設定を記述します。
{
"development": {
"API_ENDPOINT": "https://dev.api.example.com"
},
"production": {
"API_ENDPOINT": "https://api.example.com"
}
}
React コンポーネントでの使用例
React コンポーネント内でこれらの定数と設定を使用する例を示します。
import configData from './config.json';
const environment = process.env.NODE_ENV || 'development'; // 環境変数から環境を取得
export const config = configData[environment];
import React from 'react'
import { HTTP_STATUS } from './constants'
import { config } from './config'
export const Users = async () => {
const { data, error } = await fetch(`${config.API_ENDPOINT}/users`).then(
(response) => {
if (response.status === HTTP_STATUS.OK) {
return response.json()
} else if (response.status === HTTP_STATUS.INTERNAL_SERVER_ERROR) {
throw new Error('Internal Server Error')
}
}
)
if (error) {
return <div>エラー: {error}</div>
}
return (
<div>{data ? <div>データ取得成功</div> : <div>ローディング中...</div>}</div>
)
}
この例では、APIエンドポイントのURLが環境によって異なるため、設定ファイルから適切なURLを取得しています。また、HTTPステータスコードは定数ファイルから取得しています。
このように、定数と設定ファイルを適切に使用することで、コードの再利用性とメンテナンス性を向上させることができます。
他の人が理解しやすいアプローチを選ぶべき
コードを書く際に不必要に複雑、またはあまりにも洗練された方法を使わないようにして、よりシンプルで他の人が理解しやすいアプローチを選ぶべきです。
これにより、他の開発者がコードを読んだり、メンテナンスしたりする際に容易になります。
関数型プログラミングは、文字列を使って出力を取得する便利な方法を提供しますが、過度に使用すると、後からコードを読む開発者にとって理解が難しくなります。
例えば、以下のようなコードは避けてください。
return data.map((d) => [Object.keys(d)[0], Object.values(d)[0]].map(encodeURIComponent).join('=')).join('&');
関数の連鎖を過度に行わないようにしてください。
連鎖操作のパフォーマンスに注意してください(例えば、for ... of ... は .forEach() よりもずっと速いです)。
if文で単純な等号を使用しないようにしてください(例:user = getAllUser()
)。これは混乱を招き、誤解される可能性があります。その場合、代入と条件チェックを明確に分けるべきです。
const user = getAllUser();
if (user) {
// userがnullまたはfalseでない場合に実行されるコード
}
使用している技術のコーディング標準に従ってください。
複雑なことをしようとしないでください。複雑なコードを書くことが好きかもしれませんが、シンプルさが重要です。
「偶発的な複雑さ」と「プレオプティマイゼーション」
偶発的な複雑さとは、単純な問題を解決しようとする際に、不必要に複雑なコードを書いてしまうことを指します。
(ほとんどの場合)早すぎる最適化は、プログラミングにおけるすべての悪の根源である
ドナルド・クヌースによると上記のようです。
早く最適化しようとすること(新しいクラス、メソッド、抽象化を作成するなど)によって、必要のない複雑さが加わってしまいます。
したがって、注意が必要です。常に最もシンプルな方法を取るべきです。
最も重要なのは、チームによるコードの理解です。
コードが「スタイリッシュ」であることではありません。
エラー管理
例外を再スローするか、try/catchを使用するかは、使用するケースによって常に複雑です。ここにいくつかのルールを示しますので、より明確に判断できるようになるでしょう。
- 関数内で何かをテストしている場合は、例外を再スローしないでください
- 例:
isUserUnEmployed()
はtrue
またはfalse
を返すべきです
- 例:
- 関数がある種の検証や確認を行い、その結果が期待通りでない場合には、問題を示すために例外をスローしてください
- 関数やメソッドが受け取る引数に対してある前提条件や期待がある場合、その前提が満たされないときに例外を投げてください
- 例外は定義上、例外的なものですので、すべての関数で例外を再スローするのは避けてください
- 何か重大なことが起こった場合や、無視できないイベントを通知する必要がある場合に例外を使用します
- 独自の例外を作成してください
悪いコード
この例では、例外を適切に処理せず、エラーが発生した場合にユーザーに何が起こったのかを明確に伝えていません。
import React, { useEffect, useState } from 'react';
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => setUser(data))
.catch(error => {
console.log('An error occurred:', error);
});
}, [userId]);
if (!user) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
良いコード
この例では、エラーを適切に処理し、ユーザーにフィードバックを提供しています。また、エラーが発生した場合には、その情報をUIに表示しています。
import React, { useEffect, useState } from 'react';
interface User {
name: string;
email: string;
}
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return response.json();
})
.then(data => setUser(data))
.catch(error => {
setError(error.message);
});
}, [userId]);
if (error) {
return <div>Error: {error}</div>;
}
if (!user) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
この良いコード例では、以下の改善点があります。
- ネットワークリクエストが失敗した場合に例外をスローしています
- エラーメッセージを状態として保持し、UIで表示してユーザーに通知しています
後半へ続く
- 開発の原則について覚えておくべきこと
- SRP(単一責任の原則)
- KIS(シンプルに保つ)
- DRY(繰り返しを避ける)
- YAGNI(必要になるまで作らない)
- SOC(関心の分離)
- ACID(原子性、一貫性、独立性、耐久性)
- IRI(意図を明らかにするインターフェース)
- シンプルなデザインのための4つのルール
- リンターとコーディングスタンダード
- コードのクリーニング
- コードのコメントの正しい方法
- コード内のコメントのリスク
- ファイルの分割と行の長さ