はじめに
これは文系出身の私が、入社後に早く教えて欲しかったことをまとめたものです。
私は 10 年ほど前に文系大学を卒業して技術職で採用されましたが、入社 1 年も経たないうちに「使えない」と言われ、お客様向けのサポートセンター業務に就くことになりました。
その間も個人ではコーディングを続け、開発チームに厄介な口出しなどしているうちに、いつの間にか技術職に戻っていました。
プログラミング未経験者のよくある回り道をした身として、同じような境遇の方の助けになれば幸いです。
以下はすぐに使えるものから、未経験の方には難しい内容まで含まれます。
すぐに分からなくても知ってさえいれば後々効いてくる内容にしたつもりですので、「そのうち分かるかな」くらいの気持ちで読んでもらえればと思います。
👑 コーディングの基礎テクニック
初学者に役立つ汎用的なテクニックをまとめます。
特に説明のない限り、コード例は JavaScript を使用します。
(そのため、ブラウザ上で簡単に試すことができます。)
✔ 読みやすさを優先する
コードは他の人が最短時間で理解できるように書かなければいけない。
― 『リーダブルコード』、Dustin Boswell、Trevor Foucher
良いコードとは「他人が理解しやすいコード」のことです。
他人に確実に伝えるには、コードを読む場合への配慮が必要になります。
配慮されたコードなら、短い時間で読めて、バグも発見しやすく、修正にも拡張にも悩まずに済みます。
✔ 適切な名前を付ける
ネーミングは設計の中心である。
― 『レガシーコード改善ガイド』、マイケル・C・フェザーズ
コードを無茶苦茶にする方法のひとつは、名前を付ける時に「おざなりな名前」を使うことです。
「変数名」や「メソッド名」、あるいは「オブジェクト名」や「機能名」など、名前に配慮が足りない場合はどうなるでしょうか。
たった数分の「命名の手間」を惜しむばかりに、適当な名前があらぬ誤解や混乱を生み、新たなバグを作るかもしれません。
あるいは、手遅れになる前に不適切な設計に気づけたかもしれません。
それに後からコードを読む時間というのも案外馬鹿になりません。
長期的な観点でのメリットを優先し、命名の時間を惜しむべきではありません。
ここで急いでもロクなことにはならないのです。
🔖 命名規則に従う
当然ながら、コードを書くにはプロジェクトの定める「コーディング規則」に従う必要があります。
中でも重要な「命名規則」は、複合語をひとつの名前に結合して使うためのルールです。
大抵の場合、このルールには以下のいずれかのパターンが採用されます。
名称 | 別名 | 例 | 説明 |
---|---|---|---|
ローワーキャメルケース | キャメルケース | lowerCamelCase |
単語は先頭が大文字、他は小文字。 ただし、最初の単語の先頭のみ小文字 |
アッパーキャメルケース | パスカルケース | UpperCamelCase |
単語は先頭が大文字、他は小文字 |
スネークケース | - | snake_case |
すべて小文字で、単語をアンダースコア (_ ) で区切る |
コンスタントケース | - | CONSTANT_CASE |
すべて大文字で、単語をアンダースコア (_ ) で区切る |
ケバブケース | チェインケース | kebab-case |
すべて小文字で、単語をハイフン (- ) で区切る |
例えば、JavaScript のコーディング規則として「Google JavaScript Style Guide」に準拠するなら、以下のような命名規則に従うことになります。
-
クラス名:
UpperCamelCase
- 「名詞」を使う。(基本的に単数形であるべきです。)
- e.g.
Request
、ImmutableList
、VisibilityMode
-
メソッド名、関数名:
lowerCamelCase
- 「動詞」を使う。
- プライベートメソッドなら、末尾にアンダースコア (
_
) を付与する。 - e.g.
sendMessage
、stop_
-
定数名:
CONSTANT_CASE
- e.g.
IS_DEBUG
- e.g.
-
変数名:
lowerCamelCase
- 数が可変の引数の場合:
var_args
(ただし、参照する場合はarguments
オブジェクトを参照すること) - 省略可能な引数の場合:
opt_
のプレフィックスを付与
- 数が可変の引数の場合:
-
ファイル名:
- すべて小文字。
- コロンなどの英数字以外(パンクチュエーション)は使ってならない。
ただし、アンダースコア (_
) とハイフン (-
) のみは使っても良い。
残念ながらコーディング規則がまだないプロジェクトなら、コーディング規則は自分で用意する必要があります。
そのためには「世間一般でよく使われるベストプラクティス」をベースに、「既存のコードで多く使われるパターン」をすり合わせる作業が必要になります。
🔖 文脈を意識する
「使い捨てコード」でもない限り、既存のコードの持つ「文脈」を無視した名前を付けてはいけません。
例えば以下の Bad の例では、名前から何の値かを想像できません。
これらの値が DB / アプリケーション / Web の 3 つのサーバーの設定であることを示すには、Good の例のように明示的な名前が必要になります。
// 👎 Bad: 名前から何も伝わらない
const HN = 'localhost';
const PN = '5432';
const HN2 = 'localhost';
const PN2 = '8080';
const HN3 = 'localhost';
const PN3 = '80';
// 👍 Good: 名前から文脈まで想像できる
const DB_HOST = 'localhost';
const DB_PORT = '5432';
const APP_HOST = 'localhost';
const APP_PORT = '8080';
const WEB_HOST = 'localhost';
const WEB_PORT = '80';
文脈を明示的にする名前には、以下のような特徴があります。
- ➊ 意味が通じる単語を持つこと
- 無闇に省略してはいけません。
- ➋ 名前同士が衝突しないようにすること
- 名前に十分な意味を持たせることで、将来的にも重複を回避しやすくなります。
- ➌ 同じグループの名前には一定のパターンを持たせること
- 上記の Good の場合なら
{サーバーの役割}_{値の種類}
という命名規則を持ちます。
- 上記の Good の場合なら
🔖 具体的に説明する
名前に説明的な情報を付与することは、ケアレスミスを防ぐのに非常に効果的です。
// 👎 Bad: ユーザー ID 以外の ID が登場した時、ID の取り違えが発生するかもしれない
id = getUserId();
// 👍 Good: ユーザー ID であることを明示
userId = getUserId();
// 👎 Bad: 単位を明示しないと、誤って単位を揃えるのを忘れるかもしれない。秒 (sec)、ミリ秒 (ms)、マイクロ秒 (us)、ナノ秒 (ns) など
start = Date.now();
// 👍 Good: ミリ秒であることを明示(特に「時間」や「ファイルサイズ」に関する単位に有用な方法)
start_ms = Date.now();
// 👎 Bad: 平文パスワードの変数名を password とすると、ハッシュ化したパスワードをどう命名する?
password = 'password';
// 👍 Good: 平文パスワードであることを明示。これなら、ハッシュ化されたパスワードの変数を hashedPassword とできる
plaintextPassword = 'password';
// 👎 Bad: 加工が必要なことが分かっている場合、目印がないと忘れてしまうかもしれない
input = 'foo<br>bar'; // 後で HTML 特殊文字をエスケープして "foo<br>bar" に加工する必要がある
// 👍 Good: 加工前の生データであることを明示
raw_input = 'foo<br>bar';
🔖 名前で以上・以下・未満を示す
範囲を指定する場合、指定した範囲を含めるかどうか(「以下」なのか?「未満」なのか?など)は、コーディングにおいてかなり神経を使う場面です。
このような場面で慣例的に使われるパターンを見てみましょう。
// 指定行数の範囲にある行を取得する
const firstRow = 10;
const lastRow = 12;
const rows = getRowsInRange(firstRow, lastRow);
上記のように first
/ last
という単語を含む変数は、範囲に指定した値も含むことを示す際によく使われます。
他にも min
/ max
なら、同じく「以上」や「以下」のように指定した値を範囲を含みつつ、更に「上限」や「下限」であることを強調できます。
これらの単語は、使えそうな場面なら積極的に使いましょう。
また、「未満」を示したい場合は、以下のことを知っておくと便利です。
/** 指定日時の範囲にある会議を取得する */
function getMeetingsInRange(begin, end) {
// 略: begin と end がどのような値なら使いやすい関数となるか?
}
この関数は、引数の begin
は「以上」を示す(指定した値が範囲に含まれる)のが良さそうです。
しかし、引数の end
は「未満」を示す(指定される値は範囲に含まれない)方が、実は都合が良いのです。
「以上 / 以下」の組み合わせより、「以上 / 未満」の方が最適なのは、直感に反するかもしれません。
では、実際にこの関数を使う場面を見てみましょう。
//(前提)12 月 31 日の「1 日分」の会議を取得したい場合:
// 👎 Bad: 「以上 / 以下」の場合
getMeetingsInRange(
new Date('2000-12-31T00:00:00'), // 以上
new Date('2000-12-31T23:59:59.999') // 以下。どこまで精度を求める?秒?ミリ秒?マイクロ秒?それ以上?(Date 型の最大精度はミリ秒)
);
// 👍 Good: 「以上 / 未満」の場合
getMeetingsInRange(
new Date('2000-12-31T00:00:00'), // 以上
new Date('2001-01-01T00:00:00') // 未満。00:00:00 は指定範囲に含まず、その直前までを範囲とすることを期待
);
Bad の例のように end
が「以下」を示す場合だと、この関数を使う度に 23:59:59.999...
のような半端な値が必要になってしまいます。
また、23:59
のように秒単位の精度しか指定されないなら、23:59.001
は範囲に入らないなど、時間精度に関する厄介な問題が発生します。
対して Good の例のように end
を「未満」とすれば 00:00:00
と指定できて、キリの良い値を使うことができます。
これは日付などの範囲を扱う場合によく使うので、覚えておきましょう。
さて、勘の良い方は end
は「未満」の意味を持たない単語であることに気付いたかもしれません。
その end
が「未満」を示す変数名に選ばれやすいのは、プログラミングにおける命名規則の慣例によるものです。
つまり他に適切な短い英単語が存在しないため、慣例的に begin
と end
が対で「以上 / 未満」を意味する名前として使われるのです。
このように、名前は歴史的な経緯を持つことがあります。
🔖 明確な単語を選ぶ ➊: 基本編
例えば関数名に getPage()
という名前を思いついたとして、文脈に合わせたもっと良い名前がないか、検討してみましょう。
-
getPage
: 最も広い意味での取得する。あるいは、アクセサー(オブジェクト内にアクセスする手段)として、プロパティから値を取得する -
fetchPage
: ネットワークなどの離れた場所にあるものから取得する -
downloadPage
: ダウンロードしてファイル出力する -
loadPage
: 読み出してメモリ上に置く -
findPage
: 検索してページを取得する(探すものが page)- 他動詞の
find{目的語}
で、目的語が「探すもの」を示す。
- 他動詞の
-
searchPage
: ページ内を検索して検索結果を取得する(探す場所が page)- 他動詞の
search{目的語}
で、目的語が「探す場所」を示す。 - ちなみに自動詞(直接目的語をとらない)の
searchFor{名詞}
なら、「探すもの」を示すこともできる。
また、for
の代わりに別の前置詞(into/after/among/through/in
など)を使うことで、表現の幅が広がる。
- 他動詞の
-
selectPage
: コレクション(データのまとまり)から、ひとつを選んで取得する
単語ひとつで、このように多彩なニュアンスを表現ができます。
文脈上でコードの示す意味が明確になる単語を選択しましょう。
もし適切な単語選びに迷ったら、その度に立ち止まって調べましょう。
以下のような「まとめ記事」を一読しておくことも有用です。
- cf. プログラミングでよく使う英単語のまとめ【随時更新】 - Qiita
- cf. うまくメソッド名を付けるための参考情報 - Qiita
- cf. コンピュータサイエンスにおける難題、名付け。本気で良い名付けをしたい - 名付けにまつわる議論とヒント - Qiita
🔖 明確な単語を選ぶ ➋: 応用編
上記を踏まえて、文字列を切り取る関数名に clip()
(切り取る)と命名したとします。
一見、「切り取る」という単語の意味は明瞭に見えます。
しかし、「どう切り取るのか?」に言及されていないので、誤った使い方をされるかもしれません。
こういうこともバグの原因になるものです。
では、omitLongText()
(長いテキストを省略する)という関数名ならどうでしょうか。
「省略する」という単語によって、文字列の末尾から切り取られることが想像できますし、省略される条件が「長いテキスト」であることも明確です。
さらに、この関数の引数について考えてみましょう。
// 👎 Bad: 引数の length はどういう長さ?
function omitLongText(text, length) {
// 略
}
// 👍 Good: 引数が maxChars なら、最大文字数だと確実に伝わる
function omitLongText(text, maxChars) {
if (text.length <= maxChars) return text;
return `${text.slice(0, maxLength)}…`;
}
引数が length
だと、「どういう意味の長さか?」が説明されていません。(切り取る長さ or 残す長さ?何の長さ?)
これは、許容する文字数の「上限」であることを示す maxLength
にすれば、意味がもっと明瞭になります。
更にもっと具体的に maxChars
(最大文字数)とすれば、maxByte
(最大バイト数)などの別の長さと区別できるようになります。
補足:
命名の話からは脱線しますが、上記の omitLongText()
関数の実装には難があります。
この実装では、「見かけ上の 1 文字」と「内部上の 1 文字(コードポイント)」がずれるケースなどを考慮していません。
length
プロパティで文字数をカウントすべきかは、用途に合わせて検討が必要です。
詳しい検討は、以下の記事などを参考にしてください。
🔖 info や data を避ける
info
や data
は慣例的によく使われる単語ですが、名前の意味が曖昧になるのでなるべく採用してはいけません。
以下の単語のように、文脈に応じて適切な単語で代替できないか検討すべきです。
-
elements
/elems
... 要素(HTML 要素など)- ひとつの要素は、配列などのコレクションを構成する主な部品で、以下を持つ。
-
値 (
value
/val
) を持つ。 -
添字 (
index
/idx
) またはキー (key
) とセットであることが多い。
-
値 (
- ひとつの要素は、配列などのコレクションを構成する主な部品で、以下を持つ。
-
attributes
/attrs
... 属性(HTML 属性など)- ひとつの属性は、要素を親に持つキーバリュー。(要素のメタデータ)
- 関係データベースの文脈なら、行の持つ各値の型(列)のこと。
-
entities
... 実体- データのかたまりで、ひとつの実体 (
entity
) はデータベース上に永続化される単位(行)を指すことが多い。 - この文脈では列は属性 (
attribute
) と呼ばれることが多い。
- データのかたまりで、ひとつの実体 (
-
properties
/props
... プロパティ(PDF プロパティなど)- ファイルなどのオブジェクトの持つメタデータのこと。
-
metadata
... メタデータ(PDF メタデータなど)- あるデータに付随するデータ。
- 広い意味で使えてしまうので、他に適切な単語があればそちらを採用した方が良い。
-
parameters
/params
... パラメーター(クエリー文字列など)- 外部から渡される値、または、外部へ返す値。
-
options
/opts
... オプション- 必須ではない設定を示す。
- オプションが使われない場合でも、元からある設定が適用されて正しく動作するイメージ。
-
configuration
/conf
... 構成- 元からある設定は最低限なので、自分で構成を調整する必要があることを示す。
- 単数形で複数の設定の集合を示す。
-
settings
... 設定- 調節できる値の集合を示す。
- 「設定」を意味する単語では、最も広く使えるイメージ。
-
preferences
... 環境設定- お好みで変更できる設定。
- 「内部設定」を
Settings
と呼ぶ場合、Preferences
は「ユーザー設定」として対になることが多いイメージ。
なお、単語の省略は、そのプログラミング言語やフレームワークが慣例的に省略形を使う場合以外は、使用を避けた方が無難です。
また、自分で新しい省略形を考え出してしまうのは、コードの可読性を著しく損なうため、絶対にしてはいけません。
🔖 ブール値を示す
ブール値を返す関数名や変数名には、is
/ has
/ can
/ should
がよく使われます。
具体的なケースを見てみましょう。
- 👍 Good
-
is + 形容詞
- e.g.
isEmpty
... 空っぽか?
- e.g.
-
is + 過去分詞(受動態)
- e.g.
isEnabled
... 有効にされたか?(有効か?)
- e.g.
-
is + 現在分詞(現在進行形)
- e.g.
isSleeping
... 寝ているか?
- e.g.
-
has + 過去分詞(現在完了形)
- e.g.
hasChanged
... 変更されたか? - 受動態との違いは、完了「ちょうど~したところだ」、結果「~してしまった」、継続「ずっと~している」、経験「~したことがある」のいずれかを指すことです。
- 完了形は日本語には馴染みがないので、受動態で代用しがちです。無理に使う必要はありませんが、覚えておくのは悪くありません。
- e.g.
-
三単現動詞(+ 名詞)
- e.g.
useSsl
... SSL を使うか? - e.g.
exists
... 存在するか?(名詞がないパターン)
- e.g.
-
助動詞 + 動詞の原型
- e.g.
canRead
... 読み取り可能か? - e.g.
shouldDryRun
... ドライランすべきか?
- e.g.
-
is + 形容詞
- 👎 Bad
- 意味もなく上記の法則から外れている
- e.g.
downloadFlag
...canDownload
(ダウンロードできるか?) のようにすべき
- e.g.
-
true
/false
を返す条件が不明瞭- e.g.
checkValid
...isValid
(有効か?) のようにすべき
- e.g.
- 文法ミス
- e.g.
isEnable
... 受動態は「過去分詞形」が必要なのでisEnabled
(有効にされたか?) にすべき - e.g.
existError
... 三単現のs
が必要なのでexistsError
(エラーが存在するか?) にすべき - e.g.
isExist
... 動詞と動詞が重なるので、exists
にすべき
- e.g.
- 否定形より肯定形
- e.g.
disableSsl
... 否定形より、useSsl
のように肯定形の方が分かりやすい
- e.g.
- 意味もなく上記の法則から外れている
これらの「主語」は、以下のようにその名前が定義された文脈(オブジェクトやアプリケーションそのもの)を指します。
if (item.exists()) {
console.log('この項目は存在します。');
}
if (item.hasEmptyName()) {
console.log('この項目の名前は空です。');
}
名前にどうしても「主語」を含めたい場合は、英文法に従って userExists
のように「主語 + 動詞」の形にします。
ただし、基本的にはオブジェクトをうまく使って user.exists()
のように主語が不要となる名前にすべきです。
これは、オブジェクトをうまく使うには定義すべきクラス(オブジェクト)を過不足なく用意する必要があるからです。
特に新しく用意することを面倒がっていると、コード上で設計に応じた適切な表現ができなくなるので要注意です。
また、上記の item.hasEmptyName()
(項目の名前が空か?)は、主語(この場合は item
)と主題(この場合は item.name
)が異なる場合に便利な表現です。
item.isEmpty()
や item.getName().isEmpty()
が適切でない場合に、採用を検討してみましょう。
🔖 その他のバッドノウハウ
-
❌ NG: 「単数形」と「複数形」を間違う。
- 例えば
Content
クラスとContents
クラスは、ニュアンスが異なります。
前者は「単一のコンテンツオブジェクト」を扱い、後者は「コンテンツオブジェクトの集合」を扱うイメージになります。 - 配列のようなコレクションを格納する変数には、
users
のような複数形を使うか、あるいは単数形のままuserList
などとすべきです。 -
index
とindexes
のように、複数形で特殊な変化をする単語のスペルミスにも注意が必要です。 -
music
などの「不可算名詞」にも注意が必要です。なお、songs
なら「可算名詞」です。
- 例えば
-
❌ NG: ハンガリアン記法を意味もなく使う。
-
iCount
やcountInt
のように「データ型を示す接頭辞や接尾辞」は、基本的に変数名に含めるべきではありません。
データ型の変更時に変数名の変更し忘れによって、バグを生み出しやすいためです。 - ただし、コーディング規則で定められている用法があれば、それに従ってください。
-
🔖 用語集をつくる
自分たちのプロダクトに合わせて、独自の名前を持つオブジェクトを定義した方が都合の良い時があります。
例えば、既存の概念の包括するオブジェクトが必要になることはよくあります。
ファイルやフォルダーに対する「ドライブ」のような、何かの上位となる存在です。
何かを配信するサービスなら、配信するファイルを包含する管理のための実体(データのまとまり)を「コンテンツ」と呼んだりするでしょう。
このようなプロダクト特有の思想によって生まれる用語は「オブジェクト名」にとどまりません。
「機能名」や「状態名」なども、独自の名前を定義することになるでしょう。
こういった独自の名前は、コントロールしないとあっという間に増えて、開発者や利用者を混乱させます。
開発者の中でこれらの独自の用語が誤解を生む前に、用語集をつくるのは良い方法です。
この開発者向けの用語集は、ブラッシュアップしてお客様向けの操作マニュアルなどにも掲載されることになるのが理想的です。
🔖 命名のためのツール
命名は各種ツールの助けを借りる必要が多々あります。
代表的なツールを挙げてみます。
-
Google 翻訳
- 類語や英和翻訳にも使えて最も便利。
-
英和辞典・和英辞典 - Weblio 辞書
- 辞書的な意味や発音を調べるのに非常に便利な Web サイト。
-
Codic
- 初学者が最初に使うツールとしては優秀。やや強引な変換をする時もあるので注意。
-
Code Spell Checker (VS Code の拡張機能)
- IDE にはスペルチェッカーがある良い。
🔖 おまけ: 間違いやすい単語の読み方
母国語が英語でない者にとっては、英単語の読み方を間違うことはよくあります。
正しくないことは、正当な知識を持つ人からの信頼を損なうばかりなので、気づいた時に直しましょう。
とはいえ、一般的に Apple (ˈæpl) は「アップル」と発音して、「アポゥ」とか「エポゥ」とは発音しないように、程々に折り合いをつけることも忘れてはいけません。
単語 | ❌ 変な読み方 | 🔵 それっぽい読み方 | よく使われる意味 |
---|---|---|---|
height | ヘイト | ハイト (hάɪt) | 高さ |
width | ワイズ | ウィス (wídθ) | 幅 |
allow | アロウ | アラウ (əlάʊ) | 許可する |
deny | デニー | ディナイ (dɪnάɪ) | 拒否する |
accept | アセプト | アクセプト (æksépt) | 受け入れる |
none | ノン | ナン (nˈʌn) | ない |
async | アシンク | エイシンク | 非同期の。asynchronous (eɪsíŋkrənəs) の略 |
await | エイウェイト | アウェイト (əwéɪt) | 待ち受ける |
hide | ヒデ | ハイド (hάɪd) | 隠す |
hidden | ヒデン | ヒドゥン (hídn) | hide の過去分詞 |
align | アリグン | アライン (əlάɪn) | 一列に整列させる |
radius | ラジウス | レイディアス (réɪdiəs) | 半径 |
aside | エイサイド | アサイド (əsάɪd) | かたわらに |
tidy | ティディ | タイディ (tάɪdi) | 整然とした |
justify | ジャスティフィー | ジャスティファイ (dʒˈʌstəfὰɪ) | 正しいとする |
char | チャー | キャラ | 文字型 の character (kˈærəktɚ) の略 |
str | ストラ | ストリング | 文字列型の string (stríŋ) の略 |
alt | アルト | オルト | 代替の。alternative (ɔːltˈɚːnəṭɪv) の略 |
href | ハーフ | エイチレフ | HTML のアンカータグ (<a> ) に使われる属性。Hypertext Reference の略 |
Prettier | プレティア | プリティア (ˈprɪtiɝ) | Web 開発用のコードフォーマッター |
XOR | - | エックスオア | 排他的論理和 (exclusive or) |
CORS | - | コーズ | オリジン間リソース共有 (Cross-Origin Resource Sharing) |
LaTeX | ラテックス | ラテフ | 代表的な組版システム。TeX を「テフ」と読む流れから |
yum | ユム | ヤム | Fedora 系の OS で使われるパッケージ管理ソフト |
halt | ハルト | ホールト (hˈɔːlt) | 停止。システムを停止するための コマンド |
✔ コードに文脈を与える
無から有をうみだすインスピレーションなど、
そうつごうよく簡単にわいてくるわけがない。
メモの山をひっかきまわし、腕組みして歩きまわり、溜息をつき、
無為に過ぎてゆく時間を気にし、焼き直しの誘惑と戦い、
思いつきをいくつかメモし、そのいずれにも不満を感じ、
コーヒーを飲み、自己の才能がつきたらしいと絶望し、
目薬をさし、石けんで手を洗い、またメモを読みかえす。
けっして気力をゆるめてはらなない― 『きまぐれ星のメモ』、星 新一
整理整頓された状態なら、探し物も、異常も、探したいものはすぐに見つかります。
整理整頓された中でものを見つけるのは、自然な文脈に沿うだけで事足ります。
本を探す時は公民館より図書館に行き、日本の小説を探したいなら日本文学の棚を探し、著者の名前が「あ」から始まるなら「あ」の棚の場所を探すのが自然です。
コードは本のように限定的なものを扱わないにせよ、コードを読む人に分かりやすい文脈で整理整頓すべきです。
ところが、誰が見ても自然な文脈を与える作業というのは、いつでも頭を悩ませます。
これはベテランかどうかや才能に関係なく、慣れることのない苦しみを伴う作業ですが、その効果は絶大です。
上記の引用は、プログラミングとは畑違いですが、小説家で「ショートショートの神様」と称された星 新一の言葉です。
これほどの業績を残した天才でも苦悩するのですから、我々のような凡人が苦しまずに済むわけがないのです。
🔖 コードを分類して構造化する
ユーティリティを自作する場合を例に、コードに文脈を与える様子を見てみましょう。
プログラミング言語が提供する標準 API では、かゆい所に手が届かないことが多々あります。
そういう時にはサードパーティ製のライブラリに頼るか、単純なものなら自分で用意するのが、プログラマーの日常的な光景です。
特にプロジェクト固有ではない汎用コードは「ユーティリティ」などと呼ばれ、プロジェクトの中心となるコードから分離しておくと使い勝手が良いことが知られています。
汎用的なコードは後からどんどん増えていくので、あらかじめ増やしやすいように入れ物を分けておくのです。
分離する時にやりがちなアンチパターンは、CommonUtil
のような汎用的なクラス(またはファイル)を作ってしまうことです。
ここにコードを何でもかんでも追加していくと、あっという間にゴミ屋敷が誕生し、ちょっとした捜し物にも時間がかかるようになってしまいます。
これが「文脈」のない状態です。
この教訓は、ユーティリティに限ったのものではありません。
いかなるコードでも特性に応じて分割する癖をつけておくことで、利用すべき「文脈」を示すことができます。
つまり、文脈を与えることはコードを疎結合化につながり、拡張性の確保や、コードの見通しが良くなることによるコード品質を維持など、多くのメリットがあるのです。
ただコードを分離するだけでは、CommonUtil
の例のとおり、文脈のない入れ物を増やしてしまうだけです。
構造をレイヤリング(層化)し、特性に応じて境界線を定義することが重要です。
そのためには、クラスやファイルなどのコードの入れ物に「説明的な名前」を与えるましょう。
全体構造から見てどこに分類されたかを示すのは、名前の大きな役割です。
「ネーミングは設計の中心」といわれるのは、このように文脈を作ることが、適切な命名をする作業そのものだからです。
それでは具体例を見てみましょう。
// 👎 Bad: コードの見通しが悪いファイル(汎用コードが雑多に一箇所に詰め込まれており、一貫性がない)
function getUrlOrigin() {
if (location.origin == null) {
const port = location.port ? `:${location.port}` : '';
return `${location.protocol}//${location.hostname}${port}`;
}
return location.origin;
}
function searchUrlParam(key, opt_url) {
const url = opt_url || location.href;
const escapedKey = key.replace(/[\[\]]/g, '\\$&'); // ブラケットをエスケープ
const regex = new RegExp('[?&]' + escapedKey + '(=([^&#]*)|[&#$])'); // キーに一致する値を検索するための正規表現
const results = regex.exec(url);
if (!results) return null; // キーが見つからない
if (!results[2]) return ''; // 値が空
return decodeURIComponent(results[2].replace(/\+/g, ' ')); // URL デコード
}
function escapeHtml(string) {
return string.replace(/[&'`"<>]/g, function (match) {
return {
'&': '&',
"'": ''',
'`': '`',
'"': '"',
'<': '<',
'>': '>',
}[match];
});
}
// 👍 Good: コードの見通しが良い(役割ごとにファイルを分割されることで、整理された状態)
/** IE11 でも使える URL API の代替ユーティリティクラス */
class UrlUtil {
/**
* 現在のページのオリジン(スキーム、ホスト、ポートの組み合わせ)を取得する。
*
* @returns {string} オリジン e.g. http://localhost:8080
*/
static getOrigin() {
if (location.origin == null) {
const port = location.port ? `:${location.port}` : '';
return `${location.protocol}//${location.hostname}${port}`;
}
return location.origin;
}
/**
* クエリー文字列を検索し、指定されたキーに対応する値を取得する。
*
* @param {string} key キー
* @param {string} opt_url 検索する URL。省略時は現在のページの URL
* @return {string} キーに対応する値。見つからない場合は null
*/
static searchParam(key, opt_url) {
const url = opt_url || location.href;
const escapedKey = key.replace(/[\[\]]/g, '\\$&'); // ブラケットをエスケープ
const regex = new RegExp('[?&]' + escapedKey + '(=([^&#]*)|[&#$])'); // キーに一致する値を検索するための正規表現
const results = regex.exec(url);
if (!results) return null; // キーが見つからない
if (!results[2]) return ''; // 値が空
return decodeURIComponent(results[2].replace(/\+/g, ' ')); // URL デコード
}
}
// 👍 Good: コードの見通しが良い(役割ごとにファイルを分割されることで、整理された状態)
/** フォームのためのユーティリティクラス */
class FormUtil {
/**
* XSS 対策として、最低限の対象文字を HTML 特殊文字にエスケープする。
*
* @param {string} string
* @return {string} エスケープされた文字
*/
static escapeHtml(string) {
return string.replace(/[&'`"<>]/g, (match) => {
return {
'&': '&',
"'": ''',
'`': '`',
'"': '"',
'<': '<',
'>': '>',
}[match];
});
}
}
雑多な汎用コードがつまったファイルを、UrlUtil
と FormUtil
という特性に応じたクラス名を用意することで、分かりやすく分類することができました。
ちなみに、文脈を示すための「名前」は誤解や乱用を防ぐため、広い意味を与えるべきではありません。
StringUtil
のような広い意味の名前が良くないのは、今回の例では文字列操作の中でも URL に特化しているので UrlUtil
にすれば誤用や乱用を生みにくいためです。
FormUtil
も同様に、HtmlUtil
のような広い意味を持たせてしまえば、いらぬ誤用や乱用を誘発してしまいます。
補足:
文脈の話から脱線しますが、上記 Good の例の「IE11 (Internet Explorer 11) でも使える」、というコメントは正確ではありません。
このコードは説明のために ES6 (ECMAScript 2015) の構文を使っているため、古いブラウザーである IE11 で利用するには ES5 の形式に変換(トランスパイル)する必要があります。
🔖 アーキテクチャーに則る
大抵のシステムは、何らかのアーキテクチャーを下敷きに設計します。
アーキテクチャーとは、どういう風にフォルダーを切るとか、プログラムの責務をどういうレイヤー構造に分けるのかといった基本的な設計を決めるルールのことです。
アーキテクチャーは採用するフレームワークやライブラリ、プログラミング言語の制約によって決まります。
明示されていないルールは場合は一般的なベストプラクティスに従うのが賢明です。
なお、アーキテクチャーの自作はかなりハードルが高いのでオススメしません。
アーキテクチャーを理解することが、良い文脈を与えるために極めて重要となります。
しかし、大抵のアーキテクチャーの理解には時間がかかります。
ケースバイケースのことが多く、そもそも巨大で複雑だからです。
そのため、理解するには以下のような工夫ができないか検討しましょう。
- 公式のチュートリアルをやりましょう。手を動かすのが、習熟への近道です。
- 「概念を理解すること」と「実際に使えること」は別物です。使えるようになるには、何度も使う以外に方法はありません。
- 最初のうちは、チュートリアルで習ったことを真似るところからスタートしましょう。徐々に自分なりに応用ができるようになります。
- インターネット上にベストプラクティスやその解説がないか探しましょう。
- 情報が古くなっていなければ、最初はベストプラクティスに従いましょう。
- 同じアーキテクチャーを使う有名な GitHub 上のコードを読みましょう。
- GitHub 上のスターの多いリポジトリーのコードは大変勉強になります。
- 実用例を見ておくと、自分で応用できるようになるまでの時間を短縮できます。
応用例はチュートリアルやベストプラクティスでは説明されないので、このような工夫が必要なります。
-
勉強会を開きましょう。
- 他人に教えるのが習熟の近道になります。
- やる気のある行動は、他のメンバーのモチベーションに良い影響を与えます。
そのため、実際の勉強会の内容だけでなく、開催するだけでメリットを得ることができます。
コードを書き始めるまでの学習ハードルが高いにも関わらず、フレームワークやライブラリが採用されるのは、慣れてさえしまえばその方がずっと楽だからです。
最初は苦しいですが、これを乗り越えないとそもそも始まりません。
学習を怠って自己流で始めてしまうと、後は悪夢のようなコードを産み出すことになります。
✔ コードフォーマットする
読みやすいコードの条件として、統一された形式で整形されていることが挙げられます。
👎 Bad:
- インデントが統一されていない
- 括弧の前後のスペースの有無が統一されていない
- e.g.
if(!greeting)return null;
if (!greeting) return null;
- e.g.
- 命名規則に反している
整形ルールは、言語で指定された方法や、プロジェクトのコーディング規則に従いましょう。
できれば EditorConfig や、プロジェクトで共通のフォーマッター (Prettier など)、IDE でソース保存時に自動フォーマットする機能を使うことが理想的です。
✔ コメントを残す
コメントを残すことは、基本的にコードの可読性を向上させます。
自分で書いたコードでも、半年も経てば細かいことは忘れてしまいます。
半年後に保守する時に困らないように、必ずコメントを残しましょう。
🔖 アノテーションを利用する
言語によってはアノテーションを使ったコメントを利用することで、IDE が自動解析して補完や解説を表示してくれたり、API 仕様書の自動出力できたりして、非常に便利です。
例えば、JavaScript なら JSDoc を使うことになるでしょう。
/**
* 「行配列」を「行オブジェクト」に変換する
*
* @param {Array.<*>} row 行配列。インデックスを列番号として利用するため、並び替え済みであること
* @returns {Object.<number, *>} 列番号をキーにした行オブジェクト
*/
function convertArrayToObject(row) {
return row.reduce((result, cell, columnIndex) => {
result[columnIndex] = cell;
return result;
}, {});
}
convertArrayToObject(['first', 'second', 'third']); // {0: "first", 1: "second", 2: "third"}
🔖 マジックナンバーを説明する
マジックナンバーのような定数を使う場合、必ずコメントを残すべきです。
以下の CSS の例は、もしコメントがなければ確認のために無用な時間がかかるでしょう。
html {
font-size: 62.5%; /* 1rem をリセットして 1rem = 10px に */
}
body {
font-size: 1.6rem; /* = 16px */
}
補足:
コメントの説明からは脱線しますが、上記の CSS の詳しい説明は以下のとおりです。(興味がない場合は読み飛ばして構いません。)
このマジックナンバーは rem
という HTML のルート要素 (html
) を基準とするフォントサイズを使うためのものです。
多くのブラウザではデフォルトのフォントサイズが 16px
なので 1rem = 16px
なのですが、例えば 14px
を rem
に換算したいなら 0.875rem
という一見して何か分からない値を記述することなります。
これは都合が悪いので、10 進数で使いやすい数値にリセットするために 62.5%
と指定して 1rem = 16px * 62.5% = 10px
に変更しているのが、上記の例です。
こうすることで、14px
を指定したいなら 1.4rem
と 10 進数ベースで分かりやすい記述ができます。
このようなややこしい話でも、コメントに 62.5%
が 10px
で 1rem
だという情報を残しておくだけで、この値が何を示していたのかが一目瞭然となるのです。
🔖 作業途中のコメントを残す
作業途中にコメントを残すのは良い習慣です。
よく使われる記法は以下のとおりです。
これは気になったことを後で思い出すためのヒントになります。
IDE によっては、これらのコメントを強調表示してくれるでしょう。
// TODO: あとでやる
// FIXME: 既知の不具合がある。修正が必要
// HACK: 難解で下手くそな方法を使っている。注意すべきで、リファクタリングが必要
なお、どうしても避けられない場合を除き、コミットする前にこれらの問題は解決しておくこと。
🔖 不要なコメントを残さない
コードから分かることは、コメントに残す必要はありません。
以下のコメントは新しい情報を提供していないので、コードのノイズになってしまっています。
// 👎 Bad: コメントが新しい情報を提供していない
// 管理者のメールアドレスを取得
const to = getMailAddressOfAdmin();
// 件名を取得
const subject = getSubject();
// 本文を取得
const body = getBody();
// メールを送信する
const result = Mail.sendEmail(to, subject, body);
// 送信結果をログ出力する
Logger.log(result);
また、単純に名前が間違っている場合は、コメントに記載するよりも名前を修正した方が有益です。
// 👎 Bad: 関数名が間違っていることをコメントに残す
/** ファイルを削除する。実際には削除せず、退避するだけ */
function removeFile(path) {
// 略
}
// 👍 Good: 関数名を修正する
/** ファイルを削除領域へ退避する */
function archiveFileInDeleteArea(path) {
// 略
}
他にも、変更履歴をコメントで残すのも悪手です。
代わりにバージョン管理システム(Git など)を導入し、コミットメッセージを活用しましょう。
// 👎 Bad: 変更履歴をコメントに残す
const foo;
// ADD start: 2000-01-01 taro
const bar;
// ADD end: 2000-01-01 taro
const baz;
✔ ガード節
よく使う記法として「ガード節」があります。
// 👍 Good: ガード節を使って早期リターンさせる
function doSomething(text) {
if (text == null) return '';
if (text === '') return '';
// 処理が続く...
}
このように、処理すべきでないコードは最初に返却することで、その後の処理をシンプルに書くことができます。
コードを読む際に同時に考えることを減らす工夫は、読む側に非常に親切です。
また以下のように、ガード節を使わないとネストが深くなって、コードの可読性が低下してしまいます。
なお、else
が基本的に不要となるのも、ガード節の特徴です。
// 👎 Bad: if 文の入れ子を使って返却
/** うるう年か? */
function isLeapYear(year) {
if (year == null) {
throw new Error('Illegal argument.');
} else {
if (year % 400 === 0) {
return true;
} else {
if (year % 100 === 0) {
return false;
} else {
if (year % 4 === 0) {
return true;
} else {
return false;
}
}
}
}
}
// 👍 Good: ガード節を使って返却
/** うるう年か? */
function isLeapYear(year) {
if (year == null) throw new Error('Illegal argument.');
if (year % 400 === 0) return true;
if (year % 100 === 0) return false;
if (year % 4 === 0) return true;
return false;
}
ガード節に限らず、早めにリターンしたり、ネストを浅くしたりすることは、読みやすいコードの基本原則です。
この原則に反するコードを書いていないか、常に自問自答しましょう。
✔ 説明変数
式に変数名をつけることで、コードが読みやすくなります。
これは単純ながらコードをの可読性を上げるには効果の高い方法です。
なお、式が十分に分かりやすい場合、「説明変数」は不要な点には注意してください。
// 👎 Bad: 何の分岐か分かりづらい
if (file.canEdit() && file.canRead() && !file.isEncrypted()) {
// 略
}
// 👍 Good: if 文が説明変数によって読みやすく
const canEditFile = file.canEdit() && file.canRead() && !file.isEncrypted();
if (canEditFile) {
// 略
}
✔ 意味のないマジックナンバーを使うな
意味を持つ結果を、わざわざ意味のないマジックナンバーに変換する必要はありません。
(Bash など、数字以外が返却できない言語は除く。)
何かを識別するための値として、「文字列」を返却することは悪いことではありません。
この場合、識別子となる文字列を生成/判定する共通のラッパー関数/メソッドさえ作っておけば、何も恐れることはないのです。
また、Enum
型などを使って宣言的に記述することが望ましいケースもあります。
ただし、その場合でもマジックナンバーをわざわざ与える必要はありません。
// 👎 Bad
/**
* 画像の種類を識別する
*/
function identifyImageType(path) {
const matcher = path.match(/^.+\.(.+)$/); // 拡張子をキャプチャー
const extension = matcher == null ? '' : matcher[1].toLowerCase();
switch (extension) {
case 'png':
return 1; // マジックナンバーを返す
case 'jpg':
case 'jpeg':
case 'jfif':
case 'pjpeg':
case 'pjp':
return 2; // マジックナンバーを返す
default:
return 0; // マジックナンバーを返す
}
}
// 👍 Good
/**
* 画像の種類を識別する
*/
function identifyImageType(path) {
const matcher = path.match(/^.+\.(.+)$/); // 拡張子をキャプチャー
const extension = matcher == null ? '' : matcher[1].toLowerCase();
switch (extension) {
case 'png':
return 'png'; // 文字列を返す
case 'jpg':
case 'jpeg':
case 'jfif':
case 'pjpeg':
case 'pjp':
return 'jpeg'; // 文字列を返す
default:
return null; // null を返す
}
}
✔ データと処理を分離する
先程挙げた「画像の種類を識別する関数」は、Good の例でも問題が残っています。
もし扱いたい画像の種類を増やす場合、関数の処理自体を書き換える必要があるのです。
これではちょっとした修正でもバグが入り込む可能性があるので、柔軟性のある良いコードとは呼べません。
そこで、「アルゴリズムを提供する処理」と「処理の振る舞いを決定するデータ」を分離してみましょう。
// 👍 Good: データと処理が分離された
/**
* 画像タイプ
* @type {{name: string, extensionVariants: string[]}}
*/
const IMAGE_TYPES = [
{
name: 'png',
extensionVariants: ['png']
},
{
name: 'jpeg',
extensionVariants: ['jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp']
},
{
name: 'gif',
extensionVariants: ['gif'] // 簡単に増やすことができる
}
];
/**
* ファイルパスに含まれる拡張子を識別し、画像タイプを返す。
* 識別には IMAGE_TYPES を使う。
*
* @param {string} path ファイルパス
* @returns {string|null} 画像タイプ (IMAGE_TYPES.name)。識別できなかった場合は null
*/
function identifyImageType(path) {
const matcher = path.match(/^.+\.(.+)$/); // 拡張子をキャプチャー
const extension = matcher == null ? '' : matcher[1].toLowerCase();
for (const imageType of IMAGE_TYPES) {
const isHit = imageType.extensionVariants.some(variant => variant === extension);
if (isHit) return imageType.name;
}
return null;
}
データと処理を分離して、gif
にも対応させました。
扱う画像の種類を増やすことが、とても簡単になったことが分かります。
このように、「アルゴリズムの複雑さ」を「データ構造の複雑さ」に代替させる方法は、非常に有効な方法なので覚えておきましょう。
✔ 参照透過性を高める
引数が同じなら、関数の返り値はいつでも同じであるべきです。
このような状態を「参照透過性」が成り立つ、と表現します。
これによって入力と出力の関係が明白になるため、単体テストが容易になります。
また、変数スコープを小さくする働きもあります。
参照透過性を高めようとすれば、処理は自然に分離し、バグが入り込む隙きを減らすことができます。
🔖 参照透過性の有無の例
// 👎 Bad
var counter = 0;
function increment() {
return counter++; // 関数外の変数 counter に依存する(参照透過性がない)
}
console.log(increment()); // 2
console.log(increment()); // 3(毎回同じ答えにならないのでテストが難しくなる)
// 👍 Good
function increment(x) {
return x++; // 関数内で利用する値が引数で明示されている(参照透過性がある)
}
console.log(increment(2)); // 3
console.log(increment(2)); // 3(引数が同じなら、返り値は常に同じなので、テストしやすい)
🔖 メソッドの場合
関数ではなくメソッド(クラス)を利用する場合も、注意点は同じです。
単体テストを阻害しないようにすべきです。
// 👎 Bad: 部分的な挙動についてはテストできない
class DateRangeForm {
constructor(startDateText, endDateText) {
this.startDateText = startDateText;
this.endDateText = endDateText;
}
validate() {
// YYYY-MM-DD でないなら終了
if (!/^\d{4}-\d{2}-\d{2}$/.test(this.startDateText)) return false;
if (!/^\d{4}-\d{2}-\d{2}$/.test(this.endDateText)) return false;
// 不正な日付なら終了
const startDate = new Date(this.startDateText);
const endDate = new Date(this.endDateText);
if (isNaN(startDate)) return false;
if (isNaN(endDate)) return false;
return startDate.getTime() < endDate.getTime();
}
// 略
}
let form = new DateRangeForm('2000-01-01', '2000-01-02');
form.validate(); // true
// 👍 Good: 各処理ごとに参照透過性を持つメソッドに分離している
class DateRangeForm {
constructor(startDateText, endDateText) {
this.startDateText = startDateText;
this.endDateText = endDateText;
}
validate() {
return DateValidator.isValidRange(this.startDateText, this.endDateText);
}
// 略
}
class DateValidator {
static castDate(text) {
// YYYY-MM-DD でないなら終了
if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) return null;
// 不正な日付なら終了
const date = text == null ? NaN : new Date(text);
if (isNaN(date)) return null;
return date;
}
static isValidRange(startDateText, endDateText) {
const startDate = DateValidator.castDate(startDateText);
const endDate = DateValidator.castDate(endDateText);
// 不正な日付なら終了
if (startDate == null || endDate == null) return false;
return startDate.getTime() < endDate.getTime();
}
}
// 単体テストを簡単に実行できる
console.log(DateValidator.castDate('2000-01-01')); // Sat Jan 01 2000 09:00:00 GMT+0900 (日本標準時)
console.log(DateValidator.castDate('2000-13-01')); // null
console.log(DateValidator.castDate('2000/01/01')); // null
console.log(DateValidator.castDate('2000-1-1')); // null
console.log(DateValidator.castDate('2000-01')); // null
console.log(DateValidator.castDate('')); // null
console.log(DateValidator.castDate(null)); // null
console.log(DateValidator.castDate()); // null
console.log(DateValidator.isValidRange('2000-01-01', '2000-01-02')); // true
console.log(DateValidator.isValidRange('2000-01-01', '2000-01-01')); // false
console.log(DateValidator.isValidRange('2000-01-01', '1999-12-31')); // false
console.log(DateValidator.isValidRange(null, '2000-01-02')); // false
console.log(DateValidator.isValidRange('2000-01-01', null)); // false
console.log(DateValidator.isValidRange(null, null)); // false
let form = new DateRangeForm('2000-01-01', '2000-01-02');
form.validate(); // true
どちらのケースも同じ処理ですが、Bad より Good の方が単体テストを確実に実行できます。
確実な単体テストがあれば、正しく動作するか不安にならずに済みますから、自信をもってコードを修正できるようになります。
また、Good の方が各処理の役割が明確なためにコードの見通しが良くなり、処理を再利用しやすいことにも注目してください。
参照透過性を高めようとすることが、コードの品質を高めることに直結するのです。
なおここでは詳しく触れませんが、テストコードを管理するためにも、テスティングフレームワークは必ず導入すべきです。
JavaScript なら Jest などがあります。
✔ 副作用をコントロールする
関数が引数を受け取って値を返すことを「主作用」と呼びます。
反対に、それ以外のことを「副作用」と呼びます。
副作用を正しくコントロールすることで、参照透過性を高めることができます。
例を見てみましょう。
🔖 副作用を引き起こす例
// 👎 Bad
function double() {
x = x * 2; // x への代入という副作用(引数を受け取って、返り値を返す以外のこと)が発生
return x;
}
x = 10;
console.log(double()); // 20
console.log(double()); // 40(関数が呼ばれる度に、返り値が変わる)
// 👍 Good
function double(x) {
return x * 2; // この関数には副作用がない(参照透過性がある状態に)
}
console.log(double(10)); // 20
console.log(double(10)); // 20(引数が同じなら、返り値が変わらない)
🔖 副作用を避けられない場合
ファイル、DB、ビュー(UI)への入出力など、副作用を回避できない場合は副作用のあるコードを分離すべきです。
そうすることで、漏れのない確実なテストが可能になります。
// 👎 Bad
function sum(x, y) {
const solution = x + y;
document.querySelector('body').prepend(solution); // 画面を描画するという「副作用」が発生
}
sum(1, 2);
// 👍 Good
function sum(x, y) {
return x + y; // この関数には副作用がない(確実なテストが可能に)
}
function renderSolution(solution) {
document.querySelector('body').prepend(solution); // 副作用の発生するコードを分離
}
const solution = sum(1, 2);
renderSolution(solution);
🔖 破壊的メソッドに注意する
以下の Bad の例では「副作用」によって、関数の実行前後で originalList
の内容が変質してしまっています。
恐ろしいことに「破壊的メソッド」について知らなければ、この「副作用」はコード上から見つけることができません。
// 👎 Bad: 変更前の配列が変質してしまっている
function addAdmin(inputUsers) {
let ret = inputUsers;
ret.push('admin'); // 配列に "admin" を追加(破壊的メソッド)
return ret.sort(); // 配列をソート(破壊的メソッド)
}
const originalList = ['user1', 'user2'];
const modifiedList = addAdmin(originalList);
// 確認: 🔵OK: admin が追加されている
console.log(modifiedList); // ["admin", "user1", "user2"]
// 確認: ❌NG: 元データが破壊されてしまった!
console.log(originalList); // ["user1", "user2"] ではなく modifiedList と同じに
// もしここで、originalList と modifiedList を比較しようとしたら...
// 👍 Good: originalList の値は不変
function addAdmin(inputUsers) {
const adminUsers = ['admin']; // 配列に追加したい要素を用意
const addedAdmin = inputUsers.concat(adminUsers); // push() の代わりに、元の配列を破壊しない concat() で配列を結合
const sortedList = Array.from(addedAdmin).sort(); // Array.from() メソッドで、元の配列をコピーしてから sort() を実行
return sortedList;
}
const originalList = ['user1', 'user2'];
const modifiedList = addAdmin(originalList);
// 確認: 🔵OK: admin が追加されている
console.log(modifiedList); // ["admin", "user1", "user2"]
// 確認: 🔵OK: 元データは無事!
console.log(originalList); // ["user1", "user2"]
originalList
の配列は、Bad の例では関数の実行前後で変質していますが、Good の例では無事です。
原因は、「破壊的メソッド」と「非破壊メソッド」の違いにあります。
実は push()
や sort()
は配列を直接書き換えてしまうので、元の配列は残りません。このようなメソッドを「破壊的メソッド」と呼びます。
Good の例では push()
の代わりに concat()
(配列の結合)という「非破壊メソッド」を使うことで、元データへの影響を防いでいるのです。
「破壊的メソッド」はメモリ消費を抑えることができますが、気をつけないとこのような問題に陥るので、十分に注意が必要です。
cf. [Javascript] Array メソッドチート表 [非破壊・破壊・return・MDN リンク] - Qiita
なお、プログラミング言語によっては、他のも「値渡し」と「参照渡し」など、副作用をコントロールするには多くの違いの理解する必要があります。
破壊的な変更が発生する挙動は、特に学習を怠ってはいけない分野といえます。
✔ 再代入を避ける
参照透過性を高めるために、可変な値 (mutable value
) と不変な値 (immutable value
) を区別することは重要です。
このことに気をつけるだけで、コード品質が向上します。
以下の Bad の例のとおり、「可変な値」を使って変数の内容が次々に変動するコードは、コードの見通しを悪くするので、バグが入り込む「すき」が生まれやすくなります。
これを防ぐには、まずは再代入をやめましょう。
(プログラミング言語によっては例外はあるものの)再代入されないなら「不変な値」なので、変数の値がころころ変化する可能性を考えなくて済みます。
不変にできない一部の値のみ「可変な値」として注意して扱えば良いので、考えることが減るのは大きなことです。
更に副次効果として、再代入しないということは毎回変数を宣言するということになるので、値が変わる度に新しい変数名(説明変数)が与えられ、コードがずっと読みやすくなります。
なお、プログラミング言語によっては宣言時に再代入を禁止できます。
JavaScript であれば let
で宣言した変数は再代入できますが、const
なら再代入を禁止できます。
これもお手軽な方法なので、積極的に使いましょう。
// 👎 Bad: 変数の値が変動して、コードの見通しが悪い
function addAdmin(inputUsers) {
let ret;
ret = inputUsers;
ret.push('admin');
ret = ret.sort();
return ret;
}
// 👍 Good: コードの見通しが良い
function addAdmin(inputUsers) {
const adminUsers = ['admin'];
const addedAdmin = inputUsers.concat(adminUsers);
const sortedList = Array.from(addedAdmin).sort();
return sortedList;
}
補足:
JavaScript の const
宣言は、再代入を禁止するだけなので、再代入以外の変化は許容されています。
// 再代入は禁止される
const foo = 0; // 代入: 成功
const foo = 0; // 再代入: 失敗: Uncaught SyntaxError: Identifier 'foo' has already been declared
// プロパティの変更は可能
const bar = { key: 0 };
console.log(bar); // {key: 0}
bar.key = 100; // これは bar に対する代入ではないため、プロパティが変更できてしまう
console.log(bar); // {key: 100}
Object.freeze() メソッド でオブジェクトや配列の直下のプロパティ変更を禁止できますが、よほど厳重に管理したい場合を除けば、その必要性は高くないでしょう。
一般的なケースでは「再代入を禁止する」という簡単なことを心がけるだけで、値が不変であることを保つために十分な効果を発揮します。
Object.freeze()
のような機構はコードを複雑にするので、リスクとリターンをよく検討してください。
どんなリスクにも全力で対処していては、コードの可読性が失われ、本末転倒となります。
何に注意を払うべきかを知るためにも、プログラミング言語を理解する努力を怠ってはいけません。
✔ 例外を握りつぶさない
try-catch
文を持つプログラミング言語の場合、以下のようなコードを見かけたら要注意のサインです。
// 👎 Bad: 例外を握りつぶす
try {
return somethingDo();
} catch (e) {
return null; // 例外 (e) を握りつぶしている
}
例外を握りつぶしてしまうと、重大な問題が発生しても気づくことができなくなります。
プログラミング言語にもよりますが、例外をスローする目的は大きく 2 つあります。
- 正常時とは異なる型を返すため
- 大域脱出のため(呼び出し元を一気にさかのぼるため)
例外がスローされた時、呼び出し元で例外を処理できる場合、キャッチして適切な処理を実行できます。
しかし、呼び出し元で例外を処理できない場合はキャッチせずに、例外が呼び出し元を辿っていくことを邪魔しないでおくべきです。
このように例外がキャッチされるまで呼び出しスタックを戻ることをバブリングと呼びます。
例外をキャッチ後にどうしてもバブリングを継続させたい場合、新しい例外でネストする方法もありますが、これは処理が複雑化したり例外の型が変わったりするので、利用には慎重になりましょう。
// 例外をネストする例
try {
return somethingDo();
} catch (e) {
throw new Error(e); // 例外 (e) をラップして、再スロー
}
なお、プログラミング言語によっては catch
で捕捉できる例外型を限定できるので、そもそも不必要な例外をキャッチさせないようにすることでバブリングを妨げない方法もあります。
✔ ログを出力する
どんなプログラムもバグがひそんでいる可能性があります。
だとすれば、それに備えておくのが賢明です。
バグ解析の基本である「ログ」について考えてみましょう。
🔖 ログ解析を前提にする
ログは、後から解析しやすい形式にしましょう。
採用するログの解析ツールが対応していれば、JSON 形式などの「高度な検索フィルター」が使えるデータ形式もオススメです。
また、解析に使うツールで「エスケープが必要な文字列」が、ログに出力されないように注意してください。
一般的には、ログには以下のような情報が必要です。
-
ログヘッダー
-
タイムスタンプ
- 解析に必要な時刻精度と、タイムゾーンの必要性の有無を検討しましょう。
- 日時フォーマットは一般的な ISO8601 (
2020-01-01T23:59:59+09:00
) がオススメです。
-
ログレベル
- 基本的に
INFO
/WARN
/ERROR
の 3 つで十分です。 -
INFO
... ユーザー操作のトリガーなどの正常系の処理を記録します。 -
WARN
... 後から確認したい異常系の処理を記録します。 -
ERROR
... コードが自己解決できなかった問題を記録します。 - 人によって利用する目的がずれないように、ルールを定めましょう。
- 適当に扱うと、解析する時にログレベルが信用できなくなります。
- 基本的に
-
発生場所
- セッション ID(通信上の場所)
- スレッド ID(プロセス上の場所)
- クラス名
- メソッド名
-
タイムスタンプ
-
ログメッセージ
- ログの本文です。
- よく使うログは、フォーマットが決まっていると後で解析が楽になります。
なお、ログに記録する識別子(ユーザー ID、ファイル ID など)は、顧客との約束(プライバシーポリシーなど)が許す限り、出力することをケチってはいけません。
出し惜しみして、ログイン後の操作ログを見て「どのユーザーの操作か?」が分からないのなら、それは解析できない無意味な情報となります。
🔖 正常系のログ
後から統計を取りたいログを除けば、基本的に正常系ログを出力する必要はありません。
ただし、ユーザー操作などの「動作トリガーのログ」は間違いなく必要になるので、最低限これだけは忘れずに出力しましょう。
🔖 異常系のログ
異常時はなるべく穏便に処理されるようにしなければならないとはいえ、発生した問題は問題として後から確認できるようにログを残しましょう。
また、プログラム自身で復旧できない異常は ERROR
などの特別なログレベルを使って、可能なかぎり早急に障害に対応できるようにすべきです。
具体的な例を見てみましょう。
キャッシュを探しに行って何らかの予期せぬ問題で探索に失敗したとしても、そこで異常終了するよりも、キャッシュの元データを探しに行った方が親切です。
しかし、異常が発生したことは後から調査できるように、ログには忘れずに異常を記録しましょう。
そして、ファイルを読もうとしてプログラム自身で復旧できない問題(ハードウェア障害など)が発生した場合は ERROR
ログを記録して、早急に重大な障害発生していることを知らせましょう。
🔖 あちこちでログを出力しない
MVC などの階層的なアーキテクチャーを採用する場合、主要なレイヤー以外でログを吐く必要は基本的にありません。
たとえば、長大な処理をする場合を除き、ユーティリティクラスでログは出力しない方が良いでしょう。
あちこちでログを吐くと収集がつかなくなるので、適切な責務のレイヤーで例外をキャッチするなどの形で、ログ出力を任せる場所を決めましょう。
✔ リファクタリングする
日常的な開発作業を 3 つのフェーズに分類するなら、以下のようになるでしょう。
- 機能拡張 ... 新機能を追加したり、改良したりする。
- バグ修正 ... 既知の問題を修正する。
- リファクタリング ... 技術的負債を返済する。
機能拡張フェーズやバグ修正フェーズを繰り返していると、どうしても技術的負債(先送りにされた修正が残っている状態)が発生します。
各開発フェーズは目的を絞った方が効率が良いので、意識的にコントロールされた修正の先送りは、何の問題もありません。
これを故意の技術的負債と呼びます。
逆に注意が必要なのは不注意から発生する技術的負債の方です。
これは修正が予定されていないので、手遅れになる前に見つけ次第、早く修正すべきです。
「負債」の比喩のとおり、放置する限り「利息」を要求されます。
負債がある限り作業効率は下がり、常に追加コストがかかり続けているのを忘れてはいけません。
更に放置する習慣がつくとあっという間に問題は複雑化し、再最適化コストは指数関数的に増大します。
ある一定の複雑さ・無秩序さに到達すると、現実的な時間内での修正が不可能になります。
この絶望はメンバーのモチベーションを下げ、この症状が進行すれば手遅れになり、あとは問題が起きないように祈るようになります。
あまりに希望が失われると、自分の力ではどうしようもないと諦めてしまうのです。
そうならないためには、早期のリファクタリングが必要です。
リファクタリングの暇がないプロジェクトは、機能拡張もバグ修正もしないリファクタリングフェーズを定期的に設けましょう。
また、変更を怖がる態度がチームに蔓延していると、リファクタリングは実行されません。
ひとりひとりが積極的に改良していく姿勢を見せることが重要です。
リファクタリングは、マイナスをゼロにするだけで、短期的には何もプラスがないように感じます。
しかし実際には将来的な大幅なコスト削減につながり、リファクタリングしたメンバーのスキルアップ(失敗から学べる)につながるため、最終的には誰も損をしないのです。
👑 Google 検索のテクニック
Google などの検索エンジンは、誰もが日常的に使います。
この作業は、いくつかのテクニックを駆使することで効率化できます。
✔ 調べた情報をストックする
- Slack や Google Keep など、調べた情報を一時的にストックする場所を作りましょう。
- このストックを元に調査結果をまとめる習慣を持つと、激的なスキルアップにつながるので非常にオススメです。
振り返りをすることが学習において重要である上に、ドキュメントライティングの練習になること、そして 1 度つまずいたことは大抵また調べたくなることから、この面倒な作業は将来的に損をしません。
✔ アドレスバーにフォーカスするショートカット(Chrome の場合)
- Windows なら
Ctrl
+L
。 - macOS なら
⌘
+L
。
✔ ブックマークに素早くアクセスする
- ブックマーク名の何文字かをアドレスバーに打ち込み、
↑
または↓
キーで該当のページを選択してEnter
キー。
✔ 開いている Web サイト内で検索する(検索エンジンが Google の場合)
- アドレスバーの先頭に
site:
と入力し、URL 後半の不要なパスを削ってから、末尾にスペースを開けて「検索したいキーワード」を入力してEnter
キー。 - e.g.
site:https://www.postgresql.jp/document/ db_dump
... PostgreSQL 10 のリファレンスでpg_dump
について調べる。 - e.g.
site: https://www.postgresql.jp/document/10/html/app-pgdump.html pg_restore
...site:
の前にスペースを入れると(URL の前に適当なキーワードを追加すると)、ファジーな検索ができます。
✔ 検索キーワードを工夫する(検索エンジンが Google の場合)
-
AND 検索:
- キーワードをスペースで区切る。
- e.g.
A B
-
OR 検索:
- キーワードを
OR
で区切る。 - e.g.
A OR B
- キーワードを
-
NOT 検索:
- キーワードの先頭に
-
を付ける。 - e.g.
A -B
- キーワードの先頭に
-
完全一致検索:
- キーワードを
"
で囲む。ワイルドカードを使うには*
を使う。 - e.g.
A "B * D"
- キーワードを
- その他:
✔ タブを「固定」or「グループ化」する(Chrome の場合)
- タブを右クリックして
固定
orタブを新しいグループに追加
に選択。
(今はどちらも拡張機能なしで利用できる) - Chrome の 設定 > 起動時 で
前回開いていたページを開く
を選択しておくことで、ブラウザを終了してもタブのグループを保存しておくことができます。
👑 ドキュメントを書くテクニック
cf. 顧客が本当に必要だったもの
設計書、要件定義書/仕様書、試験書、バグ報告、検証エビデンスなど、ドキュメントを書くべきタイミングは非常に多くあります。
しかし、実際にまともに用意され、保守され続けることは、残念ながら稀です。
多くの問題が、ドキュメントを残すだけで解決するにも関わらずにです。
原因は様々です。
他のことで忙しい、書いてもすぐ無駄になる、書いても評価されない/見てくれない、自分は書く才能がない、などなど。
どれもこれも言い訳に過ぎません。
メリットは十分に大きいので、まずは自分から変えていきましょう。他人任せでは何も変わりません。
慣れないうちは多くの失敗をしますが、学びを得るためには必要な失敗ですから、反省し終わったら開き直りましょう。
書式は文化的または個人的な違いが大きいので一概には言えませんが、私が気をつけていることを挙げてみます。
✔ なぜドキュメントを残すのか
-
チームの認識をひとつにするため
- ドキュメントで明示されない限り、各メンバーは好き勝手に作業し、作る側の意識が細部まで統一されない雑な作りになってしまいます。
- 例えば機能を追加するなら、どのようなシーンで誰が何をするための機能かを明確にしましょう。
- 口頭やチャットなどの、ドキュメントなきコミュニケーションの空中戦は、穴だらけの浅い結論しか生みません。これはロクに事前調査をしないからです。
- 少なくとも一人が深く検討してから打合せするのと、ただ集まって突然打合せすることは、内容の深さに天と地ほどの差があります。
検討事項があるなら、事前にメモ書きくらい作るべきです。
- 少なくとも一人が深く検討してから打合せするのと、ただ集まって突然打合せすることは、内容の深さに天と地ほどの差があります。
- ドキュメントで明示されない限り、各メンバーは好き勝手に作業し、作る側の意識が細部まで統一されない雑な作りになってしまいます。
-
早めに失敗するため
- コードを書きたくてたまらないので、思いつくまま書いて、コードレビューで全部書き直しと言われれば、その批判は受け入れ難いでしょう。
- 代わりに今から何をどう作るかを仕様書にまとめていれば、容易にやり直しが効くので批判を素直に反映させることができます。
-
未来のメンバーにヒントを残すため
- 新しいメンバーが参入して、UI を触ったりコードを読んでも、なぜその機能があるのか分からない事態は避けねばなりません。
- 過去のバグの対策時期や内容、検証結果が残っていれば、新しいメンバーや未来の自分が類似の問題に挑む時の助けになります。
✔ こまめに書く
- 普段から習慣化していないと、いざという時に腰が重くて上がらないでしょう。
✔ 自分で書く
- 自分が書くべきことを、他人に押し付けてはいけません。
- 自分の頭の中にあることや、自分がやったことは、自分で書き出さなければ、正確な資料にはなりません。
- 他人が書くべきところでも、必要なら自分で書きましょう。
- 他人が書かないことを嘆いても、問題は何も解決しません。
- チームの書く文化がないなら、自分で育てる他に方法がないので、まずは自ら積極的な姿勢を見せることから始めましょう。
✔ 置き場所を整理する
- すぐに取り出せないドキュメントは、すぐに陳腐化します。
- 陳腐化したドキュメントは誰にも必要とされず、折角の努力が無駄になるのです。
- チームメンバーのアクセスしやすい場所に公開しましょう。
- 資料の利用者からフィードバックを受けましょう。
- 置き場所に気を使い、整理と周知を怠らないようにしましょう。
✔ 共有する
- チームメンバーの誰でも更新できるようにしましょう。
- 更新されないドキュメントは、すぐに信用がなくなって参照されなくなるので、これだけは避けるべきです。
- チームメンバーと同時編集できる環境が理想的です。(Google ドキュメントなど)
- Git ホスティングサービス上の Wiki も、プルリクエストとの紐付けが便利なのでオススメです。
✔ 「文書」と「表」を使い分ける
- 道具は適材適所で、本来の用途で使い分けましょう。
- 例えば、マトリクス形式の試験書は「表」にまとめ、その試験のための前提説明や試験環境、補足などは「文書」にまとめるべきです。
- 基本的に「文書」を優先して使いましょう。
- 「文書」に「表」を埋め込むことはできますが、逆はできません。
-
見出しをうまく使って目次を作ると、読む側の負荷を大きく軽減できます。
Excel 方眼紙の長文を読むのが大変な理由はこれです。
✔ 図表を使う
- 他人が読みやすいことを考慮し、簡単でも良いので「図表」を使いましょう。
- 特に「要件定義/仕様書」でよく使う「シーケンス図」や「フローチャート」は、息を吸うように作れるようになりましょう。
- 最初は、紙に手書きして写真を撮るだけでも構いません。
ただし、Mermaid のような何らかのツールが使える方が効率的です。
- 最初は、紙に手書きして写真を撮るだけでも構いません。
✔ 説明するものに「仮の名前」を付ける
- 仮でも良いので、ドキュメントの主目的となる「説明対象」には必ず最初から固有の名前を持たせましょう。
- 名前は口にしやすく、他の名前と重複しないようにしましょう。
- 名前があれば、コミュニケーションが円滑になります。
- e.g. 新機能なら「○○ 機能(仮)」
- e.g. 障害なら「○○ が ○○ する問題」
✔ 組になる単語を使う
- 組になる単語を使うと、文が理論的になって説得力が増します。
- e.g. 「課題 / 対策」
- e.g. 「現象 / 原因 / 対策」
- e.g. 「仮説 / 検証 / 結論」
- これらの単語を見出しとして使うテクニックも有用です。
✔ テンプレートは慎重に
- ドキュメントの「テンプレート」を固定するのは、基本的に避けましょう。(経費申請の帳票のような、どうしてもシステマティックに処理したい場合を除く)
- 「テンプレート」はフォーマットを自己改善しようと思う気持ちを殺し、チームの自治文化を阻害するデメリットが致命的です。
- 他のチームの「フォーマット」が、自分のチームにも当てはまると考えてはいけません。
- フォーマット自体に価値があるのではなく、フォーマットを模索するために業務を見つめ直した過程に意義があります。
- 他人任せにせず、自分の頭で深く考えなければ、当事者意識が発生しません。もちろん最初に当事者意識を持つべきは自分自身です。
✔ 厳しいルールは不要
- 特別な理由がない限りは「伝われば良いだけ」なので原則はフリーフォーマットとし、ルールを定める場合は緩やかなガイドライン程度にしましょう。
- ただし、ドキュメントの配置場所だけは厳格にしましょう。最初に悩んでおくと、後で楽になります。
- オススメのガイドライン:
- ➊ 分量が多くなる予定のドキュメントには、最初に以下を明記する(SHOULD)
- なに: 何のドキュメントか?
- なぜ: ドキュメントの目的/やらないことは何か?
- いつ: いつ読んで欲しいか?いつ更新するか?
- 誰が: 誰が読んで欲しいか?誰が更新するか?
- ➋ 見出しを付ける(SHOULD)
- e.g. 課題、対策
- e.g. 仮説、検証/考察、結論
- e.g. 現象、発生環境、再現性、回避方法、原因、対策
- ➌ 単位の省略は禁止(MUST)
- NG: 1 G
- OK: 1 GB
- ➍ 英数字は半角を推奨する(SHOULD)
- NG: 1 GB
- OK: 1 GB
- ➎ なるべく正式名称を使い、略語を避ける(MAY)
- NG: WiFi
- OK: Wi-Fi
- ➊ 分量が多くなる予定のドキュメントには、最初に以下を明記する(SHOULD)
✔ 説明会を開く
- ドキュメントの分量が多い場合、周知するために口頭でも説明しましょう。
- 「読んでおいて」と言うだけで、隅々まで読んでくれることを期待してはいけません。
- どんなに内容が重要でも、口頭で念押ししない限りは確実に伝えたことになりません。
- ドキュメントなしで口頭での説明で済ませることは避けましょう。
- 発言した本人でさえ、発言した内容の記憶はすぐに曖昧になるからです。
- 大切なことほどドキュメントに残し、後から正確に思い出せるようにしましょう。
👑 対顧客のテクニック
顧客とのコミュニケーションは、常に不平等な関係にあることを忘れてはいけません。
買い手である顧客は、売り手である自分から見れば、絶対的に不利な相手なのです。
対応方針が曖昧なままだと、いらぬ面倒事を引き寄せることになりかねません。
ここではそんな状況でも簡単に使えるテクニックについて挙げます。
✔ 第一印象を制御する
- 相手が仕事ができそうに見えるかどうかの第一印象は、今後の関係に大きく関わる
- 最初に自信があるように見せるだけで、相手からの信頼感の初期値が高くなる
- このアドバンテージは貴重で、初回にしか発生しない
- 激的なプラスというより、マイナスから始まらないようにするのが目的
- 服装なども気を付けると尚良い
✔ 自信があるように見せる
- 相手の目を見て、怒鳴らない程度に声を大きくする
- 心の中がどうであろうと、自信なさげには見えないことが重要
- 「分かりません」と伝える時も、なぜか声が大きいだけで説得力が出る
- 不自然でない程度に、ゆっくり話すのも効果的
- ミスを恐れず、慌てずに調子を崩さない方が良い
- ミスがあれば、その時に謝れば良い
- 結局、初めてやることはミスが起きないことの方が少ない
✔ 愛想よくする
- 相手を見て、声のトーンを高くする
- 心の中がどうであろうと、愛想良さとして伝わる
- 不自然でない程度には、相槌を打つことを忘れない
- 共感のサインが、相手の気分を良くする
✔ 信頼させる
- 相手の期待する表情/声になる
- e.g. 真剣に話す/聞く時は、目線を合わせ、声のトーンを落とす
- e.g. 叱責される際は、申し訳ない表情/声に
- e.g. 突拍子もないと相手が思っているなら、驚いた表情/声に
- 相手の意図に即応する
- 表情/声の切り替えが弱い/遅いと、相手にサインが伝わらない
✔ 怒りをしずめる
- 相手の期待することを先読みする
- 信頼させる表情を崩さない
- 相手の威勢に惑わされず、自分の見え方に気を配る
- どんなに怒りたがっている人でも、ひとつの怒りは長続きしない(対応を間違わない限り)
- 早めに謝罪の言葉を述べる
- タイミングがないまま相手が話し始めた場合は、その話が終わるまで待って良い
- 相手の言い分を全部聞くこと。終わるまで、絶対にこちらから説明を始めない
- 相槌はシンプルで良いが、声のトーンに共感のサインを込めること
- 下手に「ごもっともです」などと言うと、余計なことまで肯定したと取られてしまうので避ける
- 「いえ」「ですから」などの否定的な単語はなるべく使わない
- 説明に何度も失敗するようなら、相手を否定するのではなく、自分の作戦を変えたほうが良い
- 対応方針の練り直すためには、時間を改めて仕切り直す選択肢があることを忘れずに
- 信頼させる表情を崩さない
- 対応に失敗した場合、時間を置いたり、別の人間が対応したり、同じ対応を繰り返さないこと
✔ サービスしすぎない
- 相手にサービスしても、大抵は返ってこない
- サービスしても、それが返ってくる前に関係が切れるのなら、サービスすべきではない
- 愛想よく丁寧に断ること
- 契約上で要求された内容でない限り、要求に応じる義理はない
- 一度だけと受け入れてしまうと、面倒が続く
👑 マスターすべきスキル
テクニックより習得に時間がかかるスキルについて考えてみましょう。
✔ エラーメッセージを無視しない
英語でも逃げてはいけません。
最初は苦しいですし、慣れてきても苦しいですが、逃げても問題は解決しません。
分からないことに気づいたら、ググりましょう。
そうしなければ、いつまでたっても無知な状態から脱却できません。
当然ながら、Qiita や Stack Overflow などのインターネット上の情報が有益だからといって、鵜呑みにしてはいけません。
間違っていることもよくあるので、軽率にコピペせず、内容に理解しようと努め、可能な限り検証しましょう。
✔ リファレンスやチュートリアルから逃げない
始めて挑戦するプログラミング言語やフレームワーク、ライブラリ、ツール、その他なんでも必ず公式サイトのリファレンスを通読し、チュートリアルをこなしましょう。
曖昧なインターネット上の記事を参考にするより、リファレンスを読むだけで問題が解決するケースは非常に多くあります。
これが即戦力となる最短経路であることは間違いないので、面倒でも時間を作りましょう。
「見聞きした知識」より「自分で試した経験」の方がずっと強力な武器になります。
また、定期的に公式サイトをチェックして、更新された情報を確認したり、覚え間違いを正すことも忘れてはいけません。
なお、業務時間だけでは部分的な知識しか身につかないでしょう。
チュートリアルは全体像を把握するための一番手軽な方法です。
副次効果として、チュートリアルを実践した環境を使うと、細かい確認をするのに非常に役立ちます。
本当に欲しい情報がどこにもないので自分で検証するかない、というタイミングが必ず来ます。
例えば Git なら git clone
と git init
の挙動の違いを確認するなど、手を動かすことを敬遠してはいけません。
✔ 実証主義者になる
『愚者は経験に学び、賢者は歴史に学ぶ』
― オットー・フォン・ビスマルク
🔖 検証せよ
問題を解決する時、勘と経験に頼ることは、現代で天動説を信じるようなものです。
歴史を振り返れば、地動説を発見した人々は仮説と検証によって証明を積み重ねてきました。
プログラマーも同じく、原始的なオカルトを捨て、現代的な科学的なアプローチで問題を解決しなければなりません。
実際、障害解析やパフォーマンス・チューニングなどでは、プログラマーの大半の時間は「仮説を確認する検証」に費やされることになります。
陥りがちなオカルトは以下のとおりです。
- ❌ 思いつきの結論に飛びつく。
- 結論を急ぐあまりに、自分の曖昧な思いつきを信用しないこと。検証した上で結論を出すこと。
- ❌ 直感(勘や経験)に反することを信じない。
- 計測データがおかしい可能性より、自分の直感に誤りがある可能性を疑うこと。
- ❌ 問題の観察より、仮説や計測を重視する。
- 自分の仮説を信用しすぎて、現実の問題からピントがずれてしまうことに気を付けること。
- もし問題が観察できない場合、何とか観察する術をひねり出すこと。
🔖 記録せよ
作業途中を含む、現象の観察~仮説~計測~考察について検証記録ドキュメントを残すことを強くオススメします。
ドキュメントを作ることは、「近視眼的・主観的な内容」を「俯瞰的・客観的な内容」に整理する最良の方法です。
まとめているうちに正解にたどり着くケースも非常に多いので、頭の中だけで考えないようにしましょう。
アウトプットを面倒に思ったり、怖がったりするのは、結果的に時間の浪費になります。
また、チームで作業する場合は、ドキュメントがないと情報共有に失敗するリスクが発生します。
自分の頭の中にあるだけではコミュニケーションに失敗するだけなく、チームメンバーの成長につながらず、知識が属人化した不健全なチームを生むのです。
✔ 基礎を固める
基礎が重要な理由は言うまでもないでしょう。
野球の説明に「バット」を「木の棒」と言い換えて説明したくはありません。
しかし、実際には最低限必要な基礎知識を挙げるだけでも、以下のとおり多岐にわたります。
一朝一夕では身につかないので、コツコツやるしかありません。
なお、基礎すら分からない分野を作らないように気をつけてください。
結局のところ、最終的にどの分野とも無関係にはいられません。
- プログラミング言語
- サードパーティ製のソフトウェア(OS、フレームワーク、ライブラリ、マネージドサービス(AWS、Azure、GCP))
- データベース(リレーショナルデータベース、キーバリューストア、ドキュメントデータベース)
- ネットワーク
- セキュリティ
- パフォーマンス
✔ ツールを使いこなす
一流の職人が様々な道具を使い分けるように、プログラマーも各種ツールに習熟することが求められます。
適した道具を使わなければアマチュアレベルの仕事しかできません。
習熟には時間がかかりますが、「短期的なコスト」と「長期的なコスト」を天秤にかけて、楽をするために苦労しましょう。
ソフトウェア開発に求められるツールは、以下のようなものがあります。
- IDE
- パッケージ管理システム
- e.g. Homebrew、npm
- コマンドラインツール(UNIX ツール)
- e.g. SSH、GNU Core Utilities
- Git クライアントと Git ホスティングサービス
- e.g. git コマンド、GitHub
- Docker
- マネージドサービス(AWS、Azure、GCP)
- テストツール
- e.g. Postman、Jest、Puppeteer、WebDriver
- CI/CD ツール
- e.g. Jenkins、CircleCI、Travis CI、GitHub Actions、AWS Code シリーズ
- 構成管理ツール
- e.g. Chef、Ansible、Puppet、AWS OpsWorks
- 監視ツール
- e.g. Zabbix、AWS CloudWatch、Mackerel、Datadog
- コミュニケーションツール
- e.g. Slack、Chatwork、Google Workspace、Microsoft 365
✔ 最新情報を入手する
定期的にメディアをチェックしていないと、知識が更新されません。
今の業務で使わない知識でも、いずれは必要になります。
手始めに以下で情報収集を始めてみるのがオススメです。
-
Google アラート
- 気になるキーワードを登録しておくと、情報を自動収集してメールに送ってくれる。
-
はてなブックマーク
- 「テクノロジー」のカテゴリは、毎日チェックしておくと良い。
-
Twitter のトピック
- トピック機能で、気になるものをフォローしておくと良い。
- 玉石混交だが、注目が集まっている情報を入手しやすい。
また、有用なメディアは多々あるので、ウォッチしたいサイトが見つかれば RSS に登録しておくのは良い方法です。
✔ 学び続けること
cf. プログラマが知るべき 97 のこと/学び続ける姿勢 - Wikisource
cf. プログラマが知るべき 97 のこと/1 万時間の訓練 - Wikisource
何事も習熟には想像より遥かに時間がかかります。
ある道のエキスパートになるには、一般的に最低でも 1 万時間はかかると言われます。
これは膨大な時間で、1 週間に 20 時間をかけ続けても 10 年はかかる計算です。
しかもただの 1 万時間ではなく、集中した時間だけで 1 万時間が必要なのです。
ちなみに、業務時間がそのまま自分のスキルアップにつながることは少ないでしょう。
本人の希望通りの作業ができることは稀です。
課題設定とモチベーションのために、習熟時間の目安を挙げます。
自分がどの位置にいるか意識することで、次の目標を定めることができます。
-
10 時間の壁:
- 何かを継続できる条件は、「集中した 10 時間分を過ごせるか?」だと思う。
- ヒトは容易く誘惑に負けるので、習慣化の壁は意外にも厚い。
-
100 時間の壁: 20 時間/週で 1 ヶ月
- 基本中の基本を身につけ終わり、訓練のスタートラインに立ったところ。
- プロフェッショナルを名乗るにはほど遠い。
-
1,000 時間の壁: 20 時間/週で 1 年
- アマチュアを脱却してプロフェッショナルを名乗れる最低ライン。
- 視野が広がってきたが、まだまだ知らないことは多い。
-
10,000 時間の壁: 20 時間/週で 10 年
- ようやくエキスパートを名乗れる最低ラインに立った。
👑 新しいプロジェクトにアサインされたら
新しいプロジェクトにアサインされたら、最初にやるべきことを挙げます。
チームの受け入れ体制が常に万全というわけではありません。
自発的に情報を収集しましょう。
- ➊(リリース済みのプロダクトなら)公式サイトと、操作マニュアルを読み込む。
- 少なくとも顧客の知りうる情報くらいは網羅しておく。知らないと思わぬ問題に発展する。
- 動作環境、SLA/SLO、利用規約、契約ごとの保守期間といった顧客との約束もよく理解しておくこと。
- ➋ 設計書を読み込む。
- システム構成や OS、フレームワーク、主要なライブラリを把握する。
- サードパーティ製のソフトウェアを利用する場合は、「サポート期間などのライフサイクル」や「脆弱性情報のキャッチアップ方法」も確認すること。
- ➌ 仕様書を読み込む。
- Web アプリケーションの場合、特に DB と Web API の仕様書は必ず目を通すこと。
よく参照することになるので、いつでも最新版を参照できるように準備しておくこと。 - 本当はソース全体を把握したいところだが、まずは技術ドキュメントを隈なく読むこと。
- Web アプリケーションの場合、特に DB と Web API の仕様書は必ず目を通すこと。
- ➍ 試験書に目を通しておく。試験に参加できれば尚良い。
- 案外、作る側は使う機会がないことが多いので、そういう機会があれば活用すること。
- 使わないと分からないことは多い。
- ➎ 開発環境を確認する。
- 開発環境の構築方法、コーディング規約、コミットフロー、新機能開発時やバグ発見時の対応フロー、リリース/デプロイ方法、ログ解析ツールなどを確認する。
👑 新入社員の方へ
学生から社会人になると、周りは年上の大人ばかりで、自分の知識のなさに圧倒されると思います。
そうすると、大人たちに全幅の信頼を寄せてしまいがちですが、それは良い考えではありません。
自分の中学や高校のクラスを思い出してください。
30 人のクラスなら、5 人は優秀、5 人は落ちこぼれで、残りの 20 人は平凡であったと思います。
彼/彼女らが大人になっても、それは変わりません。
つまり、すごそうに見える大人たちでも、世の中の大多数の人間は平凡な能力しか持たないのです。
人には向き/不向きがありますし、能力は平等ではありません。
そして、年齢と能力に相関関係はありません。
何年も業務でコーディングしていても、適当に仕事をするような人間(そういう人間は驚くほどいます)は、入社までに数千時間コーディングしてきた新入社員に劣るものです。
あるいは優秀な大人でも、ずっとそのチームにいるが故に見えなくなってしまっている問題もあります。
ですから、大人に頼り切りではダメで、問題を見つけたら自発的に対策を立て、行動することを忘れないでください。
でしゃばると煙たがられることもありますが、問題は改善されないより改善される方がずっと建設的です。
なお、どんな大人も年長者のプライドがあるので、尊敬されることを意識的/無意識的に望んでいます。
私はあまり得意ではないのですが、リスペクトのサインを出すことは、円滑なコミュニケーションに極めて有用です。
👑 新人教育を担当される方へ
実際のところ、簡単なテクニックを伝えることはできても、その応用やマインドセットを教育するのは困難です。
我々が義務教育で教わったことをあまり覚えていないのと同じく、教えたことはほとんど理解されません。
したがって、新人教育の役割のほとんどは、やり方を教えることよりも、内発的動機づけ(本人のやる気)のサポートと、失敗のカバーだと思います。
やる気を引き出すためには、自己決定権を与える必要があります。
何でも上長に確認をとらされるような裁量権が一切与えられていない状態は、やる気を下げる結果につながります。
少なくとも、強制はうまくいきません。
さらに新人が自発的に行動できたとしても、自転車の乗り方を覚える時のように、新人の数年間は失敗の連続です。
失敗を経由せずに習熟することは不可能ですから、教育担当者は失敗可能な舞台をお膳立てする必要があります。
具体的には、主に失敗のカバーに時間を割くことになります。
このような新人教育に求められる資質は、何度も失敗することに忍耐強く耐え、彼らの人格を否定することなく、モチベーションを維持させる能力です。
この資質は、万人が持つわけではありませんが、他に適任者がいなければ自分で何とかするしかありません。
この資質を補うための手段は『新1分間マネジャー』を参考にするのがオススメです。
短い内容で比較的実践しやすい内容がまとめられています。