メンバページ: Go
汎用メトリクスツール「いちゃもん」の作り方 by Java
General-Purpose Metrics Tool "Fuss" making by Java
このページでは、C/C++/C#/Java に対応する汎用的なメトリクスツールを作成していきます。
- A 君: 汎用メトリクスツールというものと「いちゃもん」という名前がしっくり来ませんが?
- G 君: ふぉふぉふぉ、それはじゃ、メトリクスツールの本質を表すと「いちゃもん」なんじゃ。
- B 君: へ?メトリクスつったら、あんた、計測するつうことでしょ。
- G 君: 韻律学じゃ、これに決まっとるじゃろ。プログラムにおける韻じゃ。
- A 君: まぁ、まぁ。進んでいけばわかるんでしょう、きっと。
- G 君: 計測できるものは制御できるんじゃ、少なくともいちゃもんを付けられないようには
- B 君: いちゃもんはんは元気やねぇ
| 1日目 | メトリクスとは |
| 2日目 | クラス設計 最初の始まり |
| 3日目 | ファイルフィルタとワイルドカード |
| 4日目 | 字句解析部のインポート |
| 5日目 | シンボルテーブルなどの部品クラスの作成 |
| 6日目 | プログラム解析・管理部の作成 |
| 7日目 | メトリクスデータのファイル出力 |
| 8日目 | まとめと結果のサンプル |
| 参照 | Ichamon のその後はここにあります |
1日目 --- メトリクスとは
今日はメトリクスについて記述します。ここでメトリクスとは、プログラムを何らかの計測基準で計量(メジャー)することを指します。
例えば、「行数」でプログラムを計測する方法があります。システム全体は何行であるとか、1ファイル何行であるとか、1クラス平均何行であるかなどを計測します。
この計測の基準となるもの、つまり物差しに各種のものが提案されています。以下に代表的なメトリクスを紹介します。
- LOC (Line Of Codes、行数、ステップ数)
プログラムの行数。正確にはプログラム中の改行の個数に EOF のみでない EOF がある行を含む個数である。
この LOC には、空白行やコメント行、宣言部などの非実行文も含まれる。 - 命令行数
プログラムの命令が含まれる行の個数。コメント文や空行を除いたもので、宣言文やマクロ指定行なども命令行数に入れることが多い。 - ステートメント数
プログラム言語の構文規則で、ステートメント(文)として認識されるプログラム断片の個数。
例えば、for (int i = 0; i < size; i ++) は、(1) i = 0, (2) i < size, (3) i ++ の 3 ステートメントである。 - メッセージ送信数、関数コール数
+, *, < などの演算子による演算を含まない、メッセージ送信の個数である。
例えば、"abc" + "def" は 0 であるが、str.append("def") は 1 個である。(これは矛盾の例となっている) - 演算子とオペランドの個数
ここにはメッセージ送信も含まれる。
1 + 2 は1個の演算子に2個のオペランド、if (a == b) x = a; else x = b; は 4 個の演算子(if-else, ==, =x2)、6 個のオペランド(a, b, x, a, x, b)である。 - ユーザ定義関数数
ユーザ定義パッケージ、クラス、メソッド、関数、ファイルなどの個数やその段数 - 使用システム関数数
使用しているシステムのパッケージ、クラス、メソッド、関数、ファイルなどの個数やその段数 - 変数数
変数(グローバル変数、ローカル変数、クラス内グローバル変数、・・・)の個数やサイズの合計値 - サイクロマティック数
プログラムの制御構造をグラフとしてみたときの独立閉路数(いわゆるサイクロマティック数)。 - それぞれの平均値と分散
上記が代表的なもので、この他のメトリクスも存在します。
- A 君: 一杯ありますね。どれを見たらいいんですか。
- G 君: そこじゃよ。いちゃもんの目のつけどころは。
- G 君: 兎に角、数値を一杯出して読者に複雑さをアピールすることじゃ。
- A 君: それにしてもプログラムの情報量よりも、メトリクスの情報量が多いんじゃない?
- G 君: 例えばじゃ、変数名 i の1文字しか与えられなくてもじゃ、情報を水増しするもんじゃ。
- G 君: 相関が強いものや弱いものを組み合わせるんじゃ。で、結論はこの数値じゃというようにやるんじゃ。
- B 君: カラオケマシンの採点みたいなもん? がっこの先生が付ける総合評価つう感じ?
これらのメトリクスはプログラムを直接計測することにより、その値が得られます。 これを一次メトリクスと呼んでいます。
この一次メトリクスを使って計算して、二次メトリクスを得ることができます。多くのメトリクスツールではこの二次メトリクスを計算して、プログラムの総合的な計測結果を与えるようにしています。
「いちゃもん」では上記のうちの基本的なメトリクスを計測するようにします。少なくとも 1 から 3、6、 7 を計測するようにします。
またプログラムに「いちゃもん」をつけるために上記のものに加えて、以下のものも計測するようにします。
- 1行の文字数(長すぎる行を計測する)
- シンボルの文字数(長すぎる名前を計測する)
以下に現時点で想定している一次メトリクスの結果を例示します。
トータル
行数 128
命令行数 96
ステートメント数 64
コメント行数 32
空行 16
クラス数 2
メソッド数 4
使用システムメソッド数 4
ファイル test.Test.java
行数 128
命令行数 96
ステートメント数 64
コメント行数 32
空行 16
クラス数 2
メソッド数 4
使用システムメソッド数 4
クラス Test
メソッド main
出現行 7
ステートメント数 32
メソッド print
出現行 48
ステートメント数 16
また一次メトリクスで計測する警告データの表示として、以下のものを考えています。
ファイル 行番号
80桁を越える行
16文字を越える名前
- A 君: おぉ、いちゃもんの片鱗を窺わせますね
- G 君: まぁ、プログラムを斜め読みして、気になるところじゃからの。
- G 君: しかしじゃ、こんなものはまだまだいちゃもんじゃないぞ。甘いぞ。
- A 君: じゃ、もっと、あるんですね。そういえば「一次」というようにしているのが怪しい!
- G 君: そうじゃ、一次があるということは二次があるということじゃ。そこでじゃ。いちゃもんは。
- A 君: 種明かしはまだしないんですね。ここでは。B君も行方不明だし、まぁ、いいか。
明日は、作成方針を軽く立てて、概要レベルのクラス設計をしてみます。
See you again !!
2日目 --- クラス設計 最初の始まり --- 現物系クラスの設計
今日は最初のクラス設計をします。ここで「最初」と言っているには、クラス設計で2回目以降があるということを匂わせています。今日は最初のクラス設計の始まりを書いていきます。
クラス候補
まずは、システム設計の前にシステム分析をします。このメトリクスツールが行うことを時間の流れにそって記述してみましょう。
- ソースプログラムを読み込む
ソースプログラムの記述子を解析し、ファイルパスのグループを得る
ファイルパスのグループに対応する複数のファイルを読み込む - 読み込んだソースプログラムを字句解析・構文解析する
字句解析する
構文解析する、但しここでは簡単な構文解析でよい - 注目するデータを抽出する
データを収集する
データに統計的処理を行う - その結果をファイルや画面に出力する
データを見やすいように変形する
HTML, CSV 形式にしてファイル書き込みを行う
ファイル読み込み、構文解析、データ抽出、ファイル出力の4つの大きな流れがあります。これが機能クラス(またはコントロールクラスと呼ぶ)の候補になります。
次にこのシステムで登場する主要な登場人物、つまりモデルとなるオブジェクトを以下に示します。
- ファイル群
- ファイル
- ソースプログラム
- 行
- 語(トークン)
- クラス
- メソッド、関数
- ステートメント
- メトリクスデータ
- 一次メトリクスデータ (平均ファイルステートメント数、平均クラスステートメント数など)
- 二次メトリクスデータ各種の統計データ()
- 出力ファイル
- プログラム解析の状態(解析ファイル名、解析行、クラスの中か、メソッドの中かなど)
これらがクラスオブジェクトやインスタンスオブジェクトの候補、またはそれぞれのオブジェクトの属性の候補になります。
最後にこのシステムの外部インタフェースを挙げてみます。
- 1個または複数のソースファイルを入力として、そのメトリクスデータをファイル出力する
これは一般には API (アプリケーションインタフェース)と呼ばれているものです。 Java のインタフェースと区別するために境界(バウンダリ)系と呼ぶことにします。これもクラス候補の一つになります。
以上より、機能系で4個、モデル系で13個、境界系で1個のクラス候補を発見したことになります。
- A 君: モデル系のオブジェクトでやっとオブジェクト指向ぽくなりましたね
- G 君: すべてはオブジェクトじゃ。すべては平等じゃ。機能もオブジェクトじゃ。
- A 君: って言ってもね・・・ 西瓜は夏とかいうようなイメージがあるし。 継承がないものは OO に非ずって
- G 君: それは過去の遺物じゃ。原理主義者の考えじゃ。
- B 君: 最初の発言と違ってまっせ。お宅は80年代派?それとも90年代派?
- G 君: 何を言うんじゃ。Simula を使う60年代派じゃ。じゃない、2000年派じゃ。C# の次の Cm7 じゃ。
後は
- 特化方向の共通化のための部品クラス
- 一般化方向の共通化のための抽象化クラス
- イベント系クラス・例外系クラスなどその他の特定用途のクラス
などの現物系クラスでない潜在的なクラスの発見が必要になります。
現物系クラスの設計
まずは、機能系、モデル系、境界系(これは現物系ではありませんが、ついでにクラスを作成します)の現物系クラスの設計をしてみます。
前節で書いたクラス発見の第一のフィルタとしては、そのオブジェクトの「サイズ」に注目するといいでしょう。
やはり、クラスは即値型と比較して効率が悪いものなので、一定のサイズ以上のものをクラスにするのがいいでしょう。
このサイズの閾値は人(の趣味)やプロジェクトにより異なります。 Java には構造体がありませんので、構造体とクラスの両方がある言語に比較して、その閾値は小さくなるでしょう。 その閾値は、例えば8個では既に即値型として扱うのには多すぎるでしょう。4個であれば迷うでしょう。1個であれば、クラスにするにはサイズ面からは大げさすぎるでしょう。
- A 君: またマジックナンバが出ていますね。4個か8個かですか。
- G 君: 本来はもっと気楽にしたいんじゃが、Java はクラスが別れてしまうからの。
- A 君: 内部クラスではどう?
- G 君: まぁ、方便じゃな。制限があるからのぉ。
このサイズだけの観点で、モデル系を見ますと、以下のものが選定したクラス候補になります。
- ファイル群(ファイルのコレクションクラス)
属性には、ファイルを要素とするコレクションデータがある - 語(トークン)群
属性には、システムのトークンかユーザ定義のトークンか、関数か変数かなどの種別がある - ファイル単位のメトリクスデータ
ファイル単位の行数、ステートメント数、クラス数などのメトリクスデータがある。 - プログラム断片(クラスやメソッド)単位のメトリクスデータ
メソッド単位の行数、ステートメント数などのメトリクスデータがある。 - プログラム解析の状態
括弧やカーリーブラケットの深さ、メソッドの内外、クラスの内外などのプログラムの各種状態を保持する
4 のプログラム断片単位のメトリクスデータ と 5 のプログラム解析の状態は 扱う単位が両者とも同じプログラム断片になりますので、まずは同じクラスにしましょう。 大きくなるようであれば、将来のリファクタリングで分割します。
それぞれに「いい」名前を付けてクラスにしましょう(これは重要です)。 ここでは FileGroup, SymbolTable, FileStatus, ProgramStatus にしましょう。
注意: Data や Information, Flag の名前に情報は少ないのと同様に Status にもそれほど多くの情報はありませんが、それらよりはまだいいということで名前を付けています。
- A 君: これがいい名前?
- G 君: まぁ、そういうな。本人も反省していることじゃろうから。
- A 君: それなのに、Data や Information なんかには「いちゃもん」つけてるし。
次に機能系を考えみましょう。そもそも機能系クラスは存在悪なのでしょうか。それとも必要悪なのでしょうか。はたまた正義なのでしょうか。
ここでは必要悪だと考えて、必要最低限の機能クラスを作成します。
- A 君: さらっと言っていますが、流していいんですか、機能クラスについては
- G 君: <<control>> と付ければ許される、免罪符みたいなもんじゃよ
- A 君: 最初から謝ってしまおうという魂胆?
- G 君: 免罪符は他にも一杯あるしの。まぁ、ここは宗教戦争は避けるんじゃ。
- 読み込みと解析(ファイル読み込み+字句解析+簡単な構文解析)
- ファイル出力
ここもいい名前を付けましょう。ここでは Reader と Printer にします。
最後に境界系は、
- 1個または複数のソースファイルを入力として、そのメトリクスデータをファイル出力する
があります。これは外部インタフェースとなりますので、「特に」いい名前を付けましょう。ここでは Ichamon にします。
- A 君: いい名前と言って Ichamon という名前?
- G 君: すべては宿命(さだめ)じゃ
まとめ
今日はクラス設計の最初の始まりとして、現物系のクラスを発見・抽出しました。結果として以下のクラスがありました。
- FileGroup
ファイル群。入力ファイルの集合となるコレクションクラス。 - SymbolTable
プログラム中に登場するトークンを管理するクラス。 - FileStatus
ファイル単位のメトリクスデータを管理するクラス。 - ProgramStatus
プログラム断片(クラス、メソッド)単位のメトリクスデータやそのプログラム断片の状態を保持する。
将来は分割する可能性もある。 - Reader
ファイル読み込みと字句解析、簡単な構文解析を行なう機能系クラス。 - Printer
ファイル出力を行なう機能形系クラス。 - Ichamon
外部 API となるクラス。スタートクラスの役目も持つ。
次回はクラス設計から離れて、最初のクラス実装として、ファイル群 FileGroup に関して書いていきます。ここではファイルフィルタが登場します。
See you again !!
3日目 --- ファイルフィルタとワイルドカード --- ファイルグループクラスの作成
今日はファイルグループのクラスを作成していきます。このクラスは、ワイルドカード付きのファイル名を与えて、実際のファイル名を得るためのクラスです。例えば、ディレクトリに、"Abc.java", "Abc.class", "Abc$def.class" があるときは、以下のようになります。
- A*.java ---> Abc.java
- *.* ---> Abc.java, Abc.class
- [A-z|.|$]* ---> Abc.java, Abc.class, Abc$def.class
ここで注意することは、1, 2 と 3 は異なる種類のワイルドカードを使っていることです。1, 2 はファイルアクセスでよく見かける種類のワイルドカードになっています。一方、3 はどちらかというと正式なワイルドカードになっています。つまり、BNF 記法などで使われている記法と同じ記法になっています。 能力的には 3 の方が大きく、1,2 は 3のサブセットを表現しています。ここでは 1,2 だけでなく 3 もサポートすることにします。
- A 君: なんで本格的なワイルドカードを採用するんですか?
- G 君: それはじゃ、「なんとなく」じゃなくて、細かい機械的ファイル選択が必要だからじゃ。
- A 君: ほんと? そう思っただけなんでしょう。何でかな、変な風に拡張する・・・まぁ、いいか。
ワイルドカード処理のためにファイルフィルタを以下に定義します。
/**
* Regular Expression for FileFilter
*/
class FileFilterImpl implements FileFilter {
String regexp; // Regular expression
/**
* File Filter implementor
* @param regexp Reguler expression for matching file name
*/
FileFilterImpl(String regexp) {
this.regexp = regexp;
}
/**
* checks accepting given refuler expression and file name
* @param file For matching file
* @return Matching result
*/
public boolean accept(File file) {
try {
Pattern pattern = Pattern.compile(regexp);
Matcher match = pattern.matcher(file.getName());
return match.matches();
} catch (Exception e) {
return true;
}
}
}
ファイルフィルタを生成するとき(new FileFilter(regexp))に、正規表現式をコンパイル(pattern.compile(regexp))せずに、ワイルドカードの文字列をそのまま保持するようにします。
この正規表現式に対して、ファイル名がアクセプトされるかどうかをチェック(match.matches)します。ワイルドカードとして正しくないときは無制限に受け入れる(catch (Exception e) { return true; })ようにします。
デフォルトのワイルドカードの取り扱い(*.java や *.* など)よりも豊富な機能を提供することができるようになります。また $ が入るファイルなどは発見できませんでしたが、上記のものでは、例えば [A-z|$|.]* のように指定することにより、発見できます。
- A 君: またまた、デフォルトをファイル発見 (true)の方に倒す・・・ 逆でしょう、普通は。
- G 君: いや、何も出ないと寂しいものがあるからじゃ、ねずみ一匹だけではそびしいぞ。
次にこのファイルフィルタを使う側を作成します。
/**
* get File Name
* @param name file name
*/
private void getFileName(String input) {
String dir; // directory
if (input == "") return;
int dirPos = input.lastIndexOf('/');
if (dirPos >= 0) {
dir = input.substring(0, dirPos);
} else {
dirPos = input.lastIndexOf('\\');
if (dirPos >= 0) {
dir = input.substring(0, dirPos);
} else {
dir = ".";
}
}
File directory = new File(dir);
String regexp = input.substring(dirPos + 1, input.length());
// expand directy or file name with wildcard to file names
File[] filesFile = directory.listFiles(new FileFilterImpl(regexp));
if (filesFile == null) return;
for (int j = 0; j < filesFile.length; j++) {
String fileName = dir + "/" + filesFile[j].getName();
files.add(fileName);
}
return;
}
このプログラムの前半部は、与えられたパス名から、ディレクトリとファイルの切り分けを行なっています。/ (スラッシュ)と \(バックスラッシュ)のどちらでも対応するようにしています。
File[] filesFile = directory.listFiles(new FileFilterImpl(regexp)) でファイルフィルタを実行するようにしています。
その後は、得られたファイル名をディレクトリを気にせずにフラットにアレイリストに格納(files.add(fileName))しています。
- A 君: ディレクトリの解析が単なる文字列処理ではないですか、エレガントでないですが?
- G 君: ちと、面倒でのぉ。まぁ、将来リファクタリングするかも知れんの、ふぉ。
- A 君: デジャヴのような、このセリフと状況。
以下は残りのプログラムになります。宣言やクラス定義、コンストラクタを作成しています。
/*
* FileGroup
* @author go
*/
package jp.co.okisoft.esc.metrics;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.regex.*;
import java.io.*;
/**
* generates Source Program Files
*/
public class FileGroup {
/** file name list */
public ArrayList files = new ArrayList();
/**
* generates File Name Group
* @param inputs File name group or directory, file name with wildcard
*/
FileGroup (String[] inputs){
for (int i = 0; i < inputs.length; i++) {
getFileName(inputs[i]);
}
}
また、各クラスに、オブジェクトのプリントメソッド print やクラス単位の単体テストを行なうメソッド test を定義することにします。このメソッドを呼び出すメインメソッドも以下に定義します。
- A 君: 毎クラス、これを定義していくんですか? 面倒そう・・・
- G 君: 基本はそうじゃ。まぁ、見てのお楽しみじゃ。
- A 君: 500行に収まらないと思いますよ、こんなものを書いていると。
- G 君: それはじゃ、えーと、抜かすんじゃよ。ログと単体テスト、アサーションなんかはなぜか許されるんじゃ。
- 後日談
- G 君: テストプログラム自身のテストプログラムも必要で、さらにそれにも必要になるんじゃ。
- G 君: 500行のプログラムに対し、テストケースを実行するんじゃが、そのプログラムは50行でできるんじゃ。
- G 君: 50行のテストプログラムに対して、そのテストプログラムは10行でできるんじゃ。
- G 君: 10行のテストプログラムに対して、そのテストプログラムはやはり10行でできるんじゃ。
- G 君: ほうら、意味論をちゃんと定義しないと、テストをするためにプログラムを作り続けることになるんじゃ。
- B 君: 「人は皆、テストをするためにプログラムを作る」ってことでっか、まぁこういうとこにほんまのことがあるんとちゃう。
/**
* print
* @param out PrintStream for print
*/
public void print (PrintStream out) {
out.println("Files:");
Iterator elements = files.iterator();
while (elements.hasNext()) {
out.println((String) elements.next());
}
}
/**
* for Test
* @author go
*/
public static void test (String[] args) {
System.out.println("\nTEST:::FileGroup 1 (argument)");
FileGroup fileGroup = new FileGroup(args);
fileGroup.print(System.out);
System.out.println("\nTEST:::FileGroup 2 (2 wild card .../[A-z|.|$]*)");
String[] params1 = {"jp/co/okisoft/esc/metrics/[A-z|$|.]*", "[A-z|.]*"};
FileGroup fileGroup1 = new FileGroup(params1);
fileGroup1.print(System.out);
System.out.println("\nTEST:::FileGroup 3 (null string)");
String[] params2 = {""};
FileGroup fileGroup2 = new FileGroup(params2);
fileGroup2.print(System.out);
System.out.println("\nTEST:::FileGroup 4 (illegal string)");
String[] params3 = {"*"};
FileGroup fileGroup3 = new FileGroup(params3);
fileGroup3.print(System.out);
}
/**
* dummy main
* @param args nothing
*/
public static void main (String[] args) {
test(args);
}
}
実行結果は以下のようになります。
TEST:::FileGroup 1 (argument) Files: jp/co/okisoft/esc/metrics/FileGroup.class jp/co/okisoft/esc/metrics/FileGroup.java ./.classpath ./.project ./jp TEST:::FileGroup 2 (2 wild card .../[A-z|.|$]*) Files: jp/co/okisoft/esc/metrics/FileGroup$FileFilterImpl.class jp/co/okisoft/esc/metrics/FileGroup.class jp/co/okisoft/esc/metrics/FileGroup.java ./.classpath ./.project ./jp TEST:::FileGroup 3 (null string) Files: TEST:::FileGroup 4 (illegal string) Files: ./.classpath ./.project ./jp
Test 1 は引数として、jp/co/okisoft/esc/metrics/*.* *.* を与えています。$付きのものはこれでは発見できません。Test 2 ではjp/co/okisoft/esc/metrics/[A-z|$|.]* [A-z|.]* を与えています。$ 付きのファイル名も発見できています。Test 3では "" (ナルストリング)を与えています。結果はありません。Test 4 ではワイルドカードとしては誤りの "*"を単独で与えています。この場合はすべてのファイルを返すようにしています。
- A 君: やっと終わった、メトリクスから離れていくような気がしていましたが、明日からはきっともどれるんでしょうね。
- G 君: まぁ、一休みじゃ。この手のライブラリ系も作らんとの、本質的なこと以外でいちゃもんは嫌じゃからの。
今日はファイルフィルタを使ってファイル指定でワイルドカードを扱えるようにしました。明日は「やさしい Lisp の作り方」で作成した字句解析部を持ってきてプラグインするようにしてみます。
See you again !!
4日目 --- 字句解析部のインポート
今日は、字句解析の骨格を作ります。以前作成した「やさしい Lisp の作り方」の字句解析部の骨格を持ってきて、変更して使うようにします。
1ファイルの読み込み readFile と1行読み込み getLine、1文字読み込みの getChar、1文字先読みの nextChar を作成します。
少し長いですが、そのまま紹介します。
/**
* read source program file and get function names
* @param fileName source program file name
*/
public void readFile(String fileName) {
this.fileName = fileName;
status.registFileName(fileName);
try {
file = new FileInputStream(fileName);
} catch (FileNotFoundException e) {
println("File " + fileName + " is NOT FOUND by Reader.");
return;
}
in = new InputStreamReader(file);
br = new BufferedReader(in); // generats Buffer Reader
EOF = false; // EOF is false
getLine(); // get 1 line from file
getChar(); // get 1 character;
try {
getTokens(); // get tokens
} catch (Exception e) {
e.printStackTrace();
}
try {
br.close();
in.close();
file.close();
status.saveStatus();
} catch (IOException e) {
println("File " + fileName + " is NOT CLOSE.");
return;
}
}
/** get 1 Line */
private void getLine() {
String line; // 1 line buffer as String
int lineLength; // character size of 1 line
try {
line = br.readLine(); // reads first line
} catch (IOException e) {
println("Read Error " + br.toString());
return;
}
if (line == null) { // EOF is coming
status.lineFeedFinal(emptyLine);
EOF = true;
return;
}
currentLineNumber++;
status.lineFeed(emptyLine);
emptyLine = true;
indexOfLine = 0; // position of 1 line
lineLength = line.length(); // set line length
// if charBuff is overflow, then allocate charBuff
if (lineLength >= CharBuffSize) {
charBuff = new char[lineLength + 1];
CharBuffSize = lineLength + 1;
}
line.getChars(0, lineLength, charBuff, 0); // for efficient using ChAry
charBuff[lineLength] = '\0'; // set EOL mark
return;
}
/** get 1 Character */
private void getChar() {
char nch = charBuff[indexOfLine]; // get Current Character
while (nch == '\0') {
getLine(); // if end, then reads next line
if (EOF) return;
nch = charBuff[indexOfLine];
}
ch = charBuff[indexOfLine++];
}
/**
* get next 1 Character
* @return char in current char (i.e., next char)
*/
private char nextChar() {
char nch = charBuff[indexOfLine];
while (nch == '\0') {
getLine(); // if end, then reads next line
if (EOF) return '\0';
nch = charBuff[indexOfLine];
}
return charBuff[indexOfLine];
}
次に文字解析部のディスパッチャ部を持ってきます。1文字読み込んで、文字種類によって分岐する部分になります。
まずはシーケンシャルに分岐する方法で実装します。実装のコツとしては、分岐部分はなるべく1関数にすることにより、全体の見通しをよくするようにします。実際、1関数でほとんどを実装します。これにより、将来のリファクタリングでO(1)で実行する、例えばジャンプテーブルによる実装が行ないやすくなります。
- A 君: デ・ジャ・ヴですね、やっぱ。
- G 君: 何事においても繰り返しはあるんじゃ、そこで何を進歩させたかじゃ。
- G 君: 例えば、作成時間、例えば、ネーミング、例えば、可用性などじゃ。例えば、言い訳じゃ。
/**
* get Tokens via charBuff from source file
*/
private void getTokens() {
while (true) {
if (Character.isWhitespace(ch)) { // WhiteSpace
; // nothing process
} else if (Character.isDigit(ch)) { // Number
setInitDefault();
} else if (Character.isJavaIdentifierStart(ch) ||
ch == '_') { // Symbol
symbolToken(); // stackTop is keep
} else {
switch (ch) {
case '/': // commentable
commentToken(); // if not comment, not keeping
break; // if comment, comment is ignore
case '"': // String
stringToken();
break;
case '(': // OpenParen
openToken();
break;
case ')': // CloseParen
closeToken(); // stackTop is keep in toplevel
break;
case '{': // OpenCurl
openCurlToken();
break;
case '}': // CloseCurl
closeCurlToken();
break;
case ';': // Close
semiColonToken();
break;
case '\'': // SingleQuote
charToken();
break;
case ',': // Comma
symbol = null; // stakTop is keep
emptyLine = false; // i.e., f() a, b {} is o.k.
break;
default:
setInitDefault(); // otherwise
break;
}
}
if (!EOF) getChar(); else return; // read next character
} // end of while
}
ここまでは定型的なパターン、プログラムイディオムになっています。
- A 君: パターンになっているなら、文字解析プログラム生成ウィザードがあってもいいんじゃない?
- G 君: そうなんじゃ。作ろうかの。
- A 君: プログラムの可変部、えっと、いわゆる文字解析の規則だけをパラメータで与えるのは?
- G 君: それは効率の悪い lex みたいなものじゃ、趣味じゃないが。
次に各トークンの処理を見ていきましょう。例として、以下に '/' が来たときのコメント文処理のためのプログラムを示します。
/**
* Comment statement --- skip comment end
*/
private void commentToken() {
if (!EOF) getChar(); else return;
if (ch == '/') {
status.lineCommentIn();
skipLineEnd(); // skip line end
status.lineCommentOut();
return;
}
if (ch == '*') {
status.blockCommentIn();
skipCommentEnd(); // skip comment end
status.blockCommentOut();
return;
}
setInitDefault();
return;
}
前半部で "//" から始まるラインコメント文の処理を行ないます。これは status.lineCommentIn() でラインコメントに入ったことを知らせ、次にskipLineEnd()で行末まで読み飛ばし、status.lineCommentOut()でラインコメントから外れたことを示します。後半部では status.blockCommentIn() でブロックコメントに入ったことを知らせ、次に skipCommentEnd() で "*/" が出現するまで読み飛ばすようにします。setInitDefault() は字句解析の初期状態に設定するための関数です。
同様に他のトークン、例えば、シンボルや文字列、数値なども同様にその状態に入ったことを知らせるメソッド関数を呼び出すようにします。知らせを受けたメソッド関数がどのように処理をするかについては後日に記述する予定です。
- A 君: 既に Reader だけでも 200 行(ステートメント)になってるみたい・・・本当に 500行で大丈夫?
- G 君: 大丈夫じゃ、たぶん。越えた分は誤差じゃ、きっと。
- A 君: 今から逃げを打って。まぁいいか。
今日は文字解析の骨格部分をやってきました。明日はそこから使用する部品クラスについて紹介していきます。シンボルテーブルやファイルステータスなどを紹介する予定です。
See you again !!
5日目 --- シンボルテーブルなどの部品クラスの作成
今日はシンボルテーブルなどの部品クラスを作っていきましょう。まずはシンボルテーブルを作ります。
シンボルテーブル
シンボルテーブルとは、この計測ツールで、対象とするプログラムのシンボルを管理するためのテーブルです。例えば、シンボルには Foo, int, return などの予約語や変数名、関数名などがあります。これらのシンボルがどういう種別(例えば、ステートメントを作る構成子なのか、(メトリクスにとっては)単なる名前なのか、などを区別し、また以前に出現したシンボルかどうかを判定するのに使います。
シンボルテーブルは、高速化のためにハッシュテーブルで実装します。今回の場合は HashMap で実装します。同期の責任はユーザが持つようにして高速化します。と言っても要素には即値型が格納できませんので、どちらにしろ遅いのは仕方がありません。
- A 君: 最後の1行は Java に対する愚痴っぽいですが
- G 君: そうじゃ、遅いから仕方がないんじゃ。Java の呪いじゃ。
シンボルテーブルクラスを作成し、それにメトリクスツールが注目すべき予約語とその情報を格納するようにします。
package jp.co.okisoft.esc.metrics;
import java.util.HashMap;
/**
* Symbol Table
* @author gomi
*/
public class SymbolTable extends HashMap {
public final static String CLASS_SYMBOL = "CLASS";
public final static String SPECIAL_FORM = "SPECIAL FORM";
private String[] reserveds = {
"class", CLASS_SYMBOL,
"for", SPECIAL_FORM,
"if", SPECIAL_FORM,
"while", SPECIAL_FORM,
"switch", SPECIAL_FORM,
};
/**
* Constructor SymbolTable
*/
SymbolTable(){
super();
initReserved();
}
/**
* init reserved
*/
private void initReserved () {
for (int i = 0; i < reserveds.length; i += 2) {
put(reserveds[i], reserveds[i + 1]);
}
}
}
SymbolTable extends HashMap で実装します。もちろん委譲モデルでの実装もありますが この場合は継承モデルの方が効率が良いでしょう。
次に属性情報としては、クラスを判別する識別子として CLASS_SYMBOL、とステートメントを 計測するときに特別扱いする予約語を SPCIAL_FORM として与えます。 reserveds に注目する予約語を名前、属性の順で格納します。 具体的には クラス識別子として、"class"、スペシャルフォームとして、"for", "if", "while", "switch" を入れています。 例えば、C# などの場合は foreach, using なども必要になってきます。
- A 君: "switch", SPCIAL_FORM, のように最後もカンマ止めなんですね
- G 君: まぁ、最後にカンマ忘れをしないためのものじゃ。JavaScript ではこの技は効かんがの
- A 君: それだけ、忘れてしまった経験があるってことですね
HashMap を継承していますので、セッタ、ゲッタはそれを使うようにします。 クラス名に意味論を明示化することと予約語設定のためにこのクラスを採用しています。
ファイルステータス
次にファイル単位のメトリクスデータを格納するためのクラス「ファイルステータス」を作成します。
package jp.co.okisoft.esc.metrics;
import java.util.ArrayList;
/**
* class FileStatus
*/
public class FileStatus {
private String fileName;
private int totalLines;
private int operationLines;
private int statementLines;
private int commentLines;
private int emptyLines;
private ArrayList functions;
private ArrayList classes;
/**
* Constructor of File Status
*/
public FileStatus (String fileName,
int totalLines,
int operationLines,
int statementLines,
int commentLines,
int emptyLines,
ArrayList functions,
ArrayList classes) {
this.fileName = fileName;
this.operationLines = operationLines;
this.totalLines = totalLines;
this.statementLines = statementLines;
this.commentLines = commentLines;
this.emptyLines = emptyLines;
this.functions = functions;
this.classes = classes;
}
/** getter */
public String getFileName () { return fileName; }
/** getter */
public int getTotalLines () { return totalLines; }
/** getter */
public int getOperationLines () { return operationLines; }
/** getter */
public int getStatementLines () { return statementLines; }
/** getter */
public int getCommentLines () { return commentLines; }
/** getter */
public int getEmptyLines () { return emptyLines; }
/** getter */
public ArrayList getFunctions () { return functions; }
/** getter */
public ArrayList getClasses () { return classes; }
}
ここは、まさに生成時のセッタと、各ゲッタのみを用意したクラスです。Java ではこのような完全に受動的なオブジェクトでも構造体でなく、クラスで作成する必要があります。即ち、参照渡しになってしまい、コストが掛かります。
- A 君: 話を構造体に持っていくなんて、Java を敵対視していますね
- G 君: いやそうではなくて、こんな機械的なコードは書きたくないんじゃ、書かなくてもいいようにしたいんじゃ。
- A 君: じゃ、書かなければ・・・ まぁ、今日は40ステートメントも行っていないから、いいか。
全体の流れ
字句解析「リーダ」でトークンを解析して取り出し、さらにプログラムの状態を構文解析をせずにまたは簡易な構文解析で解析を行います。このクラスは明日紹介する予定の「プログラムステータス」クラスになります。シンボルが出現したときは、シンボルテーブルによって、注目するシンボルかどうかを判定します。解析した結果をファイル単位で格納するクラスとしては「ファイルステータス」クラスを用意します。最後に結果を機能クラス「プリンタ」でファイルに出力します。
今日のステートメント数は40個も行っていません。小さなクラスとなっています。
明日はプログラムの状態を解析する「プログラムステータス」クラスの実装について記述していきます。
See you again !!
6日目 --- プログラム解析・管理部の作成
今日はプログラム解析し、その情報を蓄積し管理する部分を作成していきます。先日のリーダを作るときには、例えば、 '{' が出現したときには
case '{': // OpenCurl
openCurlToken();
break;
のように openCurlToken メソッドを呼び出すところまでしかありませんでした。今日はその先をやっていくことにしましょう。
- A 君: '{' を Curl カールと呼ぶなんて、ちょっと、マイナーじゃない?
- G 君: いや、最新版のハッカーズディクショナリでは、こう呼ぶんじゃ
ここで、プログラムの解析の状態を管理するクラスとして、ProgramStatus を作成することにします。このインスタンスオブジェクトが、対象のプログラムの状態、例えば、クラス定義中なのか、メソッド定義中なのか、それともメッセージ通信を行うところなのかなどの状態を保持するようにします。この状態を保持することができるように、状態を変化させる要因、例えば、'{' のような注目する文字が出現したときには、状態変化を行う status のメソッドを呼び出すことにします。
例えば、openCurlToken メソッドでは、プログラムの解析状態を管理するオブジェクト "status" のメソッド openCurl を stackTop の値を引数として呼び出します。
/**
* OpenCurl '{' Token
*/
private void openCurlToken() {
status.openCurl(stackTop); // add 1 to Curl Bracket level
symbol = null;
emptyLine = false;
return;
}
フィールド変数 symbol は現在解析中のシンボルを保持するために用意したものです。'{' が出現しましたので、シンボル解析を抜け出したことを symbol = null によって、表現しています。またフィールド変数 emptyLine はコメントでないプログラムとして有効な文字が出現したかどうかのフラグとして使用しています。'{' のようにプログラムに有効な文字が出現しましたので、このフラグを false にしています。
他の文字に対しても、同様な処理を行うことにします。
プログラムの各種状態を持つために、いくつかのフィールド変数を持ったクラス ProgramStatus を定義します。その宣言部は以下のようにしました。
package jp.co.okisoft.esc.metrics;
import java.util.ArrayList;
/**
* Program Status Class
* @author gomi
*/
public class ProgramStatus {
// constant
private final static int lineComment = 1; // line comment
private final static int blockComment = 2; // block comment
private final static int empty = 4; // empty line or normal line
private final static int normalMode = 0;
private final static int classMode = 1;
// public variable
public int lineMode; // current line mode
public static ArrayList files; // file inf.
// private variable
private String fileName; // file name
private int curlLevel; // Curl Bracket level
private int operationLines; // total operation line number
private int statementLines; // total statement line number
private int totalLines; // total source file line
private int commentLines; // comment line number
private int emptyLines; // emptyLine number
private String functionSymbol; // current function symbol
private int startFunction; // start position of Function defined
private int startLevelFunction; // start level of Function defined
private ArrayList functions; // function inf.
private int statementMode; // statement mode;
private ArrayList classInf;
private int currentClassCurlLevel = -1;
- A 君: ちょおと、多いんじゃない、フィールド変数が、これって結局「グローバル変数の使いすぎ」っていうやつでしょ
- G 君: インタフェース用2個を除けば14個じゃ、うーん、少し多いかの、内部クラスをもっと作ろうかの
一方、解析した結果であるメソッドの情報を管理するための内部クラス MethodInf を以下のように定義します。
/** Method information */
class MethodInf {
String methodName;
int startLine;
int startStatement;
int curlLevel;
String className;
/** constructor */
MethodInf (String name, int lines, int statements,
int curl, String className) {
this.methodName = name;
this.startLine = lines;
this.startStatement = statements;
this.curlLevel = curl;
this.className = className;
}
}
同様に、解析したクラスの情報を管理するためのクラス ClassInf を以下のように定義します。
/** Class information */
class ClassInf {
String className;
int startLine;
int startStatement;
int curlLevel;
String fileName;
/** constructor */
ClassInf (String name, int lines, int statements,
int curl, String file) {
this.className = name;
this.startLine = lines;
this.startStatement = statements;
this.curlLevel = curl;
this.fileName = file;
}
}
これらの3個のクラスの役割分担は、解析中のプログラム全体の情報を管理する ProgramStatus 、クラス単位の情報を管理する ClassInf 、メソッド単位の情報を管理する MethodInf のようになります。ClassInf と MethodInf は受動的なクラスで、能動的に動作するのは ProgramStatus になります。
- A 君: って、内部クラスを作ってんじゃン、それも2個も
- G 君: また、いちゃもんかい、うーん、1個までかな
ここでも '{' が出現したときに動作する ProgramStatus のメソッド openCurl を見ていきます。
/**
* open curl bracket
*/
public void openCurl (String symbol) {
if (curlLevel == 1 + currentClassCurlLevel && symbol != null) {
userDefinedFunction(symbol);
}
curlLevel++;
return;
}
ここで、 フィールド変数 curlLevel は '{' の深さのレベルを保持しています。またフィールド変数 currentClassCurlLevel は現在のクラス定義を行ったときの '{' の深さのレベルを保持しています。メソッド userDefinedFunction はユーザ定義関数の登録を行うメソッドです。
ここでは、以下の規則で簡易的に、つまり本格的な構文解析をすることなしに、「ユーザ定義メソッドの開始」を発見しています。
- クラス定義したときの '{' の深さよりもちょうど1段だけ深い '{' の出現の前で(ここだけではわかりませんが '(', ')' が出現する前の) シンボル symbol がメソッド名であり、ここからメソッド定義が開始される
このような規則でメソッドを発見しています。
- A 君: おぉ、手抜きのメソッド発見アルゴリズムですね
- G 君: トリッキー、いや、エレガントな発見方法と言ってほしい
- A 君: マクロ定義があった場合はやばいんじゃない、それをエレガントなんて
- G 君: マクロ展開した後のプログラムを解析するんじゃ」ということにしておいて、考えないんじゃ
- G 君: マクロは必要悪または純粋な悪じゃ、決して、使ってはいかんぞ
- A 君: またそんなことを言う、マクロのみのプログラムを面白がって書いてた癖に・・・
またクラス定義外のメソッド(または関数)定義は、クラスの '{' の深さレベルを -1 にすることにより、発見しています。
- A 君: -1 なんて、いまどき、「番兵」みたいはテクニックを使うなんて古い
- G 君: そう言うな、比較が透過的にできるなんて、ちょっとエレガント、いやトリッキーじゃろ、きっと
今日はプログラム解析クラス ProgramStatus の骨格を中心に進めてきました。プログラムステータスの残りの部分やリーダの残りの部分は後日、アップするようにします。明日は一次メトリクスデータの印字について、書いていきます。
See you again !!
7日目 --- メトリクスデータのファイル出力
今日は6日目までに収集したプログラムメトリクスデータをファイル出力するプログラムを作成します。
今回は簡単のために CSV 形式のファイルを出力するようにします。
中心となるメソッド printFiles を以下に紹介します。
すべてのメトリクスデータは、ファイル単位で管理していて、ProgramStatus の static 変数 files に格納されています。
ファイル単位のデータから全体のデータを導き出すメソッドは、ProgramStatus の totalStatus() です。また全体のデータのファイル出力を行うメソッドはファイル個別のメソッド printFileStatus() と共通化しています。第2引数に true を与えることにより、ファイル個別でなく全体のデータであることを示しています。
次に各クラスの情報や各メソッドの情報のファイル衆力部分は printFiles 自身にあります。少し長めになりますが、このメソッドを以下に示します。
/**
* print files information to file
*/
public void printFiles () {
FileOutputStream file; // file input stream for source program file
try {
file = new FileOutputStream(filesFile);
} catch (FileNotFoundException e) {
println("File " + filesFile + " is USED.");
return;
}
OutputStreamWriter out = new OutputStreamWriter(file);
BufferedWriter bw = new BufferedWriter(out); // generats Buffer Writer
ArrayList files = ProgramStatus.files;
try {
// total size
FileStatus total = ProgramStatus.totalStatus();
printFileStatus(total, true, bw);
bw.newLine();
bw.newLine();
// each file
for (int i = 0, size = files.size(); i < size; i++) {
FileStatus fs
= (FileStatus) files.get(i);
printFileStatus(fs, false, bw);
// user defined class
ArrayList classes = (ArrayList) fs.getClasses();
for (int j = 0, fsize = classes.size(); j < fsize; j++) {
ProgramStatus.ClassInf classInf
= (ProgramStatus.ClassInf) classes.get(j);
bw.write(", Class, " + classInf.className);
bw.write(", start line, " + classInf.startLine);
bw.write(", statement number, " + classInf.startStatement);
bw.newLine();
}
// user defined methods
ArrayList functions = (ArrayList) fs.getFunctions();
for (int j = 0, fsize = functions.size(); j < fsize; j += 2) {
if (j + 1 < fsize ) {
if (functions.get(j + 1).getClass() == integerClass) {
bw.write(",, Function, " + functions.get(j) +
", statement number, " + functions.get(j + 1));
bw.newLine();
} else {
j--;
}
}
}
bw.newLine();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
bw.close();
out.close();
file.close();
} catch (IOException e) {
println("File " + out + " is NOT CLOSE.");
return;
}
}
クラスに関する情報は ClassInf オブジェクトとして格納しています。一方、メソッドの情報は名前とステートメント数の2項だけですので、 MethodInf オブジェクトではなく、アレイリストに直接代入しています。
- A 君: クラス情報とメソッド情報の扱いの差がひどい気がするんですが
- G 君: うーん、メソッド情報はたった2個だし、クラス情報はすごく多くて・・・ 3個もあるんじゃ。
- A 君: え、じゃ、たった1個の差でこんなに扱いの差があるの?MethodInf というクラスもあるのに・・・
- G 君: 2個を許せば1個のクラスも出てくるんじゃ、0個のクラスも。さらに笑い声しかないクラスも。
次に上記のメソッドで呼び出されるメソッド printFileStatus を紹介します。
/**
* print file status
* @param fs File Status
* @param total Total Program or Each Function
* @param bw Buffered Writer to files.csv
* @exception IOException
*/
private void printFileStatus(FileStatus fs, boolean total,
BufferedWriter bw) throws IOException {
if (total) {
bw.write("Total Size, for all files");
} else {
bw.write("File Name, " + fs.getFileName());
}
bw.newLine();
bw.write(", Total Line Number, " + fs.getTotalLines());
bw.newLine();
bw.write(", Operation Line Number, " + fs.getOperationLines());
bw.newLine();
bw.write(", Statement Number, " + fs.getStatementLines());
bw.newLine();
bw.write(", Comment Line Number, " + fs.getCommentLines());
bw.write(checkMetrics(1, fs.getCommentLines(), fs.getTotalLines()));
bw.newLine();
if (total) {
double a1 = ((Double) fs.getFunctions().get(0)).doubleValue();
double a2 = ((Double) fs.getFunctions().get(2)).doubleValue();
double a3 = ((Double) fs.getClasses().get(0)).doubleValue();
double ret = Evaluation.programRate(a1, a2, a3);
bw.write(", Average Statements a Class, " + a3);
bw.write(", Class Number, " + fs.getClasses().get(1));
bw.newLine();
bw.write(", Average Statements a Method, " + a1);
bw.write(", Method Number, " + fs.getFunctions().get(1));
bw.write(", Average Methods a Class, " + a2);
bw.newLine();
bw.write(", Program Rate, " + ret);
bw.write(", Program Quantity, " + ret * fs.getStatementLines());
bw.newLine();
}
}
全体のメトリクスデータのときに実行される a1 〜 a3 を使って表示する部分が、メトリクスデータの統計値になります。
a1 は平均メソッドステートメント数、a2 は平均クラスメソッド数、a3 は平均クラスステートメント数になります。
そして Evaluation.programRate(a1, a2, a3) はこれらの3個の数値を使って評価する「プログラム率」を返します。これはプログラムの「良さ」になります。平均値は1となるように調整しています。
このプログラム率がこのメトリクスツールの眼目になります。まだここでは不明の Evaluation.programRate で計算した結果を持ってくるところから、この評価関数で悪い点を取ったプログラマから、「いちゃもん」と呼ばれているのもここからになります。
- A 君: いちゃもん評価関数が出てきましたね。こそっと。どうなっているの?
- G 君: これは別のクラスじゃ、別のクラスに権限を委譲しておる。
- B 君: ふーん、Evalation なんて名前、付けてると逆コンパイルされてばれまっせ。
後は、片手間に各種の警告も出力するようにしています。例えば、コメント行が一定に比率以下であるときに警告を出します。そこのプログラムを以下に示します。
/** threshold for metrics */
private static int commentThreshold = 20;
/**
* checkMetrics
* @param number Metrics Number
* @param m1 Metrics data 1
* @param m2 Metrics data 2
*/
private String checkMetrics(int number, int m1, int m2) {
switch (number) {
case 1:
if (m2 == 0) return "";
if (m1 * 100 / m2 < commentThreshold)
return ", too few comment";
else
return "";
つまり、閾値と分母、分子を与えて、越えていれば、メッセージを文字列として返すメソッドになっています。
他の警告として、1行も文字数が多すぎると、警告を出すようにしています。
- A 君: ここもいちゃもんぽい
- G 君: ふふふ、switch でいくつも警告パターンがあるようじゃし、ふふふ
- A 君: やーらし、メトリクスツール自分自身はどうなの?
- G 君: もちろん、閾値を調整するんじゃ、自分は引っかからないようにじゃ
今日はメトリクスデータのファイル出力について、書いてきました。 Evaluation.programRate のメソッドを除けば、一通りのメトリクスツールの作成について述べてきました。明日は残りの部分やファイルの出力結果などを報告する予定にしています。
- A 君: 「Evaluation.programRate のメソッドを除けば」って、教えないつもり?
- G 君: いや、それはじゃ、まぁ、なんというかの。色々と実験していちゃもんでなくなるまでじゃ
- A 君: 引数をばらしていますが?まぁいいか
See you again !!
8日目 --- まとめと結果のサンプル
昨日まででソースプログラムの入力、メトリクス計測から結果のファイル出力までを書いてきました。今日は今までのまとめを書いていきます。
まずは自分自身を自分自身で計測してみます。
Total Size, for all files Total Line Number 1562 Operation Line Number 1037 Statement Number 758 Comment Line Number 522
全体の総行数は 1562 行で、命令行数は 1037行、ステートメント数は 758、コメント行は 522 行であることがわかります。但し、ここでコメント行とは、コメントが少しでもある行のことで、コメントだけの行のことを指していません。
500行程度で作成する予定でしたが、命令行数で約 1000行、ステートメントで 約 750 になっています。
- A 君: 500行プログラムとは言えませんねぇ、これでは。
- G 君: まぁ、誤差じゃ・・・、1行の長さやコメント率でいちゃもんつけたりしたからのぉ。
- A 君: でも、もっといちゃもんを付けられるように増やすんでしょう!
次にクラス数やメソッド数、さらにそれぞれの平均ステートメント数などを以下に示します。
Average Statements a Class 61.2 Class Number 13 Average Statements a Method 7.1 Method Number 87 Average Methods a Class 6.7
1クラスの平均ステートメント数は 61.2、1メソッドの平均ステートメント数は 7.1、1クラス当たりの平均メソッド数は 6.7 個であることが分かります。なお、平均メソッド数/クラス×平均ステートメント数/メソッド = 6.7 × 7.1 が1クラスの平均ステートメント数に一致しないのはフィールド変数の初期設定や static 文などのためです。
次にプログラム率とプログラム生産量を見てみます。
Program Rate 0.98 Program Quantity 743.1
プログラム率が 0.98 で、プログラム生産量は 743.1 であることが分かります。プログラム率は 1.00 を中心とした指標ですので、0.98 はほぼ平均的なプログラムであることが言えます。758ステートメントを作成しましたが、このツールの評価では 743 ステートメントとして評価しています。
- A 君: え、自分で評価関数を作っているのに、1.0 以下の評価!
- G 君: まぁ、それはじゃ、まぁ、近い値も出ておるしの
- G 君: 自分自身が 1.0 となるようにリファレンスとしてもおもしろいんじゃが
次にファイル単位の計測結果を見てみます。
File Name jp/co/okisoft/esc/metrics/Evaluation.java Total Line Number 95 Operation Line Number 69 Statement Number 49 Comment Line Number 39 Class Evaluation start line 6 statement number 48 Function setStandardWeight statement number 9 Function setSimpleWeight statement number 5 Function programRate statement number 9 Function programRate statement number 6 Function programQuantity statement number 4 Function programQuantity statement number 3 Function main statement number 3
ファイル Evaluation.java の計測結果です。ファイル単位の総行数などの情報が前述の全体の情報と同じように出力されています。
次にこのファイルで定義されているクラスとして、Evaluation があり、6行目から開始され、48 ステートメントであることがわかります。
最後にこの Evaluation クラスのメソッド関数の名前とステートメント数が示されています。同じメソッド名があるものはオーバーロードされているメソッドになります。
- A 君: こんな感じなですか、秘密の Evaluation は。大きさは大したことはないですね。
- A 君: また SimpleWeight というのは手抜きの匂いがあるような
- G 君: ここの変数を増やすと、何がいいプログラムが分からなくなるんじゃ。Simple はすてきなもんじゃ。
- A 君: でも 0.98 ・・・
このメトリクスツールのプログラムは、自分自身に対して警告は出ませんでした。
もし、コメント率や1行の文字数が長すぎるなどの一般的には複雑すぎるプログラム断片を発見したときには、各種警告を出せるようになっています。
- A 君: ち、警告はなかったのか。あれば面白いのに。
- G 君: メトリクスツールで警告を出されるのはさすがに「恥ずかしい」ことじゃ。
- A 君: 最低限のエチケットということですね。
- G 君: そうじゃ。表面的なことしか見ない、この手のツールで口出しされるのはじゃ。
- G 君: クラス設計、アルゴリズムやデータ構造など、本来突っ込むところはここなんじゃ。
これでメトリクスツール「いちゃもん」の作り方は終了になります。最後まで読んでくださってありがとうございました。
大きく拡張したときには続編も書いていこうと思います。
See you again !!