私は家では趣味で、会社では仕事でシステムを作っています。
自分や他人が作ったプログラムに機能を追加したり、バグ対応したりする事は今までも、そしてこれからもあります。そんな中で私が編み出したメンテナビリティをあげる方法をご紹介します。
ドキュメントは作らない
会社ではSEさんがドキュメントを作ります。
それがSEさんやSIerさんの仕事なのですが、実際はドキュメントは作らないことが多いです。お客さんに説明するための資料を一時的に作って、時間が立つと消えてなくなる事が多いです。
また、ドキュメントを修正するのが面倒くさいという理由から、プログラムが出来上がってからドキュメントを作る事もあります。(意味ねぇじゃん)
ドキュメントを作ってもプログラムと同じ状態に保つことは大変面倒くさく、誰かが修正を忘れてた…というのを一度でも見つけるとドキュメントとプログラムが同じなのか疑うようになります。
…ではどうするか?
ドキュメントがなくてもわかるソースを書きましょう!
コメントの書き方
ソースを見れば何をやってるのかわかるようなコーディングをするので、基本的にはコメントは不要なのですが、ソースが何をやってるのかを1行ずつ見てからわかるというのは時間がかかるので、概要を随時挿入します。
ソースの先頭
ソースの先頭は概要を説明するコメントを入れましょう。
以下はSampleというクラスがあるソースです。
/* ###########################################################
*
* Sample.java - Sample
*
* [[ summary ]]
* 概要を書く
*
* [[ class tree ]]
* 呼び出し階層を書く
* AAAA
* +- Sample
*
* ######################################################### */
大分類
クラス内のプロパティやメソッドを大分類のコメントで分けます。
/* ###########################################################
*
* constructor
*
* ######################################################### */
public Sample() {
}
/* ###########################################################
*
* file control
*
* ######################################################### */
/* ************************************************************
* load file
* ********************************************************* */
public boolean loadFile() {
}
このように、同じようなメソッドは大分類内に集めます。
コンストラクタ関係はconstructorの大分類の下に並べファイル制御のメソッドはfile controlの下に並べられます。こうするとどこに何があるのかが特別なツールがなくても上から見ていくとわかります。
中・小分類
以下は中・小分類のコメントです。
線が細く小さくなってて、中・小分類っぽいのが目で見てわかります。
/* ###########################################################
*
* file control
*
* ######################################################### */
/* ************************************************************
* load file
* ********************************************************* */
public boolean loadFile() {
StreamReader objReader = new StreamReader("sample.txt");
while (!objReader.EOF()) {
String strLine = objReader.readLine();
this.loadFile_edit(strLine);
}
}
/* ============================================================
* load file - edit
* ========================================================= */
private void loadFile_edit(String strLine) {
if (strLine.contains("FILE-VERSION")) {
this.loadFile_edit_fileVersion(strLine);
}
}
/* ------------------------------------------------------------
* load file - edit - file version
* --------------------------------------------------------- */
private void loadFile_edit_fileVersion(String strLine) {
this.strFileVersion = strLine.substring(12, 10);
}
まずloadFile()の部分は他のクラスに公開しているメソッドです。この部分のコメントは/* ***** /で中分類枠で囲まれています。こうすることでこの枠が登場する部分は公開メソッドだとプログラマに伝えています。
次に/ ===== /で囲まれた部分。
ここは公開メソッドから呼び出される処理です。
処理は機能ごとに小分けにすると余計な情報を頭に入れなくていいので、非常にわかりやすいソースになりますが、さらに小分類枠で囲って中分類から呼び出されていることをプログラマに知らせています。
最後の/ ----- */は小分類から呼び出されるミニ分類です。
量が少ない場合は以下のように書いてもいいかもしれません。
/* ============================================================
* load file - edit
* ========================================================= */
private void loadFile_edit(String strLine) {
// file version
if (strLine.contains("FILE-VERSION")) {
this.strFileVersion = strLine.substring(12, 10);
}
}
量が多いか少ないかは普通の人の頭に搭載されているCPUが一度に処理できる数(2個〜3個)を基準に考えましょう。
世の中には頭の中に広いホワイトボードを持っている人も居ますが、頭が悪い人を基準にしなければ、色々な人が触る事になる業務のソースコードでは破綻します。また、業務でなく趣味のソースでも、自分が過去は頭が良かったとしても10年後には脳の処理性能が下がって一度に2個の物事しか処理出来なくなる可能性もあります。
ソースコードの可読性は、今ではなく、10〜20年後を見越して
処理を細かく分ける
可読性はメンテナビリティに大きく影響します。
例えば先程のコメントの説明で使用したソースが以下のようになっていたら、初めてこのソースを見る人にとって読みやすいでしょうか?
/* ###########################################################
*
* file control
*
* ######################################################### */
/* ************************************************************
* load file
* ********************************************************* */
public boolean loadFile() {
StreamReader objReader = new StreamReader("sample.txt");
while (!objReader.EOF()) {
String strLine = objReader.readLine();
if (strLine.contains("FILE-VERSION")) {
this.strFileVersion = strLine.substring(12, 10);
}
}
}
え?
読みやすいじゃん?
そう思われたかもしれません…その時点であなたは可読性が悪いプログラムをばらまいている人です。LANコードで首を吊ることをオススメします。
先ほど、私は「量が多いか少ないかは普通の人の頭に搭載されているCPUが一度に処理できる数(2個〜3個)を基準に考えましょう」と書きました。
上のソースは2個以下ですから分割するに値しません…が、何故、LANコードで首を吊らなければならないのでしょうか?
今が良ければいいわけじゃない
例えば、以下のようだったら…分けるべきでしょうか?
/* ###########################################################
*
* file control
*
* ######################################################### */
/* ************************************************************
* load file
* ********************************************************* */
public boolean loadFile() {
StreamReader objReader = new StreamReader("sample.txt");
while (!objReader.EOF()) {
String strLine = objReader.readLine();
if (strLine.contains("FILE-VERSION")) {
this.strFileVersion = strLine.substring(12, 10);
}
if (strLine.contains("FILE-OLDVERSION")) {
this.strFileVersionOld = strLine.substring(12, 10);
}
if (strLine.contains("IMAGE-ID")) {
this.strImageID = strLine.substring(1, 2);
}
}
}
おや…まだ分けるべきではないと…?
…では、さすがに以下の状態だと分けるべきだと考えるでしょう。
/* ###########################################################
*
* file control
*
* ######################################################### */
/* ************************************************************
* load file
* ********************************************************* */
public boolean loadFile() {
StreamReader objReader = new StreamReader("sample.txt");
while (!objReader.EOF()) {
String strLine = objReader.readLine();
if (strLine.contains("FILE-VERSION")) {
this.strFileVersion = strLine.substring(12, 10);
}
if (strLine.contains("FILE-OLDVERSION")) {
if (strLine.substring(10, 2).equals("test")) {
if (strLine.substring(15, 2).equals("1")) {
this.strFieldVersion = 1;
} else {
this.strFieldVersion = 2;
}
} else {
this.strFileVersionOld =
strLine.substring(12, 10);
}
}
/* この間に1000行ほどの似たような処理 */
if (strLine.contains("IMAGE-ID")) {
this.strImageID = strLine.substring(1, 2);
}
}
}
階層がどんどん深くなっていますね。
しかも1000行にも及ぶ似たような処理が間にあります。
こうなってくると、自分が今、何のメソッドを読んでいるのかすらスクロールしないとわからなくなります。手が滑って上にスクロールし過ぎて別のメソッド名を見てしまい、別のメソッドの処理として頭に展開してしまうかもしれません。
COBOLなどの化石言語でしぶとく生き残っているものは、メンテナンスを行う人がプログラムに精通した人ではないことがよくあります。
分けるのが面倒くさいのか上記のような「if文お化け」が様々な場所に点在し、メンテナビリティを悪化させています。
さて。
お気づきの方もいらっしゃると思いますが、可読性とは「過去・現在・未来」に対して働かなければならないものです。
今が良ければいいのではありません。
私は「量が多いか少ないかは普通の人の頭に搭載されているCPUが一度に処理できる数(2個〜3個)を基準に考えましょう」と書きましたが、続いて、「ソースコードの可読性は、今ではなく、10〜20年後を見越して」と太字で書きました。
今さえいいのなら、すぐに結婚をしてください。
結婚は慎重ですよね?
何故なら10年後、20年後が不安だからです。
ソースコードだって同じです。
今が良くても10年後、20年後も生き続ける…生き続け、迷惑をかけまくり、最初に作った奴が呪いにかかる…そんな事もあるかもしれません。呪われて死ぬのは苦しいですよ。(΄◞ิ౪◟ิ‵)
/* ###########################################################
*
* file control
*
* ######################################################### */
/* ************************************************************
* load file
* ********************************************************* */
public boolean loadFile() {
StreamReader objReader = new StreamReader("sample.txt");
while (!objReader.EOF()) {
String strLine = objReader.readLine();
this.loadFile_edit(strLine);
}
}
/* ============================================================
* load file - edit
* ========================================================= */
private void loadFile_edit(String strLine) {
// invest file-version
if (strLine.contains("FILE-VERSION")) {
this.loadFile_edit_fileVersion(strLine);
}
// invest file-old-version
if (strLine.contains("FILE-OLDVERSION")) {
this.loadFile_edit_fileOldVersion(strLine);
}
/* 中略 */
// invest image-id
if (strLine.contains("IMAGE-ID")) {
this.loadFile_edit_imageID(strLine);
}
}
/* ------------------------------------------------------------
* load file - edit - file version
* --------------------------------------------------------- */
private void loadFile_edit_fileVersion(String strLine) {
this.strFileVersion = strLine.substring(12, 10);
}
/* ------------------------------------------------------------
* load file - edit - file old version
* --------------------------------------------------------- */
private void loadFile_edit_fileOldVersion(String strLine) {
if (strLine.substring(10, 2).equals("test")) {
this.loadFile_edit_fileOldVersion_change(strLine);
} else {
this.strFileVersionOld = strLine.substring(12, 10);
}
}
/* 以下略 */
かなりスッキリしました。
便器に残ったクソとティッシュが一度の洗浄で綺麗に流され、飲んでも大丈夫なぐらいに綺麗な便器になった時のようですね。
最初に大分類・中分類・小分類の説明にあったような書き方をしておけば、わざわざそれをブチ壊してまでコードを追加する人は居ないでしょう(仮に居たとしたら背後から静かに忍び寄り、LANコードで首を締めてkill -9しましょう)
テストが面倒臭くなるから上のソースのように、決められた場所にコードを追加して同じような書き方で増えていきます。
もちろん、自分一人でシステムを作っている時も同様です。
将来を見越すコツとは?
「未来を予想するなんてプログラマは預言者じゃねーんだから無理に決まってんだろ、アホかテメェは!!Oracleのこと言ってんのか?俺はMySQL派だクソが」
そう思われてるかもしれません。
なので、コツを伝授しましょう。
ループや分岐と処理を分ける
ループ(while/for)、分岐(if/switch)と処理を分けましょう。
/* ###########################################################
*
* file control
*
* ######################################################### */
/* ************************************************************
* load file
* ********************************************************* */
public boolean loadFile() {
StreamReader objReader = new StreamReader("sample.txt");
while (!objReader.EOF()) {
/* ここの処理が増えていく可能性あり */
}
}
whileの中の部分が「処理」です。
増えていく可能性が高いです。
メソッドを追加して、それを呼ぶようにしましょう。
/* ###########################################################
*
* file control
*
* ######################################################### */
/* ************************************************************
* load file
* ********************************************************* */
public boolean loadFile() {
StreamReader objReader = new StreamReader("sample.txt");
while (!objReader.EOF()) {
String strLine = objReader.readLine();
this.loadFile_edit(strLine);
}
}
/* ============================================================
* load file - edit
* ========================================================= */
private void loadFile_edit(String strLine) {
// invest file-version
if (strLine.contains("FILE-VERSION")) {
/* ここは処理なので以後のメンテナンスで増える可能性が高い */
}
}
FILE-VERSIONが含まれていたら…の処理も増える可能性が高いですね。
メソッド化しましょう。
このように、将来的に処理が追加されるであろう場所をメソッド化して、極力、階層を深くならないようにします。if文お化け(別名:階層お化け)が出始めそうな箇所も同じ様に変えていきます。
タブが2、3個あるような場合はもう危険信号です。
また、開発ツールではこのように処理分けされたのをアウトラインで確認することもできるようになっているのが殆どなので、アウトラインを見ただけでソースを見なくても何をしているのかがわかるようになります。
本日はここまで
いかがでしたか。
メンテナビリティの悪化はテストがしづらくなることだけではなく、トラブルも引き起こしますし、例えば…化石言語であるCOBOLなどで作られたシステムを刷新し多言語に移行する場合の障害にもなっています。
そして、そのどれもがコストが膨大にかかります。
膨大なコストがかかるということは経営者側・客がそれを払えなくなることがあり、結果的にシステム開発を請け負った会社が払いますが、払いたくないので現場で作業する人々(プログラマ)がその皺寄せを受けます。
残業、休日出勤、プロジェクト炎上…デスマーチ…。
そして支払われない残業代。
まさかメンテナビリティがブラック企業化にまで繋がっているとは知りもしなかったと思います。それだけ重要な事なんです。
今回はこれにて終了。
また今度、時間がある時に付け足そうと思います。