「説明変数」と分かりやすいコードを書くことの価値
過去に「読み手に詳細が伝わらない変数名を書くな」っていう記事を書いたことがあるけど、今回はその発展系というか、焼き直しというか。
名著「リーダブルコード」に「説明変数」という言葉が出てくる。統計学の文脈で「説明変数と目的変数」といった用いられ方がするモノとは全く別の言葉。
今回はこの「説明変数」という言葉を用いて、読み手にとって分かりやすいコードというモノがどうしてそれほど重要視されるのか、というところを整理してみる。
目次
- 筆者の立ち位置と前提整理
- プログラムは「書いている時間」より「実行される時間」「読まれる時間」の方が長い
- 読み手に伝わりにくいコードが生まれる根本原因
- 「読み手にどう思わせたいか」が表現できていれば良いコード
- 悪いコード例から「説明変数」を説明する
- 「説明変数」を使って良いコードに書き直してみる
- 改善後のコードを細かく見ていく
- 他の説明変数の例
- 「変数の命名をサボれば早くコードが書ける」なんてウソ
- 文字数・行数が長くなっていいから、スラスラ読めるコードを書こう
筆者の立ち位置と前提整理
この記事を書いている僕の立ち位置と、そんな僕が何を重視している人間なのか、という前提を整理しておく。
- 僕は職業プログラマ・エンジニアとして、システム開発や保守を生業にしている
- 多人数で一つのシステムのプログラムを書く経験もあるし、よそのベンダ (開発会社) が開発・リリースしたシステムを引き継いで保守しつつ機能改修する、といった仕事の経験もある
こういう出自の自分は、プログラミングにおいて次のようなことを重視している。
- 最初からミスなく成果物を作れること。ミスによる手戻りをしたくない
- 自分の手元を離れても長く生きるコードを書きたい
- 成果物が誰の手に渡っても保守・改修してもらいやすくしておきたい
こういうことを重視しているので、僕は「読み手に詳細が伝わらない変数名を書くな」といったことをクドクド書いていたし、今回の記事でも書く。
だもんで、趣味でプログラミングしている人とか一人で開発している人とか、「書いたコードを他の人が保守するなんて場面は絶対に発生しません」といった人は、僕がこれから話すことはもしかしたら重視されないことなのかもしれない。でも、そういう人達にとっても決して無意味なことでもないと思っている。
それでは、職業エンジニアとして、こういうことを重視するのはなぜなのか、順に説明していく。
プログラムは「書いている時間」より「実行される時間」「読まれる時間」の方が長い
まずは当たり前のところから。プログラム、コードというモノは、「プログラマがコードを書いている時間」よりも、「そのプログラムが実行される時間」や「そのコードを自分や他の人が読んでいる時間」の方が長いモノなのだ。
- 「開発に5年かかったけど、リリースして半年稼動させたら全破棄します」なんていうプログラムを作ってしまったら、基本的には採算が取れてないので「悪いコード」となる
- 住宅建築だって同じ。「法定耐用年数」という考え方もあるように、1・2年で建てて30年とか40年とか住めないような建物は、採算が取れていないことになる
このように、作るのにかかる時間よりも、利用する時間の方が長いからこそ、わざわざコードを書いてシステム化・自動化することに意味・効果が出てくる。
しかし、一度書いたコードを一切変更せずに何年・何十年と運用し続けることは不可能に近い。対象システムの機能追加・機能改修の依頼が舞い込んでくることもあれば、そうでなくとも経年により周りの状況の方が変わるので、それに合わせる必要が出てくる。
- 単純にお客様からの機能追加・機能変更の要望により改修案件が始まる
- 「当時はこういうつもりで開発してもらったけど、やっぱりこういう機能もほしいな」
- 「新しくできた部署からの要請で、こういう帳票を出力してもらいたくなった」
- システム化した対象業務が、法改正・法規制などにより変更が入るケース
- EC サイトのシステムで例えれば、増税されたら消費税率のパラメータを変更しないといけないし、「軽減税率」のように新しい制度が追加される場合もある
- OS やランタイムのバージョンアップ、セキュリティアップデートに追従するため、機能や振る舞いは変えないがコード修正が必要な場合
- 例 : Windows Server の OS が EOL を迎えるので、OS をアップデートしないといけなくなった
- → 現状利用している (.NET などの) ランタイムのバージョンが、新しい OS では対応していないので、ランタイムのバージョンも上げる必要が出た
- → 新バージョンのランタイムでは、既存コード中の ○○ API が Deprecated になっていて、新しい API を使って実装しないといけない
- …こういう場合に、見た目に分かる挙動の変更はないが、実際のコードは一部書き換えることになる
そういった理由で、コードを書いていた当初は「完璧なモノができた!もう二度とこのコードに触れることはないぞ!」と思っていても、リリースして1・2年、いや数ヶ月くらいで、もう一度そのコードを開いて編集する必要が出てくるモノなのだ。
既存のコードを読み、どこに何を書けば要望を満たせるのか調査を始めるワケだが、すると今度は、コードを書いていた本人でさえも当時どういうつもりでそんなコードを書いたのか覚えておらず、改修方法がすんなり出てこない、といった事態が容易に発生する。人間は忘れる生き物だ。自分の渾身のコードでも、書き終えたら細かいことは忘れてしまうし、当時は想定していなかったような内容を付け加えることになるワケで、仮に細かいことを全部覚えていたとしても、改修は難しいことなのだ。
最初のコードを書いた本人がこんな調子だ。では、「機能改修案件」が始まったことで新規参画させられた開発メンバはどうだろう。メチャクチャ当たり前のことを言うが、新規参画したメンバは「当初のコード」を書いていないので、全体を読み解く必要が出てくるのだが、その人の労力を想像できるだろうか。つまり「当時の自分はこう考えて書いていたと思う」という予備知識がない状態の人達がコードを読解するワケで、そのコードの書き方が悪かったら、どれだけ苦労するだろうか。
- 筆者や編集者が手間をかけて出版した本は、普通に読める
- 一方、同僚や友人の走り書きのメモは、てにをはや主述もグチャグチャで、何のことが書いてあるか分からなかったりする
さて。
「当時急いで書いていたんで、変数名とかテキトーです、インデントも間違いまくってます、どの関数がどこに影響しているか分かりません」
こんなコードを渡されて、「なんとか読解して必要な修正を入れてください。もしも修正内容が間違っていてバグらせたら損害賠償です」と言われる怖さが、少し想像できただろうか。「当時の自分は何を考えてこんなコード書いてたんだよぉ!!」という過去の自分への怒りも湧くかもしれない。自分がその現場を離れていて、他の人が改修案件を担当していたら、「あの野郎、クソコード残して逃げやがって」と嫌われるかもしれない。「成果物の品質が著しく悪い」となったら、人事評価に響く恐れもある。
こうならないためには、プログラムを書いている段階から、実行される時のこと、未来の自分や他の人がこのコードを読む時のことを考えて、いかに読み手に配慮したコードを最初から書けるかが重要なのである。
読み手に伝わりにくいコードが生まれる根本原因
世のウンコードのどこが悪いのか。「ガード節がなくインデントが多い」「グローバル変数を多用している」などなど、プログラミング的なテクニックからの指摘もできるが、それらの根本原因は一つである。
そのコードを書いている瞬間の、書き手の思考しかコードに残されていないのが根本原因である。
どういうことか。
そのプログラマは、今まで Java の経験しかなく、Python を使った案件は初めてで文法を調べながらコーディングしていたかもしれない。突然金融系の会社の案件に参画させられて、金融業の専門知識がないまま、それらを処理するシステムの開発を任されたかもしれない。上長のサポートが不十分で、分からないことを質問できる場がなく、独力でこなすしかなかったのかもしれない。
時間がない、知識がない、余裕がない。原因はともあれ、そうした目の前のことで精一杯な人は視野が狭い状態である。そんな状態では、独りよがりで自分本位なコードしか書けないだろう。
- プログラミングに限らず、文書作成、その他の生産活動に置き換えても理解できるだろう
しかし、そもそも、コードを書いたり、文章を書いたりする時に、「他人目線」で物事を考えながら書いたことがない、という人も、想像以上に存在するっぽい。「視野が狭い」状態がデフォルトの人生だった人達だ。かなり恐ろしいけど、「自分がコードを書く」ことと「読み手のことを考えること」がどうして関係するのか、全く想像もつかないという人種も、かなり見てきたので、その辺ももう少し深堀りしてみる。
「読み手にどう思わせたいか」が表現できていれば良いコード
さて、職業エンジニアがプログラムを書くにあたって、
- 自分がコードを書いたら、それを読む人が必ずどこかにいる
- 読み手はコードをすんなり読解できると助かるし、理解できないとメチャクチャ困る状況にある
という話は理解していただけたと思う。そして、
- 分かりにくいコードというのは、得てして「自分目線」でしか考えていない人が書いたコード
という話もした。
それではどういうコードが良いコードなのか、一言でいえば、読んだ人に「このように思わせたい」という意図が表現できていて、その意図したとおりに読み手に理解してもらえるコード、といえるだろう。
例えば、懇親会などの招待状を作るときのことを考えてみる。
- 招待状を送る側は、相手に「参加するかしないかを、○月×日までに返事してほしい」と思っている
- 「へぇ~それは面白そうなイベントですね!」だけの返信なんかは、相手に期待していない。参加か不参加を答えてほしいし、回答してほしい期日がある
- そしたら、相手に期待どおりのアクションを起こしてもらうために、何と書いたら良いだろうか
- → 「参加できる方は『参加』に ○ を、参加しない方は『不参加』に ○ をして、○月×日までに (私) 宛にご返信ください」というように、招待状に書くことだろう
- 「参加か不参加かを答えてくれ」とハッキリ書くはず
- 「いついつまでに」の情報を書かずに送ることは考えられない
招待状ひとつ取っても、「相手に参加可否を答えてほしい」という「期待するアクション」があるワケで、それが相手に伝わるように招待状を書かなければ、相手は参加可否を答えないかもしれないし、期日を知らないがために回答が遅れるかもしれない。
会話、電話、メール、文書のやり取り、コーディング、そうしたあらゆる情報伝達は、送信側が、相手に何らかのアクションを期待しているから行われる、という根本をしっかり押さえ直そう。
- 登壇やスピーチというのは、「自分の話を聞いて、あなた達にもこういうことを考えてほしい・こうしてほしい・こう感じ取ってほしい」という目的があるから行うモノである
- 自分が発表をする緊張感だけで登壇してしまうと、聞き手にとっては「んで、何が言いたいの?」と終わってしまうことになる
- 「なるほど、そういうことなんだな、自分もそうしてみよう」と聞き手に思わせるように、説得するつもりでスピーチをするのが前提となる心構え
- 小説や映画の脚本なども、「ココで主人公の家族に不幸が起こることで、主人公に感情移入して感動してほしい」もしくは「復讐心に燃える主人公の気持ちに同調してほしい」という風に、読み手に期待する反応があって執筆される
- 書き手としてシナリオや世界観を妄想しているだけの視点では、読み手の気持ちを操作できない
- 作業手順書を書く時は、自分ではない作業者が作業することを踏まえて、色んな説明を書く必要がある
- その手順を実行した後に、どういう結果が出力されれば成功していて、何がダメな状態なのか、「期待値」「期待結果」を明確に書く必要がある
- 「このぐらい書かなくても常識的に分かるだろ」と思って書くのをサボったことで、「作業者の勘違いにより想定外の作業をされてしまった」というインシデントに繋がる恐れもある
- 作業者の勘違いを生んだのは、作業手順書を作業者のために適切に書けなかった、執筆者の落ち度である
コーディングも、それと同じだ。いつかこのコードを読んだ人が、この関数やクラスをどういうモノだと思ってくれるだろうか? それをよくよく想像し考えながら、読み手に「コレはこういうモノなんですよー」「この関数で事前処理をして、こっちの関数で後処理をしているんですよー」などと解説するかのようにコードを書いていくのだ。
悪いコード例から「説明変数」を説明する
読み手に「こう捉えてください」「こう思ってください」と、解説・説得するように、コードを書くことが、読みやすい・分かりやすいコードに繋がる。
書き手の意図を読み手に伝えるための、一番基礎的なテクニックの一つが「説明変数」である。やっと出てきた。w
例えば、とある帳票を出力するプログラムを考える。書き手としては、
- 帳票の「レイアウトを組み立てる関数」を切り出しておけば、「帳票を出力する機能全体のコード」が肥大化しないで済むだろう
なんて考えてコーディングをしていたが、思考が整理しきれておらず、またプログラミングの基礎スキルも不足していたとする。書き手の思いとは裏腹に、実際のコードが以下のようなコードだったらどうだろうか。
- JavaScript による擬似コード
new Sheet()
やaddRow()
など、何らかの帳票用のライブラリが提供する API を利用しているとする
function print(t) {
let w = new Sheet();
let g = w.addRow()
g.addColumn();g.addColumn();g.addColumn();
a = fetch();
g = w.addRow();
if ( t == '1') {
c=g.addColumn(); c=g.addColumn(); c2=g.addColumn();
c.mergeColumns(c2);
} else {
g.addColumn();
g.addColumn();
g.addColumn();
}
dl(w);
}
function dl(s) {
s.getRow(0).getColumn(0).setValue(a.namae);
s.getRow(0).getColumn(1).setValue(a.age);
s.getRow(0).getColumn(2).setValue(a.brtd);
s.getRow(1).getColumn(0).setValue(a.no);
if(s.getRow(1).getColumn(2)==null) {
s.getRow(1).getColumn(1).setValue(a.address);
} else{
s.getRow(1).getColumn(1).setValue(a.country);
s.getRow(1).getColumn(2).setValue(a.address);
}
Printer.print(s);
}
適当にこしらえたウンコードではあるが、コレでもパッと見での文句は色々あると思う。
- ほとんどの変数名が1文字で何のことか分からない
- それでいてコメントすらない
- インデントやスペース・改行を挿入するポイントが不揃い
- 変数のスコープがデタラメ
- 変数
c
はグローバル変数になってしまっているが、書いた本人は意図していないと思われる - 変数
g
は再代入しているが、この関数を修正する際に何をしたらバグに繋がって、何をすれば「2行目に新しい行を挿入する」改修を適切にできるだろうか?
- 変数
- セミコロンの有無もデタラメ (多分
w.addRow()
の後ろにセミコロンがないことに、書いた本人は気付いていない) print()
関数の引数t
を用いてやっている条件分岐とほとんど同じ内容が、dl()
関数のif
文に相当しているprint()
関数とdl()
関数は、結局どっちを呼べば「帳票出力ができる」のか?- 少なくとも関数名では判別できず、コードを全部読まないといけない
- 実際は、依存関係を整理してパブリックな関数とプライベートな関数を区別するのが適切だが
print()
関数がレイアウトを作っていて、dl()
関数が値を埋め込んで実際に帳票を出力する関数…のつもりで作っている。帳票に書き込むテキストデータは変数 d
に格納されているのだが、分かっただろうか。
コレはかなり下手に書いた例だが、プログラミングのテクニック的なこと以前に、コード全体から、読み手に伝わりやすくしようとする工夫が見られないのが大きな問題である。こういう状態では、そもそもコードを書く際の心持ちが足りていないので、個々のプログラミングテクニックを聞きかじったところで正しく適用できず、誤った「改善」をしてしまい余計に分かりにくくなることだろう。
設計書が別にあるとしても、コードを読む人間にとって知りたいのは、こういう内容ではないだろうか。
- 使い方 : 「帳票を出力したい」と思った時に、どの関数を、どのように呼び出せばいいのか。引数に何を与えたらいいのか
- 影響範囲 : どの関数とどの関数が、どういう順序で関連していて、どういう風に作用するのか
- バグを修正する際にどういう修正が必要なのかとか、機能を付け加える時に副作用を与えないようにするにはどうしたらいいか、といったことを計るために重要な観点
- 処理の流れ : その関数の中で何をやっているのか、コードを読み進めていけばすんなり理解できる
- 手続き的に書けという意味ではないが、物語のように上から下へ読み進めれば理解できる構成だと、読み手の負担は少ない
- 登場人物である「変数名」、大見出しや小見出しといった章立てに相当する「インデント」が適切なら、より読みやすい
- 日本人の場合、英語を読むスキルに差はあれど、アセンブラ言語みたいな見た目よりは自然言語に近い方が、大抵は「読みやすい」と感じることだろう
「説明変数」を使って良いコードに書き直してみる
さて、それでは「説明変数」という考え方で、上述のコードを書き直してみよう。
「説明変数」というのは簡単なことで、変数名によって対象や意図を説明してあげるということだ。
前述の悪いコードの「名前」に関する問題点は、ざっとこんなところがあるだろう。
- 「変数
w
」は、work
(一時変数) のつもりではなく、Worksheet のw
だった…が、その意図は伝わっただろうかdl()
関数の引数s
に与えるべきは、この「変数w
」なのだが、このs
というのは workSheet のs
のつもりだった- 同じモノを説明するのに違う変数名を利用している、というのは読み手に不親切だ
- 書き手が混乱していることは読み取れるw
- 変数
g
は「行 (ぎょう)」のつもりでg
だったし、変数a
は「値 (あたい)」のつもりでa
だった。中途半端に日本語が混じっている- 変数
c
・c2
は「column (列)」から取ってc
だった。コチラは英語から取ってはいるが、依然として1文字変数は分かりづらい
- 変数
print()
関数は帳票をプリントしそうだし、dl()
関数は (好意的に解釈してあげれば) DownLoad の略で帳票がダウンロードできそうに感じる- 関数がやることの内容、および関数の使い方が、関数名から読み取れないことが分かる
if
文の条件分岐が何を示すのか分からないif
に合致したらどういう帳票で、else
に合致したらどういう帳票なのか分からない- コードを頑張って解読すれば、何となくセルの結合があるか否かの違い、というところは分かるかもしれないが…
これらを踏まえて、変数名・関数名とインデントを中心に修正してみよう。なお、今回はもっと根本的に設計からやり直した方が良いと思われる題材だが、やんごとなき事情によって大幅なリファクタリングが禁じられている案件を想定して、比較的小規模な修正でできるだけ読みやすくなるように工夫した。
/**
* 帳票の種別コードマスタ
*
* HACK : 画面上で利用しているコード値が帳票にも紐付いてしまっているため、現状は仕方なく数値の文字列を利用している。
* それだと分かりにくいためこの定数で名前を付けてあるが、画面機能と切り離して分かりやすい種別体系にしたい。
*/
const workSheetTypes = {
japanese : '1',
fromAbroad: '2'
};
/**
* 帳票のレイアウトを作成するプライベート関数
*
* @param {number} workSheetType 帳票の種別
* @return {Sheet} レイアウトを組み立てた帳票オブジェクト
*/
function _createWorkSheetLayouts(workSheetType) {
const workSheet = new Sheet();
const firstRow = workSheet.addRow();
const numberOfColumnsInTheFirstRow = 3;
for(let num = 0; num < numberOfColumnsInTheFirstRow; num++) {
firstRow.addColumn();
}
const secondRow = workSheet.addRow();
const secondRowFirstColumn = secondRow.addColumn();
const secondRowSecondColumn = secondRow.addColumn();
const secondRowThirdColumn = secondRow.addColumn();
const isJapaneseWorkSheetType = workSheetType === workSheetTypes.japanese;
if(isJapaneseWorkSheetType) {
secondRowSecondColumn.mergeColumns(secondRowThirdColumn);
}
return workSheet;
}
/** 帳票の入力データとなる人物情報を表現する Value Object */
class Person {
constructor(name, age, birthdate, applicationNumber, isJapanese, country, address) {
this.name = name;
this.age = age;
this.birthdate = birthdate;
this.applicationNumber = applicationNumber;
this.isJapanese = isJapanese;
this.country = country;
this.address = address;
}
}
/**
* 帳票に値を設定するプライベート関数
*
* @param {Sheet} workSheet レイアウト組み立て済みの帳票オブジェクト
* @param {number} workSheetType 帳票の種別
* @param {Person} person 設定する値を持った人物情報
* @return {Sheet} 値を設定した帳票オブジェクト
*/
function _setValuesInTheWorkSheet(workSheet, workSheetType, person) {
const firstRow = workSheet.getRow(0);
const nameColumn = firstRow.getColumn(0);
const ageColumn = firstRow.getColumn(1);
const birthdateColumn = firstRow.getColumn(2);
nameColumn .setValue(person.name );
ageColumn .setValue(person.age );
birthdateColumn.setValue(person.birthdate);
const secondRow = workSheet.getRow(1);
const applicationNumberColumn = secondRow.getColumn(0);
applicationNumberColumn.setValue(person.applicationNumber);
const isJapaneseWorkSheetType = workSheetType === workSheetTypes.japanese;
if(isJapaneseWorkSheetType) {
const addressColumn = secondRow.getColumn(1);
addressColumn.setValue(person.address);
} else {
const countryColumn = secondRow.getColumn(1);
const addressColumn = secondRow.getColumn(2);
countryColumn.setValue(person.country);
addressColumn.setValue(person.address);
}
return workSheet;
}
/**
* 「申請書」帳票を出力する
*
* グローバルの `fetch()` 関数により、現在のセッションから人物情報を取得し
* 「申請書」帳票を組み立てダウンロード可能にする
*/
function printApplicationForm() {
const rawResponse = fetch(); // 人物情報を外部から取得する関数のテイ
const person = new Person(rawResponse);
// 帳票の種別を判定する
// HACK : 帳票の種別を「国内向け」「国外向け」で分類しているが、画面側の処理と切り離せたら
// 素直に2種類の帳票出力機能に分けた方が拡張性が上がると思われる
const isJapanese = person.isJapanese;
const workSheetType = isJapanese ? workSheetTypes.japanese : workSheetTypes.fromAbroad;
const workSheetWithLayouts = _createWorkSheetLayouts(workSheetType);
const workSheetWithValues = _setValuesInTheWorkSheet(workSheetWithLayouts, workSheetType, person);
Printer.print(workSheetWithValues);
}
コレでもまだ全然ツッコミどころはあると思うのだが、一番感じ取ってほしいのは、関数名や変数名によって、どんな処理をしているのか読み取りやすくなったというところ。
- プログラム的に Private なメソッドを用意した方が良いのは分かっているが、やんごとなき理由で大規模リファクタリングは NG な案件なので、プライベートな関数名の先頭にアンダースコア
_
を付ける、という苦肉の策を取っている - 「帳票の種別コード」という概念から葬りたいのだが、やんごとなき理由で画面の機能と帳票とが紐付いているという設定なので、定数
workSheetTypes
を作ることでごまかしている…というテイ - できれば直したいんだけど、色んな都合で直しきれなかった、というポイントには
HACK :
という注釈コメントを残している- こうした注釈コメントの代表例としては、
HACK :
(キレイではないコードなのでリファクタリングしてほしい)、FIXME :
(既知の不具合がある、修正が必要)、TODO :
(未完部分がある・これから実装する) などがある - 参考 : TODO: 以外のアノテーションコメントをまとめた - Qiita
- こうした注釈コメントの代表例としては、
改善後のコードを細かく見ていく
「説明変数」を用意したことで分かりやすくなったと思われる点をいくつか抜粋してみる。
// 関数名により、帳票の「レイアウト」を作る関数であることが分かりやすくなった (元は `print()` 関数だった)
// 引数も `workSheetType` という「種別」を示す変数であることが分かる
function _createWorkSheetLayouts(workSheetType) {
// 1文字変数で横着せず、「ワークシート」と書いた
const workSheet = new Sheet();
// ワークシートの1行目を示すオブジェクトとして First Row と書いた
const firstRow = workSheet.addRow();
// 「1行目には3列のセルを配置したい」ということで、「3」というマジックナンバーに説明変数を用意した
// 元は `addColumn()` を3回実行していたが、コレで「3列用意する」ことが重要であることが伝わるようになった
// さらに、1行目の列数を増やしたい場合はこの定数の値を 3 から増やすだけで対応できることも分かる
const numberOfColumnsInTheFirstRow = 3;
for(let num = 0; num < numberOfColumnsInTheFirstRow; num++) {
firstRow.addColumn();
}
一番「説明変数」として分かりやすくなったのは、numberOfColumnsInTheFirstRow
だろう。変数名としてはいささか長過ぎるかもしれないが、3
が1行目の列数を意味することは間違いなく伝わるようになった。
function _createWorkSheetLayouts(workSheetType) {
// 引き続き…
// 同じ `addRow()` でも、2行目を指していることが分かるように Second Row と書いた
const secondRow = workSheet.addRow();
// 元は `if` 文の中に一部同じ処理が混ざっていたが、
// `workSheetType` の種別に関わらず同じ動きをするコードは `if` 文の外に出した
const secondRowFirstColumn = secondRow.addColumn();
const secondRowSecondColumn = secondRow.addColumn();
const secondRowThirdColumn = secondRow.addColumn();
// 引数 `workSheetType` の値は `workSheetTypes` というコードマスタと対応することが読み取れる
const isJapaneseWorkSheetType = workSheetType === workSheetTypes.japanese;
if(isJapaneseWorkSheetType) {
// 「2行目の2列目」と「2行目の3列目」を結合していることが変数名からも分かる
secondRowSecondColumn.mergeColumns(secondRowThirdColumn);
}
if
文の条件に名前 (isJapaneseWorkSheetType
) を付ける、というのが分かりやすくなるところかも。今回の例だとちょっと分かりにくいかもしれないが、他の例でいうと、
if(person.age >= 20 && person.gender === '1') {
// ……
}
このような条件文だと、どうしてこういう条件になっているのか、'1'
という値をコーディングしていて本当に正しいのかどうか、分かりにくいところがあると思う。
コレを、次のように直したらどうだろう。
// 予め、「男性を示すコード値は 1」「女性を示すコード値は 2」と明確に定義しておく
// ちなみに性別の表記に関しては ISO 5218 という国際規格があるので、コレに合わせた値を使っておくと間違いが減らせるだろう
const genderCodes = {
male : '1',
female: '2'
};
// 予め、「成人とみなす開始年齢」を明確に定義しておく
const adultAge = 20;
// 成人しているか否か
const isAdult = person.age >= adultAge;
// 男性であるか否か
const isMale = person.gender === genderCodes.male;
if(isAdult && isMale) {
// 成人男性のみの処理……
}
if
文の部分を見ると、「もし、成人していて、かつ、男性なら…」と自然に読める文章になっていることが分かるだろう。コレが説明変数の効果である。ついでに、法改正で「成人とみなす年齢」が変更になった場合は、定数 adultAge
のマジックナンバーを修正するだけでよく、コードの見通しがよくなっていることが分かるだろう。
単にコードの行数を短くすることしか考えていないと、前者のようなコードを書きがちである。しかし、行数がかさんでいる後者のコードの方が、意味が伝わりやすく、読みやすくはないだろうか? コードの読みやすさは「行数が少なければいい」というモノではなく、意味がすんなり理解できれば行数が長くなったって問題がないのである。
class Person {
constructor(name, age, birthdate, applicationNumber, isJapanese, country, address) {
元のコードは namae
(「名前」…!)、brtd
(何の略?)、no
(何のナンバー?それとも「ノー!」の意味?) という風にクソコード感満載だったが、ちゃんと名前を付けてあげた。birthday
(誕生日) と birthdate
(生年月日) は意味が違うので、一般的な英単語の意味は辞書を引いて確認すること。
名前付けによってコードの補足解説をしていくのが「説明変数」なので、今回のような「申請書に書く生年月日」のつもりで birthday
と書いてしまうとおかしなことになる。「誕生日」というと、楽天市場のような EC サイトにある「誕生日割り引きクーポンの発行」とか、そういう違う機能を想像させてしまいかねない。
情報が相手に間違いなく伝わるのは、相手と同じ知識・共通認識を利用して話している時だけである。相手が持っているであろう常識や基礎知識に沿わない、自分だけの思い込みだったり、自分が勘違いしているモノを確認せず使ってしまったりすると、相手に誤解を与える。「ココは生年月日欄だから、変数名は『バースデー』だ!」なんて間違えていると、「説明変数」は意味を成さない。
function _setValuesInTheWorkSheet(workSheet, workSheetType, person) {
// 1行目を取り出す
const firstRow = workSheet.getRow(0);
// 1行目の各カラムは何の欄なのか、変数名で説明している
const nameColumn = firstRow.getColumn(0); // 名前欄
const ageColumn = firstRow.getColumn(1); // 年齢欄
const birthdateColumn = firstRow.getColumn(2); // 生年月日欄
// それぞれのカラムに、引数 `person` の情報を設定している
nameColumn .setValue(person.name );
ageColumn .setValue(person.age );
birthdateColumn.setValue(person.birthdate);
垂直アラインメントは好みが別れるというか、自動フォーマッタなんかはまず対応していない整形方法なので、真似するかどうかはお任せする。それよりも重要なのは、
nameColumn
にはperson.name
がセットされて、ageColumn
にはperson.age
がセットされている
ということが、説明変数によって分かるようになったことだ。コレがもし
ageColumn.setValue(person.birthdate);
なんて書いてあったら、「値をセットするカラムが間違ってるんじゃない?」と気付きやすいコードになるだろう。
function _setValuesInTheWorkSheet(workSheet, workSheetType, person) {
// 引き続き…
if(isJapaneseWorkSheetType) {
const addressColumn = secondRow.getColumn(1);
addressColumn.setValue(person.address);
} else {
const countryColumn = secondRow.getColumn(1);
const addressColumn = secondRow.getColumn(2);
countryColumn.setValue(person.country);
addressColumn.setValue(person.address);
}
workSheetType
という気持ち悪い分岐を少しでも和らげたい思いはつたわるだろうか。w
ココでは、if
ブロック内と else
ブロック内とで addressColumn
が示すカラム位置が異なる、という点が読み取れる。セル結合してあるタイプの帳票は address
だけ設定して、セル結合されていないタイプの帳票は国名と住所を設定する、というワケだ。この辺も、説明変数による名前付けが、この手の帳票ライブラリでありがちな Index (添字) の操作を多少分かりやすくできているところだと思われる。
function printApplicationForm() {
// 中略
const workSheetWithLayouts = _createWorkSheetLayouts(workSheetType);
const workSheetWithValues = _setValuesInTheWorkSheet(workSheetWithLayouts, workSheetType, person);
Printer.print(workSheetWithValues);
}
最後に、新しい関数ではあるが、「Application Form」を「Print」する関数であることがひと目で分かる、printApplicationForm()
関数を用意した。コレがパブリックな関数である。
元のコードは workSheetType
や person
の情報が微妙に混ざり込んでいて、print()
と dl()
関数の呼び出し順やスコープが分かりにくかったが、改善後のコードは、外部から呼び出す際は printApplicationForm()
関数を叩くだけで良いことが分かりやすくなったと思う。
2つのプライベート関数についても、コードの行数を節約しようと思えば
Printer.print(_setValuesInTheWorkSheet(_createWorkSheetLayouts(workSheetType), workSheetType, person));
こんな風に、変数を使わずに1行にまとめて書いてしまうこともできる。しかしコレでは、処理順が一見して分かりにくく、デバッグもしづらい。
原始的な「Print デバッグ」を書き足して表現すると、改善後のコードはこういう形で、一つひとつの中間処理の妥当性をチェックしやすい。
function printApplicationForm() {
// 中略
console.log('帳票の種別 : ', workSheetType);
const workSheetWithLayouts = _createWorkSheetLayouts(workSheetType);
console.log('帳票のレイアウトを組み立てた', workSheetWithLayouts);
const workSheetWithValues = _setValuesInTheWorkSheet(workSheetWithLayouts, workSheetType, person);
console.log('組み立てた帳票に値を設定した', workSheetWithValues);
Printer.print(workSheetWithValues);
}
サンプルコードでいうと、以上のような感じで、「説明変数」、名前付けによってだいぶマシなコードにできたと思う。
他の説明変数の例
その他に説明変数が有効な例でいうと、JavaScript の場合 map()
や filter()
のような高階関数を使う時だろうか。
const files = fs.readdirSync('./example-directory', { withFileTypes: true }).filter(x => x.isFile()).map(x => x.name);
fs.readdirSync()
というのは、Node.js の fs
モジュールが持つ API で、指定のディレクトリ配下のファイル一覧を取得できるモノ。ココでは簡単にするため同期型の API で書いている。
このコードではその後に filter()
と map()
があるのだが、引数 x
が何を意味するのか分からないし、最終的に取得できた const files
とはファイルオブジェクトなのか、ファイルのフルパスなのか、何なのかが分かりにくい。少なくとも、Node.js の API を理解して、コードを追って、「コレってこういう意味だよな」と整理しないといけないので、たった1行に対して、読み手が理解するための負荷が極めて高い。
同じ処理だが、こんな風に書き直してみたらどうだろう。
const dirents = fs.readdirSync('./example-directory', { withFileTypes: true });
const direntsFileOnly = dirents.filter(dirent => dirent.isFile());
const fileNames = direntsFileOnly.map(dirent => dirent.name);
- 1行目、
readdirSync()
の結果はdirents
。Linux が分かっている人ならディレクトリエントリのことだと伝わるだろう - 2行目、
filter()
内の引数名はdirent
と単数で書き、何のオブジェクトを操作しているのか分かりやすくしている。結果の配列名もdirentsFileOnly
となり、ディレクトリが除外されたのかと分かる - 3行目、結果は
fileNames
となっており、ファイルオブジェクトではなく、フルパスでもなく、ファイル名の文字列が格納された配列なのだと分かる
filter()
内の引数名が x
ではなく dirent
となるだけでもだいぶ分かりやすくなるし、「ディレクトリを除外しファイルだけ抽出する」「ファイル名を取得する」といったコメントを書かずとも、コードだけで相当伝わるようになったと思う。
「変数の命名をサボれば早くコードが書ける」なんてウソ
ところで、よく「急いで書いたので、変数名とかテキトーです」みたいな戯言を言う輩がいるのだが、僕に言わせればそんなの 100% ウソである。変数の命名に時間を割かなければ早くコードが書けるだなんて、ありえない。
変数名をパッと上手く付けられず、1文字の変数名にしたり、info
とか list
とか書いて「何の情報?」「何のリストなの?」と思わせているような状態というのは、どんな処理を実装するのかという言語化ができていないままコードを書き始めているだけである。設計書を書いていないか、書いていても日本語の時点で適切な言語化ができていないか、誰かが書いてくれた設計書をまともに読めていないか、のどれかである。
どんなモノを作るか、どういう登場人物が出てくるか、どういう筋道を立てて処理を進めていくのか。「シナリオ」がちゃんと描けていれば、「ココで人がでてきてー、情報をリストに追加するー」なんて表現の仕方はしないはずなのである。「ログイン中のユーザ情報をセッションから取得し、ログイン履歴一覧の配列先頭に追加する」というように、何をどうする、ということが細かく言えるはずなのだ。
それを細かく書けないというのは、どう考えても「何を実装したらいいのか分かっていない」のと同じ状況だし、明らかに読み手のことを考えていない。
というか、「最初の読み手」って、書き手本人なんだけどな? 書いたコードに対するテストコードを書いて直して、コードレビュー依頼を出すまでの間に、書いた本人がコードを読むターンが必ずあるはずなのだ。そこで自分自身がちゃんと分かって書いていることを、どこで確認しているのだろうか? その後にコードレビューを依頼するレビュアーは、どこをどう見て、どうして正しいと再鑑するというのだ? 自分の正しさを他人に確認してもらおうっていうんだから、その時点で少なくとも自分本人が理解していて、レビュアーには理解してもらえるコードじゃないといけないのに。
- 上長に提出する「勤務報告書」って、ただ「私は今日も仕事をしました」って書けばいいワケじゃなくて、「ほら私はこれこれこういうことを、コレだけちゃんとやりました、分かってくれますよね部長!」と上長に説明し、理解を得るためのモノだよね
- 自分がどれだけ頑張ったかを相手に伝えるには、相手が理解できるように言い回しを工夫したり、定量的な表現をしたり、過去実績と比較したりして、初めて伝わるモノだ
命名をサボると早く書ける、というのは、「自分自身の理解もおざなりなまま、とりあえずレビュー依頼を出すまでの時間が短く済む」ことを指しているのか?だとしたらバカげている。それは「実装が終わった」とは言わない。汚れがキレイに落ちたかどうか確認していないけど雑巾がけが終わったと主張しているようなモノだ。
こういう輩に、「テキトーに書いたのは変数名だけですか?じゃあ変数名だけ今から時間かけてリファクタリングしてください」と依頼してみると、
- 時間だけかかった挙げ句、全然相手に伝わらない変数名であることに変わりがなかったり
- 配列を示す変数名が
file
からfiles
に訂正されたが、やっぱりファイルオブジェクトなのかファイル名なのかフルパスなのかは分からなかったり
- 配列を示す変数名が
- 「やっぱり処理順と構造を変えました」なんつって一人大規模リファクタリングしてたり
- それも思考が整理しきれていない状態で思いつきのリファクタリングをしているので新たなバグを埋め込んでいたり
こんな調子で、こういう輩がまっとうなコードを出してくることは絶対にない。時間があるからないからではなく、頭が足りていないから常にクソコードが出てくるだけなのを言い訳しているに過ぎないのだ。何を作ったらいいのかも分かっていないし、適切な命名が何たるかも分かっておらず、そんなクソコードを読むハメになる他人の気持ちも考えられないバカであることをさっさと認めて辞めてくれ。
僕は、「設計書」をまともに書かないような個人的なスクリプトでも、ドラフトの状態から変数名をちゃんと考えて書いている。list
だの item
だの、意味不明な変数名を付けてしまったら、5分後の自分がどう困るか目に見えているからだ。「リストってなんだろう…?」とサッパリ記憶喪失になってしまうワケではなくて、「このリストはファイル名の一覧だったと思うんだけど、拡張子は除去してあるんだっけ、ないんだっけ?」というように、細かな仕様に自信が持てなくなるから、である。
もしもその変数名が list
なんかではなく baseNames
と書かれてたら、「拡張子を除去したファイル名の一覧」であることが確実に分かる。後からデバッグしたりテストコードを書こうと思った際も、チェックすべき観点が確実になる。すなわち「この配列には拡張子の文字列が含まれていないこと」と。
そんなチマチマやってたらコーディングに時間がかかるのでは?と思われるかもしれないが、命名によって処理内容が明確になっていくので、手戻りが発生しにくく、後で関数切り出しなどしようと思った時も切り貼りが容易だったりして、とにかく時間がかからない。元来タイピング速度がそこそこ速いのもあるし、IDE による入力補完なども駆使して、誰よりも早く、最初からレビュー指摘ゼロのコードを書けている。入力文字数が数文字増える程度の「誤差」で、早くコードが書けるとか遅くなるとか言わないでくれ。そんなことキーボード触ったことないド素人でもない限りありえないから。
自分の経験上、コードレビュー指摘が多かったのは、自分の知らない業界のシステム開発に関わったりしていて、何を実装すべきか理解がしきれていない時だった。月末に起動する基幹バッチの存在を知らずして実装できないような関連システムだとか、顧客データの扱いに法的な決まりがあって、そのとおりにデータを加工しないといけないのを知らずに、該当部分の設計書しか読んでいなかった時なんかに、「項目 A と B を取り違えています」などと指摘される感じだ。
文字数・行数が長くなっていいから、スラスラ読めるコードを書こう
つーワケで長々愚痴ってきた説明してきた「説明変数」と、分かりやすコードの価値についてだが、いかがだったろうか。
コードは書く時間より読まれる時間の方が長い。誤解なく読んでもらわないと、バグを埋め込んでしまうリスクがある。
クラス・関数・引数・変数の命名を、より説明的にするだけで、コードが自然言語のようにスラスラ読める読み物になっていき、読み手の認知負荷が軽減できる。読み手が正しく理解できたコードは、ミスなく直せるコードでもあるので、みんながハッピーになる。
どういう命名にしたらいいかハッキリ分からない状態では、本当はコーディングなんてできないはず。そんな中途半端なコードを読まされるのは、書いたコードの動作確認をする5分後の自分、レビュアー、テスター、保守メンバ、改修メンバである。皆が困るようなコードを書く前に、自分の理解を整理してから書き始めよう。
何を作るべきかハッキリしていれば、変数名の文字数が数文字増えても、説明変数を分けるために行数がかさんでも、コーディングの時間は長くなるどころか、むしろ分からないまま書き始めている時より短くなる。タイピング速度はそれを生業にしてるプロなんだから日々鍛えときゃいいし、IDE など文明の利器をフル活用して時短効率化できなくて何がエンジニアだ。頭を使って概念を具体化し、正確に言語化・文書化・コード化してこそエンジニアだ。
命名は横着こいていいポイントじゃない。たとえ時間がかかろうとも一番注意すべきポイントだ。ココさえできていれば、あとは文字を打つだけ、とすらいえる。作業の仕方・考え方が間違っているから、もっと周りの読み手のこと、コードを書いた後、リリース後の運用保守フェーズで自分のコードがどのように扱われるのかを考えて、逆算してやるべきことをやっておこう。