メンバページ: Go
汎用メトリクスツール「いちゃもん」の読み方
How to read 'General-Purpose Metrics Tool "Fuss"'
このページでは、C/C++/C#/Java に対応する汎用的なメトリクスツール Ichamon の計測結果の読み方を紹介します。
- A 君: 計測結果の根拠がほしいって
- G 君: 「すべてはソースコードに書いてある」、挨拶は不要じゃ
- A 君: って言ってもねぇ、やぱ、書かない?
- G 君: そうじゃのぉ、そうするかのぉ、実は評価関数もいくつかあるしのぉ
| 1日目 | JavaApplet 版メトリクスツール |
| 2日目 | 評価関数と警告 (Ichamon の読み方と歩き方) |
| 3日目 | Ichamon の修正 |
| 4日目 | コマンドライン版の使い方 |
| 5日目 | 拡張のコツ |
1日目 --- JavaApplet版 メトリクスツール Ichamon
まずは、メトリクスツールの JavaApplet 版を以下に置きます。
| ソースプログラム |
| メトリクス |
上のテキストエリアにソースコードを貼り付けます。そのコードが C または C++ であれば、C/C++ のボタンを クリックします。Java または C# であれば、Java/C# のボタンをクリックしてください。 下のテキストエリアに計測結果が表示されます。
ここで使用している評価関数はボタンにも書いてありますが、2004/09/15 版のものです。以下に主な評価値の説明をします。
- ProgramRate --- プログラムの評価で、1.0 を基準として、高い評価のときは数値が大きくなります。
- ProgramQuantity --- プログラムの生産量で、 ステートメント×プログラム率で計算しています
- A 君: え、いちいち、貼り付けるんですか、プログラムを
- G 君: まぁ、試用じゃ、仕様じゃ、使用じゃ
- A 君: 何、言ってるんですか
次に以前の評価関数である 2004/09/07 版で評価するものをここに置きます。
違いを強調するために GUI を AWT で記述しています。
- A 君: それじゃ、次は SWT 版もあるってことですね
- G 君: そして GUI Toolkit がなくなってしまえば、評価関数とともに作らんとな、まぁ、専用に作ればいいか
- A 君: え、本気
明日は、本日紹介しました二つのメトリクスの評価関数について書いていく予定です。
See you again !!
2日目 --- 評価関数と警告 (Ichamon の読み方と歩き方)
今日は評価関数について書いていきます。まずは 2004/09/07 版の評価関数について記述します。
最後に、メトリクスツールの読み方について書きます。
2004/09/07 版評価関数
Java/C# 版評価関数
対外発表している評価関数をそのまま実装したものになります。以下に評価関数式を示します。
プログラム生産率= ((1.0 - dev1 + 0.5) ** 係数1)
× ((dev2 + 0.5) ** 係数2)
× ((1.0 - dev3 + 0.5) ** 係数3)
dev1: ステートメント数/ メソッドの偏差値
dev2: メソッド数/ クラスの偏差値
dev3: ステートメント数/ クラスの偏差値
偏差値計算のために平均や分散は、計測対象でない別のサンプル集団のものを使用
係数1〜3: 各値の重み付け
** はべき乗を表す
この式は、バグを多数内在する品質の悪いプログラムの傾向性として、以下の特徴、相関関係があるなどの観測結果から導出しています。
低生産量のプログラムは、
1) 1メソッド当たりのステートメント数が多すぎる
2) 1クラス当たりのメソッド数が少なすぎる
3) 1クラス当たりのステートメント数が多すぎる
評価関数の注意点として、計測対象のプログラムと偏差値を出すための標本集団が異なりますので、イリーガルな値が出ることがあります。例えば、マイナスの値が算出されることがあります。
- A 君: これが評価関数ですか
- G 君: そうじゃ。対外発表用にシンプルにしてあるがのぉ
- A 君: ふーん、そうなんですか。それでもちょっとシンプルすぎない?
- G 君: まぁ、サイクロマティック数なんかを入れるのはいいんじゃが、それよりもクラスじゃ。
C/C++ 版評価関数
C/C++ 版の評価関数は、Java/C# 版の評価関数で、dev1 のみに注目したものです。つまり dev2 や dev3 は計測せずに 1関数(メソッド関数)当たりの平均ステートメント数からの評価になります。C はクラスによる明示的なモジュール分割がありませんので dev1 のみを注目することにしました。C++ は Java や C# と比較して、C 的なクラス設計のプログラムが多く、評価も C と同じようにしました。
2004/09/15 版評価関数
この評価関数は 2004/09/07 の評価関数に改良を加えたものです。この改良は係数の値、標本集団の平均値、分散の値の変更だけでなく、式そのものを変更しています。しかし基本的な考え方は変更していません。つまり、プログラムの品質の評価(バグ件数など)との相関関係を大きくするようにしています。
具体的には dev2(クラス当たりのメソッド数の偏差値)の扱いの変更や、マイナスの数値にならないようにしたなどがあります。
完全な計算式は、次の改良した評価関数を策定したときに公開するように考えています。
- A 君: どうして指針だけで式そのものを公開しないの?
- G 君: 秘密じゃ、または微密じゃ。対策ツールが出る可能性もあるし。
- A 君: まさか。でも、昔、命令行数(ステップ)増加ツールを作ったことがあるとか・・・
- A 君: 別名、「究極の生産性向上ツール」と呼ばれていたとか
- G 君: a = b + c の式を whitespace を入れていい箇所にすべて自動挿入するものじゃ。
- G 君: a [改行] = [改行] b [改行] + [改行] c [改行]で、1命令行が 5命令行になり、ステップ生産性が5倍になるのじゃ。
- A 君: また FPA 自動増加ツールも作ったとか。
- G 君: これは指針だけでツール化しておらん。
- G 君: 「〜し、〜する」は必ず別の Function にするんじゃ、例えば「読み飛ばす」は「読む」「飛ばす」じゃ。
- G 君: これは UPA でも使えるんじゃ。
- A 君: っていうのが非公開な訳ですか。
- G 君: 例えば、盗作チェックのための類似度チェックプログラムがそうじゃ。
- G 君: このアルゴリズムが公開されれば、それを括りぬけるコピーツールができるんじゃ。
この評価関数に従ってプログラミングするのではなく、例えば「美人プログラミング」を目指してプログラムすれば、自然に高い評価が得られます。
評価関数の実装はアダプタブルにして、今後も変更をしていく予定にしています。
警告
このメトリクスツールではいくつかの警告を発します。プログラム生産率も 1.0 よりも小さいときは警告の一つになります。
以下の警告や計測結果に注意するようにしてください。
- コメント率 --- ドキュメンテーションコメントとプログラムコメントの合計の率が小さいときに警告を発します
- 1行の文字数 --- 1行の文字数が多いときに警告を発します
- 1メソッド当たりのステートメント数 --- 警告は発しませんが、 100 個(または 50個)を越えるときは警告レベルになります。ここの数値が高いとプログラム清算率も低くなります。
なお、警告はこれからも増加する予定です。
Ichamon の読み方と歩き方
警告やステートメント数が大きいものは、チェックするようにしてください。警告をなくすのは最低限のエチケットみたいなものです。
特別な理由もないのに プログラム生産率が 1.0 未満であるときは、普段のプログラミングを見直してください。自動生成されたコードが原因とか、流用したプログラムが原因であるなどは仕方がないでしょう。上記にも書きましたが、この評価関数に従ってプログラミングするのではなく、例えば「美人プログラミング」を目指してプログラミングしてください。
- A 君: 警告は無視しないでということですね
- G 君: それと大きすぎる関数はものは駄目ということじゃ
- G 君: 逆に言えばじゃ、このような表面的な構文チェックレベルで引っかかるようでは、プログラムは駄目じゃ。
このチェックはまだまだ第一次審査、書類審査のようなもので、本審査はプログラムの内容になります。
それでは、みなさん、楽しみながら、プログラミングしていきましょう。
See you again !!
3日目 --- Ichamon の修正 (2004-10-14)
Ichamon は汎用メトリクスツールですが、一部 C++ で合致しない動作がありました。そこを修正しました。上記のアプレット版では修正されています。
具体的には C++ のクラス定義後のクラス定義外での関数定義の処理が正しく動作していませんでした。これを修正しました。
See you again !!
4日目 --- コマンドライン版の使い方
まずは機能追加1件とバグフィックス2件を行いました。
機能追加は、コンソール版の Ichamon ですが、ファイルタイプを見て、C や C++ であれば、simple 版評価関数を使い、 Java や C# であれば標準版評価関数を使うようにしました。
バグフィックスですが、内部クラスでない、兄弟クラスがあるときに、クラス当たりのステートメント数が間違うという バグを修正しました。さらに JavaApplet 版のときに、スレッドセーフにしました。
- A 君: ちょこ、ちょこと直しているのですね、バグ報告が入ったのでしょうか。
- G 君: 実はそうなのじゃ。前回の3日目には我々が登場しておらんし、今回もばっくれようとしたんじゃが。
- A 君: まぁ、そう言わず。2005年になったんだし。
- A 君、G君: あけおめ、じゃない、明けましておめでとうございます。本年も読んでもらえれば嬉しく思います。
このページに JavaApplet 版の Ichamon を貼り付けていますが、この Ichamon にはコマンドライン版の機能も入っています。
というよりも、コマンド版の機能を削除していません。
使い方は以下のようになります。
>java -cp Ichamon.jar jp.co.okisoft.esc.metrics.Ichamon *.java >
上記の計測結果は files.csv に出力されます。また exit コードとして、プログラム量を返しています。
>java -cp Ichamon.jar jp.co.okisoft.esc.metrics.Ichamon -print test.c File is ./test.c Function: test 0 - 2 (3) a1 = 3.0 a2 = 1.0 a3 = 0.0 dev1 = 0.6384393063583815 dev2 = 0.3278592375366569 dev3 = 0.6807692307692308 rate = 1.1128366204464937 programRate 1.1128366204464937 programQuantity 3
上記は print オプション付きで test.c を計測してみました。
>java -cp Ichamon.jar jp.co.okisoft.esc.metrics.Ichamon -help
Metrics tool for Java/C#/C++/C
[-simple][-print][-help] file*
output metrics data to "files.csv"
-simple --- for C++, C, and so on
(default) --- for Java, C#, and so on
-print --- prints metrics data
-help --- prints this help
ヘルプの表示です。
Ichamon.jar は ここ からもリンクしました。 保存して上記を実行してください。
このページを表示してアプレットが表示されているということは、既にダウン ロードされていることになりますが、37 KB の小さなものですので、 気楽にダウンロードしてください。
注意: WindowsXP SP2 をインストールしたマシンでは、zip 互換の圧縮ファイルは ファイルタイプが zip になりますので、 上記の例では zip に変えて実行してください。
See you again !!
5日目 --- 拡張のコツ
今日は Ichamon の拡張のコツについて、書いていきます。
Ichamon で拡張される可能性の高い部分として、評価関数とプログラムステータスの部分を考慮しています。
評価関数の拡張
評価関数については、拡張というよりも「改善」「改良」になる部分が多いかも知れませんが、 この機能だけのクラスを独立したクラスにすることにより、拡張を容易にしています。
public static double programRate (double a1, double a2, double a3) {
のように、基本である Evaluation$programRate と、そのカスタマイズである EvaluationCustom$programRate を継承関係にしています。まだインタフェースは用意していません。
今回のカスタマイズは、パラメータを変更せずに、評価関数の機能カスタマイズだけなので、継承ではなく委譲でも良かったでしょう。
しかし、評価関数では、パラメータ変更なども入る可能性がありますので、継承にして、クラス全体をカスタマイズ対象にした方が便利であるでしょう。
実際に、二つの評価関数を用いています。これからも、さらにパラメータ込みの評価関数を用意することになります。このときは、インタフェース+継承で作成するように考えています。
- A 君: つまり、これからも基本評価関数を継承して、改良版を作っていきますということですか。
- G 君: そうじゃ。また継承の継承の継承とかも、あるかも知れん。試練じゃ。
- A 君: (無視)
プログラムステータスの拡張
この Ichamon では、字句解析と、超簡単な(または手抜きの)構文解析を行っています。
メトリクスを新たな視点で計測することが必要になってきたときに、構文解析部を直接変更するのではなく、構文解析部とコルーチンのように動作するクラス「プログラムステータス」を構文解析から、独立して設けました。
このクラスは興味ある構文が来れば、対応するメソッドが呼び出されるようになっています。例えば、"class { }" というクラス定義構文に興味を持ったとすると、class が来たときに、それに対応するプログラムステータスのメソッド classID が呼び出されるようになります。またユーザ関数定義に興味を持ったとすると、メソッド userDefinedFunction が呼び出されるようにしています。
これにより、構文解析部に興味ある構文の一部を示し、それに対応するプログラムステータスのメソッドを登録することにより、拡張が容易に行えます。例えば、if 文に興味を持ったとすると、それに対応するプログラムステータスのメソッドを用意し、そこで収集した情報を解析して蓄積することができます。
- A 君: 自慢なんですか、結局
- G 君: いや、言い訳じゃろ、次を読めば分かるて。
しかし、構文解析部は手抜きして作ってあり、かつ上記のようにその処理部である「プログラムステータス」クラスを独立させたために、ルックアップとそのフック関数呼び出しのみになり、少ない量になりましたので、字句解析部の一部に含むようにしています。
これは拡張性を低下させています。例えば、C#, Java, C のような構文規則が似ているプログラム言語に対しては問題ありませんが、構文規則が大きく異なる言語、例えば、FORTRAN や Lisp, Python などに対しては、拡張を困難なものにしています。字句解析部から構文解析部の癒着部分を切り離して、独立したクラスにする必要があるでしょう。
- A 君: なるほど、いいことを先に書いて、悪いことを後に書く。つまりは悪いのだと。
- G 君: そうではない。先においしいものを食べるのじゃ。後では満腹で食べられないかも知れんて。
- A 君: まぁ、プログラムステータスだけを変更すればいいということですね。
- G 君: それと構文解析部で、そのルックアップとフック登録だけ。
付録として、プログラムステータスのプログラムを掲載します。(インデントは、ハードウェアタブを用いていますので、Web ページでは、ずれて表示されています。)
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;
/**
* construct program status
*/
ProgramStatus () {
functions = new ArrayList();// function inf.
curlLevel = 0; // Curl Bracket level
lineMode = empty; // current line mode as empty line
operationLines = 0; // total operation line number
statementLines = 0; // total statement line number
totalLines = 0; // total source file line
commentLines = 0; // comment line number
emptyLines = 0; // emptyLine number
functionSymbol = null; // current function symbol
functions = new ArrayList(); // function status ArrayLists
currentClassCurlLevel = -1;
classInf = new ArrayList();
}
/**
* regist file name
* @param fileName File name
*/
public void registFileName (String fileName) {
this.fileName = fileName; // file name
}
/**
* open curl bracket
*/
public void openCurl (String symbol) {
if (curlLevel == 1 + currentClassCurlLevel && symbol != null) {
userDefinedFunction(symbol);
}
curlLevel++;
return;
}
/**
* close curl bracket
*/
public void closeCurl () {
curlLevel--;
// for class
for (int i = 0, size = classInf.size(); i < size; i++) {
ClassInf classInformation = (ClassInf) classInf.get(i);
if (classInformation.curlLevel == curlLevel) {
if (!classInformation.setStatements) {
classInformation.startStatement
= statementLines - classInformation.startStatement;
classInformation.setStatements = true;
if (Ichamon.print) {
System.out.print("Class " + classInformation.className);
System.out.print(" definition end");
System.out.print(" at " + statementLines);
System.out.println(" in " + classInformation.startStatement);
}
}
}
}
// for method
if (functionSymbol == null) return;
if (startLevelFunction == curlLevel) {
functions.add(new Integer(statementLines - startFunction));
functionSymbol = null;
if (Ichamon.print) {
System.out.print(" - " + (statementLines - 1));
System.out.print(" (" + (statementLines - startFunction));
System.out.println(")");
}
}
return;
}
/**
* line feed
* @param emptyLine The ine with line feed is emmty line ?
*/
public void lineFeed (boolean emptyLine) {
totalLines++;
lineFeedFinal(emptyLine);
return;
}
/**
* line feed Final at last line
* @param emptyLine The ine with line feed is emmty line ?
*/
public void lineFeedFinal (boolean emptyLine) {
if (!emptyLine) operationLines++;
if (emptyLine) emptyLines++;
if (lineMode == lineComment || lineMode == blockComment) commentLines++;
if (lineMode == lineComment) lineMode = empty;
return;
}
/**
* semiColon
*/
public void semiColon () {
statementLines++;
return;
}
/**
* line comment in
*/
public void lineCommentIn () {
lineMode = lineComment;
return;
}
/**
* block comment in
*/
public void blockCommentIn () {
commentLines++; // starting block comment self
lineMode = blockComment;
return;
}
/**
* block comment out
*/
public void blockCommentOut () {
lineMode = empty;
return;
}
/**
* user defined function
* @param function Function name
*/
public void userDefinedFunction (String function) {
if (functionSymbol != null) return;
functionSymbol = function;
startFunction = statementLines;
startLevelFunction = curlLevel;
functions.add(function);
if (Ichamon.print) {
System.out.print("Function: " + functionSymbol);
System.out.print(" " + statementLines);
}
return;
}
/**
* symbol name
* @param name symbol name
*/
public void symbolName (String name) {
if (statementMode == classMode
&& Ichamon.mode == "normal") {
classInf.add(new ClassInf(name,
totalLines,
statementLines,
curlLevel,
fileName));
currentClassCurlLevel = curlLevel;
if (Ichamon.print) {
System.out.print("Class: " + name);
System.out.println(" at " + statementLines);
}
}
statementMode = normalMode;
return;
}
/**
* class id
*/
public void classId () {
statementMode = classMode;
return;
}
/**
* special form
* @param symbol special forma identifier
*/
public void specialForm (String symbol) {
statementLines++;
return;
}
/**
* save program status
*/
public void saveStatus () {
FileStatus fileStatus = new FileStatus(fileName,
totalLines,
operationLines,
statementLines,
commentLines,
emptyLines,
functions,
classInf);
files.add(fileStatus);
}
/**
* total count
*/
static public FileStatus totalStatus () {
int totalLines =0;
int operationLines = 0;
int statementLines = 0;
int commentLines = 0;
int emptyLines = 0;
int classNumber = 0;
int methodNumber = 0;
int classStatements = 0;
int methodStatements = 0;
for (int i = 0, size = files.size(); i < size; i++) {
FileStatus fs = (FileStatus) files.get(i);
totalLines += fs.getTotalLines();
operationLines += fs.getOperationLines();
statementLines += fs.getStatementLines();
commentLines += fs.getCommentLines();
emptyLines += fs.getEmptyLines();
// class
ArrayList classes = fs.getClasses();
for (int j = 0, jsize = classes.size(); j < jsize; j++) {
classNumber++;
classStatements += ((ClassInf) classes.get(j)).startStatement;
}
// method
ArrayList methods = fs.getFunctions();
for (int j = 0, jsize = methods.size(); j < jsize; j += 2) {
methodNumber++;
if (j + 1 >= jsize) {
break;
}
methodStatements += ((Integer) methods.get(j + 1)).intValue();
}
}
// for class average and method average
double average = 0.0;
double averageM = 0.0;
if (classNumber != 0) average = classStatements * 1.0 / classNumber;
if (methodNumber != 0) averageM = methodStatements * 1.0 / methodNumber;
ArrayList classAverage = new ArrayList();
classAverage.add(new Double(average));
classAverage.add(new Integer(classNumber));
ArrayList methodAverage = new ArrayList();
methodAverage.add(new Double(averageM));
methodAverage.add(new Integer(methodNumber));
if (classNumber == 0) classNumber = 1;
methodAverage.add(new Double(methodNumber * 1.0 / classNumber));
return new FileStatus("Total", totalLines, operationLines,
statementLines, commentLines, emptyLines,
methodAverage,
classAverage);
}
/**
* print program status
*/
public void printStatus () {
println("Total lines : " + totalLines);
println("Operation lines : " + operationLines);
println("Statement lines : " + statementLines);
println("Comment lines : " + commentLines);
println("Empty lines : " + emptyLines);
for (int i = 0, size = classInf.size(); i < size; i++) {
ClassInf classInformation = (ClassInf) classInf.get(i);
println("Class " + classInformation.className +
" : statement number = " + classInformation.startStatement);
}
for (int i = 0, size = functions.size(); i < size; i += 2) {
println("Function " + functions.get(i) + " : statement number = " +
functions.get(i + 1));
}
}
/**
* print string with newLine
* @param string String for output
*/
private static void println(String string) {
System.out.println(string);
}
/**
* print newLine
*/
private static void println() {
System.out.println();
}
/** function size threshold */
private static int maxFunctonSizeThreshold = 50;
/** Class information */
class ClassInf {
String className;
int startLine;
int startStatement;
int curlLevel;
String fileName;
boolean setStatements;
/** 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;
this.setStatements = false;
}
}
/** 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;
}
}
}
- A 君: うぁ、長!
- G 君: それにフィールド変数が多すぎるのぉ。いくつかにまとめて、クラスにするべきだの。
- A 君: って、わかってて、やらないのですか
- G 君: いや、Visual Basic 対応という話もあるから、そのときにでも・・・
See you again !!