3行
- クソコードは無知から生まれる
- 個人的にコーディングのときに心掛けていることのまとめ
- ここに書いてあるのが全て正しいわけではないので参考程度に、という保険
クソコードを滅ぼしたい
おはようございます。デブです。
早速タイトルと趣旨が食い違っているような気がします。
さて「クソコード」という言葉があるようにコードにもピンキリがあります。
因みにピンキリの語源はポルトガル語のpintaとcruzらしいです(諸説あります)。
時としてクソコードは見た者の精神を破壊します。
私の場合は、年上で先輩で異性であんまり話したことのない方のコードをレビューしろと言われたときに「今まで何やってきたの?」という感情を常識と礼節でねじ伏せて柔らかく表現しようと言葉を選びまくったときが一番精神にキました。
虚空に口汚く文句叫ぶのが一番冷静になれる方法だと学びました。リモートワークで丁度叫びやすいので毎日叫んでます。
私の精神衛生の話は置いといて、何故クソコードが生まれるのかというと、大きく分けてちゃんとしたやり方を知らないかコーディング規約がクソないし古いに分かれると思います。
後者は「ご愁傷様です」しか言えることはないですが、前者は改善の余地が大きくあります。前向きに捉えれば伸びしろの塊です。
そう思って今後の私の労力軽減のためにも、私がコーディングで心掛けていることを「コーディング指南書」という形で作成して上司に「これを展開してほしい」と提出しました。
実際に上司と2年目くらいの後輩に説明をしたところ「分かりやすい」「根拠があって納得できる」「ノリがちょっとだけフォーマルなTwitter」と比較的好評でした。
そして数か月が経ち……何も起こりませんでした。
いやホントに何も起こらないんですよね……。「品質担当と共有した」とか「誰某さんにも説明してくれる?」とかの連絡を期待していたのですが何もないです。
結構な時間を掛けて作ったのにサーバーの隅っこで埃をかぶせとくのも勿体ないのでここに(ほぼコピペで)公開します。
コーディング初心者やこれからプログラミングを始めてみようと思う人に是非見てもらいたいです。
とはいえどこまで言っても我流ですし、コーディングに絶対的な正解はなく、またそれを提示するものではありません。
あくまでもクソコードにならないための個人的な心掛けです。(保険)
コーディングの基本
コード事例を出す場合は基本はjavascript(以降js)だが、型を使用する場合はC++(以降cpp)で提示する。
言語仕様依存の実装例は基本的に出さないので、いちいち提示はしない。
環境
エディタ
コーディングはIDEないし高機能エディタを使用して行う。
(最低でもインテリセンス/定義確認ジャンプが出来ること)
迷ったらvscodeを使用する。(2021/1月現在)
理由は以下の通り。
- OSSで無料
- MS謹製で品質が高い(アップデートも高頻度)
- シェアが高い = 多くの情報があり、日本語の情報も多い
- デフォルトでインテリセンスが備わっており、メジャー言語の開発はデフォルトでもある程度可能
- 拡張機能が豊富(多すぎて逆にデメリットの可能性があるくらいには)
命名規則
複合語
命名において複合語を使用する場合、空白を挟めない制約上複数の単語を繋げて記載する。
その繋げ方にも種類がある。
- PascalCase(UpperCamelCase)
- camellCase(lowerCamelCase)
- SNAKE_CASE(UPPER_SNAKE_CASE)
- lower_snake_case
統一されていればどれでもいいのだが、基本的には各言語の推奨コーディング規約に従う。
(標準関数/クラスを使ったときに結果的に統一されなくなるため)
例えばC#であれば基本はPascl、ローカル変数及び引数はcamelとなっている。
関数(メソッド)名
関数及びメソッド(以降、関数で統一)名は原則としてVO形式で行う。
因みに……
雑に違いを説明すると、
- 関数
トップレベルで定義されているもの - メソッド
オブジェクト内で定義されているもの
区別して厳密に呼び分けてる人は極わずかだが。
日本語がSOV言語なのでOVの方が直感的で、そう書きたくなるのは分かるがVO形式で書くこと。
//bad(OV)
function dataUpdate(){
}
//good(VO)
function updateData(){
}
ただし、以下のような場合はその限りではない。
class Xxx{
//ラップしているインスタンス、またはステートや値オブジェクトを取得する場合
function target(){}
function current(){}
function status(){}
//状態を取得する場合
function canUpdate(){}
//コールバックの場合
function onChange(){}
function onClick(){}
function willMount(){}
function didMount(){}
//接頭辞がつく場合
function tryParse(){}
function forceStop(){}
}
class Logger{
//クラスのやることが明確で、種別のみがことなる場合
//この場合はinfo/debugレベルでログを出力することが明確
function info(){}
function debug(){}
}
他にも動詞から始まらない関数名は多くある。(特に計算系)
関数をVO形式以外で命名する場合は、根拠がある場合のみ。
(標準関数や有名ライブラリが同じような処理で採用している名前、等)
クラス/変数名
名詞系にする。
例外として、状態を表す変数は関数名と同一にしてよい。
//OK
bool isValid;
bool canUpdate;
bool hasData;
フォーマット
フォーマット(あるいはスタイルとも)というのは要するに、以下のようなもの。
if(boolean){
}else{
}
//or
if(boolean)
{
}
else
{
}
や
{
int int1 = 0;//タブでインデント
int int2 = 0;//半角4つでインデント
int int3 = 0;//半角2つでインデント
}
身も蓋もないが、プロジェクト全体で統一されていれば問題ない。
ただし言語によっては公式推奨スタイルがあるので、特段理由もなく、実務で使うのならばそちらを採用するのがベター。
チームで開発する場合は事前に決めておくこと。
prettierなどフォーマットツールを採用して設定を共有すると間違いがない。
(その場合は往々にしてデフォルトが良い)
言語仕様
調べるときには
一言でコーディングと言っても言語ごとに書き方は大きく異なる。
初めて、ないし慣れていない言語を使用する場合は、調べることが多くなると思う。
その際、最初に見つけたサイトに飛びついてそのまま実装をしないこと。
飛びついたものが古い技法かもしれないし、最適な回答ではないかもしれないし、最悪大嘘の可能性も大いにある。
基本的なことはともかく、その言語特有の機能、構文を使う場合は最低でも複数サイトを確認、理想を言えば公式のリファレンスを確認し、理屈を理解してから実装する。(サードパーティーライブラリを使用する場合も同様)
また、調べた先で記載されているバージョンと自分が使用しているバージョンを比較して本当に使える技術か確認する。
演算子
演算子とは「+」とか「-」等の数値計算である。
(因みに演算される数値は被演算子やオペランドなどと呼ばれる)
演算子は大体のプログラミング言語で統一されているが、独自の演算子やオーバーロード等あるので確認しておく。
(例えばjavaの文字列比較がString.equals()であるように)
演算子の優先順位
演算子には優先順位がある。
四則演算でいうところの「÷(/)と×(*)を最初に計算する」というものだ。
しかし、プログラミングの演算は四則を遥かに超えているので必然、上記の認識だけでは足りない。
演算子の種類、優先順位は大体統一されているが、「大体」でしかないので確認しておいて損はない。
とは言え、中間変数などを用いて優先順位を気にする必要のないコーディングを心がけるのが一番ではある。
その他知っておいた方がいい言葉/概念
詳しくは解説しないが知っておいて損はない低レイヤーシリーズ。
- 値/参照/参照値(ポインタ)
- ディープコピー/シャローコピー
- ボクシング
- 数値の有効範囲
- 浮動小数の誤差
- ビット演算
- 右辺値/左辺値
品質の高いコードの書き方
品質の高いコードとは
個人的見解であり、これが100%正解というわけではないので、鵜呑みにしないで欲しい。
- ここには同意(反駁)する。理由は○○だから
- 自分ならこうする
等を考えながら読んでほしい。
大事なのは「何故か」を考え、実践することだ。
なお、ここでは関数とメソッドに区別を付けないものとする。
コード事例を出す場合は基本はjavascript(以降js)だが、型を使用する場合はC++(以降cpp)で提示する。
言語仕様依存の実装例は基本的に出さないので、いちいち提示はしない。
品質の高いコードとは?
品質の高いコード、と言っても求められる「品質」というのはケースバイケースだ。
実行速度、データサイズ、汎用性、etc……
ここでは以下の要素を「品質」と定義する。
- 可読性
- 保守性
- 再利用性
また、可読性の高いコードは必然、保守性/再利用性がある程度高くなる 。
なのでここでは可読性を重視してコーディングのコツを書き記していく。
なお、参考までにWikipediaの「ソースコード品質」には以下のように記述されている。
- 可読性
- ソフトウェア保守、ソフトウェアテスト、デバッグ、バグ修正、改造、移植の容易性
- 複雑すぎないこと
- リソース(記憶装置、CPU)を過度に必要としないこと
- コンパイルやlintでの警告数
- 適切なコメント
とは言え、繰り返すがここでは可読性、保守(、改造の容易)性、再利用性(移植の容易性)を重視する。
なぜコードに品質を要求するのか?
プログラムとは手段であり、究極的には結果さえ実現出来ていればどう作っても構わない。(意図しない副作用やバグは別として)
コードとはそのプログラムの設計図であり、言うなれば「手段を構築する手段」である。
だのに何故、そんな木っ端に品質が求められるのか。
それはほとんどのプログラムは一度作ったらおしまいではなく、修正したり流用することが常であり、それは往々にして作成者ではない他人が行うからだ。
また、未来の自分も他人である。半年前に書いたコードを完璧に覚えていられる人間は稀有だ。
3日前の自分は他人である。
コードの品質が低いとどうなるか、
- 修正したいが、どこでどの機能を実装しているのかが分からない
- 流用したいが、どこからどこまでが必要な部分なのか分からない
そんな経験はないだろうか。
私は先に挙げた三要素全てが低いコード相手に日々格闘している。
格闘する時間が出来るということは、本来よりも工数が増え、誰も幸せになれない。
少しでも可読性を高くすることで、そういった悲劇を減らしてほしい。
可読性≒命名の適切さ
可読性の高さとは、誤解を恐れず乱暴に言ってしまえば適切な命名がされているかということになる。
無論、可読性を上げるには命名以外のテクニック、そもそもの設計など他の要素もあるが、まずは適切な命名をしてほしい。
これを意識するだけでコードの品質は各段に上がる。
実際にコードをリファクタリングしながら進めようと思う。
リファクタリング
以下がリファクタリングをするコードだ。
function funcA() {
var a = 0;
for (var b = 0; b < 2200; b++) {
if ((b + 1) % 4 == 0) {
if ((b + 1) % 100 == 0) {
if ((b + 1) % 400 == 0) {
console.log((b + 1) + ":U");
a++;
} else {
console.log((b + 1) + ":H");
}
} else {
console.log((b + 1) + ":U");
a++;
}
} else {
console.log((b + 1) + ":H");
}
}
console.log("count:" + a);
}
かなり品質の低いコードである。
では何をもって品質が低いとするのか、考えてほしい。
「コメントが書かれていない」?
それ以前の話だ。(なおコメントについては後述する)
リファクタリングをする前に関数の目的(処理)を以下に記す。
- (西暦)1~2200年の各年に対して閏年(U)か平年(H)かを出力し、
- 最後に閏年の数を出力する
このコードを見て関数の目的が分かっただろうか。あるいは理解するのにどれくらい掛かっただろうか。
例えば出力内容を修正しなければならないときに、一つの出力内容を修正するのに複数個所を変更しなければならないことに気付いただろうか。
折角閏年判定のロジックが出来ているのに、この出力にしか使えないことに気付いただろうか。
このコードの良くない点をまとめると以下のようになる。
- コードの内容を理解するのが困難、ないし時間がかかる(可読性が低い)
- コピペ記述が多く、1つの修正内容に対して修正箇所が多くなる(保守性が低い)
- ロジックがその場限りで流用出来ない(再利用性が低い)
命名しなおす
まず
function funcA() {
//中略
}
という関数名をなんとかする。
「名は体を表す」というが、それがコードにおいて理想の状態である。
なので、
function printYearsType() {
//中略
}
という名前に再命名する。
これで「この関数は年のタイプを出力するんだな」というのが関数名を見ただけで分かるようになる。
次に
var a = 0;
for (var b = 0; b < 2200; b++) {
//中略
}
console.log("count:" + a);
a,bはそれぞれ
- a : 閏年の数
- b : カウンタ兼「年 - 1」
を表すが、for文のカウンタは慣習上、i,j,kを使用する。(因みにこれはFORTRAN由来)
なので、以下のように命名する。
var countLeapYears = 0;
for (var i = 0; i < 2200; i++) {
var year = i + 1;
//中略
}
console.log("count:" + countLeapYears);
これで各変数がどんな意味を持つか明瞭になった。
この時点でコードは以下のようになる。
function printYearsType() {
var countLeapYears = 0;
for (var i = 0; i < 2200; i++) {
var year = i + 1;
if (year % 4 == 0) {
if (year % 100 == 0) {
if (year % 400 == 0) {
console.log(year + ":U");
countLeapYears++;
} else {
console.log(year + ":H");
}
} else {
console.log(year + ":U");
countLeapYears++;
}
} else {
console.log(year + ":H");
}
}
console.log("count:" + countLeapYears);
}
命名する
名前を変更したことで何がしたいのかが伝わりやすくなり、幾分かマシになったがまだ見づらい。
というのも閏年の判定方法が少し複雑で、それをそのままif文に落とし込んでいるからだ。
- 4で割り切れれば閏年(1つ目のif)
- ただし100で割り切れれば平年(2つ目のif)
- ただし400で割り切れれば閏年(3つ目のif)
なので「閏年かどうか」に名前を付けて、それで分岐を行う。
var isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
if (isLeapYear) {
console.log(year + ":U");
countLeapYears++;
} else {
console.log(year + ":H");
}
元の処理と比べるとその違いは一目瞭然だ。
このように条件そのものを変数に出して命名することで、分岐を一つにまとめられることがある。
この時の変数(ここではisLeapYear)を中間変数と呼ぶ。
そして、この閏年判定ロジックそのものに名前を付けること、つまり別関数にすることで再利用性が出てくる。
そうした場合のコードが以下のものだ。
function isLeapYear(year) {
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
}
function printYearsType() {
var countLeapYears = 0;
for (var i = 0; i < 2200; i++) {
var year = i + 1;
if (isLeapYear(year)) {
console.log(year + ":U");
countLeapYears++;
} else {
console.log(year + ":H");
}
}
console.log("count:" + countLeapYears);
}
これで閏年を確認する処理は全て、isLeapYear関数を使えばよい。
もっと丁寧に命名する
function isLeapYear(year) {
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
}
上記のコードは他の部分に比べて、若干横長な印象を受ける。
コードは横が短く、縦に長い方が良い。(厳密には1行内の式が少ない方が良い)
横書きの読物は、基本的に縦長の方が(厳密には横が短い方が)早く読めるからだ。
詳しいことは省略するが、読書において最も時間を要するのは眼球運動で、横長だと(厳密には単語が多いと)眼球運動も多くなるからだ。 (詳細はこちらの文献を参照)
では、横長のものを縦長にするにはどうするか。
やはり命名だ。
各式にそれぞれ命名することで縦長に出来る。
また、コードを理解する上で重要なのは式を理解することではなく、式が持つ意味を理解することだ。
例えばこのコードで言えば「year % 4 == 0」という式そのものはどうでも良く、yearが4の倍数であるかが重要なのだ。
命名することで、式が持つ意味を明示出来る。
故に以下のように修正する。
function isLeapYear(year){
var is4n = year % 4 == 0;
var is100n = year % 100 == 0;
var is400n = year % 400 == 0;
var isLeapYear = is4n && (!is100n || is400n);
return isLeapYear;
}
行数は増えたが、こちらの方が理解しやすい(はず)。
式を分割することで、
- yearの倍数判定
- 倍数の組み合わせによる閏年判定
というプロセスがコードだけで示せた。
また名前を付けてあげることでバグやタイポにも気づきやすくなる。
例えば、
var is4n = year % 5 == 0;
となっていれば少なくとも変数名と式が食い違っていることは明白だ。
式単体であればコードだけで間違いに気づくのは困難だが、式の意味を明示することでコードを見ただけで明らかな間違いに気づくことが出来る。
比べる
//リファクタリング前
function funcA() {
var a = 0;
for (var b = 0; b < 2200; b++) {
if ((b + 1) % 4 == 0) {
if ((b + 1) % 100 == 0) {
if ((b + 1) % 400 == 0) {
console.log((b + 1) + ":U");
a++;
} else {
console.log((b + 1) + ":H");
}
} else {
console.log((b + 1) + ":U");
a++;
}
} else {
console.log((b + 1) + ":H");
}
}
console.log("count:" + a);
}
//リファクタリング後
function isLeapYear(year){
var is4n = year % 4 == 0;
var is100n = year % 100 == 0;
var is400n = year % 400 == 0;
var isLeapYear = is4n && (!is100n || is400n);
return isLeapYear;
}
function printYearsType() {
var countLeapYears = 0;
for (var i = 0; i < 2200; i++) {
var year = i + 1;
if (isLeapYear(year)) {
console.log(year + ":U");
countLeapYears++;
} else {
console.log(year + ":H");
}
}
console.log("count:" + countLeapYears);
}
リファクタリングで行数は増えたが、先に挙げた問題点が解消されている。
- コードの内容が変数名、関数名を見ることで大体の予想がつく(可読性がある)
- 1つの修正内容に対して修正箇所は1箇所で済む(保守性がある)
- ロジックが外だしになって他で流用しやすい(再利用性がある)
命名のまとめ
長々とリファクタリングしてきたが実のところ、やったことはたった3つだ。
- 関数、変数に適切な命名をする
- 関数を機能単位で分割する
- 中間変数を作成する
リファクタリングしたコードは極端な例であるが、大抵のコードは上記の3つ意識するだけで品質は確実に上がる。
おまけ
保守性なども突き詰めてjsで本気で書くとこんな感じ。
(本来は処理に関するコメントも書くべきなのだが、ここではあえて書かない)
長いので折り畳み
//isLeapYearは定義済みとする
const generateYears = (start, end, fillter = (year) => true) => {
const length = end - start + 1;
const years = [...Array(length).keys()]
.map(i => i + start)
.filter(fillter);
return years;
}
//手続き型
const printYearsTypeSeriously = () => {
const yearRange = {
start: 1,
end: 2200,
};
const yearType = {
leap: "U",
common: "H",
}
const years = generateYears(yearRange.start, yearRange.end);
const classifiedYears = years
.map(year => {
const type = isLeapYear(year)
? yearType.leap
: yearType.common;
return { year: year, type: type };
});
for (const y of classifiedYears) {
console.log(`${y.year}:${y.type}`);
}
const isLeap = y => y.type === yearType.leap;
const countLeapYears = classifiedYears
.filter(isLeap)
.length;
console.log(`count:${countLeapYears}`);
}
//オブジェクト指向プログラミング
class ClassifiedYear {
static yearType = {
leap: "U",
common: "H",
}
constructor(year) {
this.year = year;
this.type = isLeapYear(year)
? ClassifiedYear.yearType.leap
: ClassifiedYear.yearType.common;
}
isLeap = () => this.type === ClassifiedYear.yearType.leap;
toString = () => `${this.year}:${this.type}`;
}
const printYearsTypeOop = () => {
const yearRange = {
start: 1,
end: 2200,
};
const years = generateYears(yearRange.start, yearRange.end);
const classifiedYears = years
.map(y => new ClassifiedYear(y));
for (const y of classifiedYears) {
console.log(y.toString());
}
const countLeapYears = classifiedYears
.filter(y => y.isLeap())
.length;
console.log(`count:${countLeapYears}`);
}
命名のコツ
命名しようにも、方法を知らないとやりたくても出来ない。
なのである程度の指標を示す。
アンチパターン
アンチパターン、つまり「原則的によくない命名の仕方」を紹介する。
あくまで原則なので、使わなければならない場面(コーディング規約等)は出てくるのでそこは柔軟に対応する。
日本語/ローマ字
基本的には命名は英語で行い、コード全体で名称に用いる言語を統一すること 。
固有名詞等はローマ字で構わないが、
function touroku(){
}
などは絶対にしないこと。
ほとんどのプログラマは英語で書かれているのが当然と思っているからだ。
まれに英語だと思っていたら和製英語やドイツ語/ポルトガル語だったのような例はあるがそこまで気にしなくて良い。
jsやC#など純粋に日本語を命名に使用できる言語もあるが、これらもやはり英語で命名すること。(他は全て英語なので)
余談だが「なでしこ」等の日本語プログラミング言語を使用する場合は日本語で書くこと。(漢文で書く言語もあったような)
とはいえ実務でそういった類の言語は使うことは考えにくいが。
ハンガリアン記法
ハンガリアン記法とは大雑把に言うと「変数/関数に接頭/接尾辞を付けて型を示す」ことである。(厳密に言うとシステムハンガリアン)
詳しくはWikipediaを参照
例は省略するが、色々制約のあった時代のコーディングスタイル。
名前から型情報が分かると言えば聞こえはいいが、現代ではほぼIDEないしそれに準ずる高機能エディタで開発するのが当たり前であり、定義等は簡単に確認出来る。
むしろ型を修正したときに修正量が増えるので邪魔だ。
IDEを使えば修正作業自体は一瞬だが、バージョン管理時には無駄な差分が出てくる。
それを一々確認するのも手間だ。
そもそも品質の高いコードを書こうすると、大抵の場合は匿名型など値オブジェクト(値のみを持つ構造体ないしクラス、あるいはそのインスタンスのこと) を多用することになるので「CXx/tagXxx」が量産され、しかしそれだけでは名前から型を特定するのは事実上不可能。
よってハンガリアン記法は避けた方がベター。
(特殊な事情、例えばCで組み込みなど使用すべき理由がある場合を除く)
xxFlag
処理がある程度複雑になるとフラグ変数が必要になる。
そこで安易に「xxFlag」と付けるのはどのような場面であっても避けるべき。
例えば、
bool deleteFlag;
という変数があったとして、deleteされたかどうかのフラグなのは分かるが、実際deleteされたときはtrueなのかfalseなのか分からない 。
定義時にコメント等で決めてあれば確認すればいいだけだが、そんな手間をこのコードを見る者全員に課すのはどうだろう。
人によってこのtrue/false感性は違うし、何なら気分で反転してもおかしくなく、プロダクト全体で統一を取れない可能性が出てくる。
なので、こうする。
bool isDeleted;
これでtrueであれば削除されていることが分かる。
後述するが、基本的にis/has/can/validなどtrueなときどんな状態か、ということを意識して命名する。
そもそもフラグという単語が悪いような気もする。
どのような状態(ステータス)を表す変数かという見方をしてほしい。
checkXx
これも上記と同じような理由だ。
この場合は、
bool isValidXx(xx xx){};
のようにするとtrueでOK、falseでNGというのが一発で分かる。
なお、後述するが、存在/包含/等価などの確認などはそれにあった動詞を用いること。
場面ごとの命名例
場面ごとに簡単な例を出す。
必ずしも正解とは限らないので参考程度にすること。(保険)
状態を表す
class Example{
//単純なenum,値オブジェクトを返す場合は型名そのままでも良い
//プロパティ構文がある言語を使用する場合はそちらを使用する
ExampleState exampleState();
//dataを含んでいるか(主にコレクション関係で使用)
bool includes(Data data){}
bool contains(Data data){}
//dataを持っているか(dataが単一の場合)
bool hasData(){}
//インスタンスレベルで有効な状態か
bool isValid(){}
//dataが有効な状態か
bool isValidData(){}
//update可能な状態か
bool canUpdate(){}
}
操作系
class Example{
//intへ変換
int parseInt();
//parse試行(成否を返し、参照でパース結果を返す)
bool tryParseInt(int &value);
//型が緩い言語の場合は複数値返却や値オブジェクトを使用するのも可
//dataを追加(主にコレクション関係で使用)
void addData(Data data);
void appendData(Data data);
//dataを除去(同上)
//deleteは強い単語なのでインスタンスごと完全削除するとき以外には使用しない
void removeData(Data data);
//コレクションの中身を全てremove
void clear();
//開始
void start(){}
//停止(正常)
void stop(){}
//異常/強制終了
void abort();
void terminate();
//初期設定
void init(){}
void initialize(){}
//インスタンス破棄時(デストラクタ)
void abandon(){}
void dispose(){}
//イベント
void onChanged(){}
void onClicked(){}
//(無から)data(比較的重いインスタンス)を作成する
Data createData(){}
//(既存のデータから)data(値オブジェクトなど軽いもの)を作成する
Data generateData(){}
//jsonからExampleを作成する
static Example fromJosn(string json){}
//インスタンス内容をjsonで出力
string toJson(){}
//(ファイル等へ)保存する
void save(){}
//(ファイル等から)読み込む
void load(){}
//(別の場所へ)保存する
void store(){}
//(別の場所から、あるいは別のデータから)復元する
void restore(){}
}
単語を調べるときは
同じような意味の単語は「どっちでもいいや」で適当に決めない。
極端な例を出すと、「『笑う』と『嗤う』、どっちでもいいや」と言っているのと同じである。
単語を調べて複数候補が出てきた場合は「xxx yyy 違い」などで調べる。
例えばsetting,configであれば、調べると概ね以下のような違いがある。
- setting
〔機器の〕設定値
比較的軽いもの(ユーザー側で簡単に設定可能なレベル) - configuration
〔コンピューターの〕機器構成、設定
構成など重いもの(アプリ等で再起動を要するレベル)
1行目の翻訳は英次郎より
無闇に単語を省略しない
単語を無闇に省略すると意味が正しく伝わらない可能性がある。
例えば以下の通りである。
- passwrod => pwd
- calibration => clb
- right,left => r,l
pwd、clbは他に当てはまる単語がある。
この程度であれば文脈で判断出来るが、原型が伝わらない可能性もある。
なお、コメントや文脈から明らかで公式でその略称が使用されている場合は積極的に使用する。
また、r,lなど区別キーワードが1文字だとぱっと見では判別が付かない場合がある。
- mainPagePainRLines
- mainPagePainLLines
現代ではソースコードの容量は気にする必要はほぼなく、インテリセンスが入力の補助をしてくるので多少長い名前でも支障をきたすことはまずない。
名前が長すぎる場合は詰め込みすぎの可能性があるので、機能や意味の分割を検討する。
英単語は接頭/接尾辞で長くなりやすく、面倒になることはままある
ただし、スコープの短いローカル変数(一時変数など)は省略しても問題ない。(特にラムダ式のワンライナーなど)
function exmaple(){
for(let v in values){
console.log(v);
}
}
肯定系で使用する
変数や関数名には肯定系を使用する。
理由は以下の通り。
- 否定形を否定すると二重否定になり、ややこしい
- 否定形と肯定系が混ざると、混乱の元になる
否定形は!をつける、とすると直観に反さない(二重否定の例)
//bad
bool isInvalid = !isValid();
if(!isInvalid){
}
if(isInvalid){
}
//ok
bool isValid = isValid();
if(isValid){
}
if(!isValid){
}
余談
言語(ここでは自然言語の意)にもよるが、二重否定は常に単純な肯定を示すとは限らない。
肯定を強調あるいは緩やかにする場合(緩叙法)もあれば、否定形の統一の場合もある(否定呼応)
- 緩やかな肯定
- 「興味なくはないね」
- 肯定の強調
- 「涙を流さない者はいなかった」
- 否定呼応
- 「I don't know nothing」(私はなにも知らない)
とはいえこれは古い表現で、現代英語では普通に「知らないことはない」という意味である(らしい。詳しいことはネイティブの方に聞いてほしい)
- 「I don't know nothing」(私はなにも知らない)
- ひと昔前のギャル(死語)
- 「やばくなくなくない?」
(これは結局どっちなのだろうか)
- 「やばくなくなくない?」
似た話に「ヘンペルのカラス」というのがある。
雑に要約すると「待遇論法(論理学における二重否定)は微妙に腑に落ちない」という話である。
とにもかくにも、二重否定は単純な肯定ではない場合があってややこしいので使用を避けるべきである。
コメント
コメントは最小限に抑える。
過ぎたるは猶及ばざるが如し
前提として、コメントは保守性を下げるノイズだ。
何故かと言えば、処理を変更した際にコメントも変更しなくてはならない。
忘れた場合、コメントと処理が食い違って「正しいのはどっちだよ」状態になる。
(結果仕様書の確認へ戻ることになる)
つまり、管理物が増えることになり、しかも気付く方法はコードを読む以外にない。
しかし、適切なコメントは可読性を飛躍的に上げる。
不必要なコメント
まれに以下のようなカスみたいなコメントがある。
function func(){
var a = 0;//トータルを示す変数
for(var i=0;i<5;i++){
//aにiを足しこむ
a+=i;
}
}
上記のコードのポイントは以下の通り。
- コメントで説明するより見て分かる変数名を付ける
- 誰が見ても分かる処理にコメントは不要
- どうしても名前では説明しきれない場合や、例外的な処理に理由を含めてコメントを添える
修飾
/*****************************
* func関数 *
******************************/
function func(){
//処理
}
のような修飾されたコメントがあるが、悪以外のなにものでもないので書かないこと。
(しかも上記の場合全く情報量が増えてない。いやほんとにこんなコードがあるんですって悲しいことに)
またアウトラインで折りたたまれる可能性があり、目立たせようとして逆に非表示になる可能性がある。
ただし、コーディング規約で定められている場合は血涙を流しながら記述すること。
余談(私怨)
上記のようなコメントを書く者は口をそろえてこう言う。
「重要なコメントだから目立つようにしたんだ」
突っ込みポイントは以下の通り。
- 逆説的に、君は重要ではコメントを冗談か何かで書いてるのか?
- 重要ではないコメントなどない
- というか君もしかして修飾されていないコメントは読んでないってこと?
時間もデータサイズも行数も表示演算も何もかもあらゆるリソースを無駄に浪費しているだけなので本当にやめてください。
酷いところでは*や の数を指定しているコーディング規約もある。
一体なんの意味があるんですか本当に。
改変履歴
以下のようなコード履歴がソース上に残っている場合がある。
function func(){
// 20xx Y田 mod start
// console.log("旧処理");
console.log("新処理");
// 20xx Y田 mod end
}
現代においては、ただの嫌がらせでしかないのでやらないこと。
くどいようだが、コーディング規約に定められている場合は恨みを込めながら記述してください。
同様に、
function func(){
// デバッグ処理
// console.log("debug");
console.log("main");
}
デバッグ処理や旧処理もコメントアウトではなくざっくり消すこと。
コード履歴の管理はツールに任せるべき。
コメントには意図を書く
コメントは処理の説明を書く
とよく言われるが、これは微妙に的を外している。
コメントには何を目的とした処理なのか、つまり処理の意図を書くべきだ。
具体的な書くべき意図の例は以下の通り。
- 例外的な分岐
- 理由も書く
- 例えば奇数偶数判定での0の扱いなど
(数学的には偶数だがシステム上そうしたくない場合もある) - やたら複雑な処理
- 関数分けをするのは前提だが、どうしても分解しきれない箇所
- 専門的な処理(高度な数学を用いる場合など)
- ただし、浅学者が触る可能性がない場合は不要
専門的であったり、ぱっと見では意図が伝わりにくいコードにはコメントを付ける。
ドキュメントコメント(jsdoc等)
理想ではあるが、最終的なコードには全てドキュメントコメントを付ける。
厳密にはドキュメントコメントはC#の用語で、jsdoc/javadocなど言語毎に呼び名やツール等あるがとりあえずdoxygenに対応しているコメントスタイルと定義しておく。
jsでは以下のこうなもの。
/**
* xとyの合計を返す
* @param {number} x 数値1
* @param {number} y 数値2
* @returns {number} x + y
*/
export const sumXY = (x,y) => {
const result = x + y;
return result;
}
ドキュメントコメントを付与することで各IDEは関数や引数の情報をインテリセンスやポップアップで表示してくれる。
修正/利用する側はいちいちコードを見ずに済むので分かりやすい。
また、細かい仕様書を求められた場合にdoxygen等のツールにかけるだけで済む。
ただし、実装の最後の方で付けること。
途中で付けると、最悪処理とコメントが食い違う可能性があるため。
また、時間をあけて処理を確認することで机上デバッグにもなる。
その他のコードの品質を上げるテクニック
値オブジェクト(バリューオブジェクト)
既に何度か出てきているが、いくつかの値をまとめて扱う場合は、値をまとめたオブジェクトを使用する。
これを値オブジェクトと呼ぶ。
(具体的な実装方法は言語によってクラス、構造体、オブジェクト、匿名型など様々)
値オブジェクトの特徴は以下の通り。
- プリミティブ型または値オブジェクトのみを持つ
- 副作用のある関数を持たない
//横に並ぶと見づらい
void manyParameterFunction(int param1,double param2,std::string param3,bool param4){
}
struct ValueObject{
int param1;
double param2;
std::string param3;
bool param4;
//関数の実装もOK
//ただし副作用のないものに限る
int doubleParam1(){
return this.param1 * 2;
}
}
//単一なので引数がややこしくならない
void valueObjectFunction(ValueObject param){
}
//流用も出来る
//引数に修正が必要な場合は元構造体の修正のみで良い
void otherValueObjectFunction(ValueObject param){
}
class ExampleClass{
//クラスのパラメータにも使える
ValueObject valueObject;
}
文字コード
特段理由がない限りファイルはUTF-8(BOMなし)にしておく。
(ソースコード/入出力ファイルなどほぼ全て)
また、ファイルの入出力時には文字コードを省略せずに明示的に設定すること。
その他知っておいた方がいい言葉/概念
詳しくは解説しないが知っておいて損はないコードの指標シリーズ。
- モジュール結合度
- モジュール強度
- サイクロマティック(循環的)複雑度
- 凝集度
バグの少ないコード
値/参照/参照値
プログラミングにおいて変数と一口に言ってもその実態は大きく分けて3種類ある。
これは言語ごとに対象や扱いやまるで違うので把握しておくこと。
詳しく書くと結構な量になるのでこの記事を参照。
この違いを理解せずにトリッキーなコードを書くとバグの原因になる。
(トリッキーなコードを書くな、というのが正論だが)
ディープコピー/シャローコピー
参照値の話としてインスタンスのコピーには2種類ある。
シャロー(浅い)とディープ(深い)だ。
と言っても違いは簡単だ。
シャローコピーは参照値のコピー、ディープコピーはオブジェクトのコピーだ。
この二つを理解していないとバグを引き起こす可能性がある(特に配列のコピーを行う場合)
なお大抵の場合はシャローコピーの場合はcopy、ディープコピーの場合はcloneという関数名が付けられている。(絶対ではないので初めて使用する際は確認すること)
副作用
プログラムには副作用という概念がある。
乱暴に言うと「評価することで評価値以外の値が変化すること」である。
出力引数も副作用と見做すこともあるが、事実上の返却値なのでここでは副作用とはしない。
var number = 5;
function funcA(){
//副作用無し
return 1;
}
function funcB(){
//副作用無し
//numberを参照しているが変更はしていない
const n = number + 1;
return n;
}
function funcC(){
//副作用あり
//numberの変更がある
number += 1;
return number;
}
class Example{
constructor(height, width) {
//副作用なし
//副作用のある関数を呼んでいるが、副作用はない
//新しく生成されるExampleインスタンスが評価値であるため
this.setSize(height, width);
}
diagonal(){
//副作用無し
return Math.sqrt(this.height ** 2 + this.width ** 2);
}
setSize(height, width){
//副作用あり
//メンバ変数の変更があるため
this.height = height;
this.width = width;
}
}
余談
C++にはconst関数やconst引数がある。
要するに副作用がないということの証明。
(const変数もあるが、そちらは定数という意味)
詳しくは以下を参照。
https://docs.microsoft.com/ja-jp/cpp/cpp/const-cpp?view=msvc-160
外部への副作用を抑える
副作用は決して悪者ではない。
むしろ抽象化においてはなくてはならない。
ただし、影響範囲が狭いに越したことはないので最小限に抑える。
目安として、
- 基本的にはインスタンス内部まで
- クラス外部にまで波及した場合は一度再考する
- モジュール外に及んだらアウト
くらいを推奨。
(変更を意図した引数への副作用はノーカウント)
引数の変数を操作しない(操作する場合は明示する)
原則として引数の変数は操作しない。
処理が複雑になった場合、追うのが困難なデバッグ殺しになる。
出力引数として使用する場合は、その旨を明確にしておくこと。
void function(int i){
//何らかの処理
i = 3;//禁忌
//その後の処理
}
bool tryParseInt(string str,int &out){
//この場合はOK
//関数名及び引数名からoutを操作することが明確
//処理は中略
}
バグを出したときには
どんなに気を付けていてもバグは出るときには出る。というか一切出さないのはほぼ不可能。
そのために動作確認、レビュー、テストがある。
どんな段階にせよ、バグを見つけた際にはやるべきことがある。
バグの原因を理解する
バグを出した際にはその原因を特定し、修正内容の根拠を確立する。(両方とも他人に説明出来るレベルが望ましい)
タイポや見落とした副作用であれば特に問題はないと思う。
が、仕様の理解不足や勘違いだった場合は結構な労力になる。(特に経験が浅い場合)
決して「何となく変えたらそれっぽく動いた」ですまさないこと。
その場合他のバグが潜在している可能性が非常に高い。
バグを修正して動作確認をする
原因を理解したら実際に修正を行い、どんなに些細なバグやタイポでも必ず動作確認は行う。
頭の中では完璧に修正されていても、実際に修正出来ているとは限らない。
特に設定系変数やユーティリティ関数など参照範囲が広い場所を修正した場合は念入りに確認する。
軽い修正のつもりが、思わぬところに影響が出て意図しない動作をするのはよくあることだ。
可能であればメモをとる
タイポ程度のものだったら問題ないが、少し複雑で調べるのに時間を要した場合はメモをとる。
現象と参考サイトのURLのみ程度でも構わないのでメモを残しておくと、時間を空けて同じ問題が起きたときにメモを見ればすぐに解決するので作業効率が上がる。
一度やっただけで覚えられるのは一握りの優秀な人間のみだ。
あとがき
ここはこの記事を書くときに新規で起こしてます。
言いたいことも冗談も冒頭で言い切ったので最後に一言だけ。
こんな記事よかリーダブルコードの方がよっぽどためになるのでそっち読んでください。