シグネチャ・サーベイ:
よくわからないコードを
眺める方法

[原文(英語):Signature Survey: A Method for Browsing Unfamiliar Code]


Workshop Position Statement
Software Archeology: Understanding Large Systems
OOPSLA 2001

Ward Cunningham
Cunningham & Cunningham, Inc.

日本語訳:木村 伸一

ときどき、人は規模の大きなソフトウェアに精通しなくてはならないことがある。 たとえばレガシー(遺産的)プログラムのバグ修正、知的財産購入での精査、 特許や著作権にかかわる法的紛争だ。 私の立場は:ソースコード閲覧や調査のためのカスタムツールは精通するのに効率的な手段をたいていは提供してくれるし、 サーバベースのエンジンは便利なカスタムツール構築環境になる、というものだ。 この報告で私はさらに、一対のcgiスクリプトとして実装したそんなツールの例を要約する。

プログラマはコードを構造化するのにさまざまな仕組みを使う。 ときにはこれらはプログラミング環境によって強制されたものだ。Smalltalkのクラスやメソッド、 Javaのパッケージやファイルのように。多彩な構文を用いる言語が、 ブロックや文といった構造をさらに挿入する。 変更の歴史は、コード・リポジトリからも得られるだろう。そしてさらなる構造化 の手がかりがコーディング規約ほか、当の問題に取り上げられた問題領域の所産から 得られるかもしれない。

典型的には、大規模で構造化されたプログラムの閲覧機構は、 一覧からの選択のように単純なものでもそうだが、 なんらかの問い合わせに対する応答として、プログラムのある小さな部分を見せる。 しかし、よくわからないプログラムを調べているとき、人はプログラム全体を一挙に感じとりたいのだ。 この必要に迫られたら、プログラム全体を眺めるためのカスタムツールを書くのがいい。 そういうツールは、cgiスクリプトとして書くとかんたんだ。 サーバ上で走り、コードベース全部を読み込み、そしてコードの要約を単一のhtml文書として報告する。 ここにいくつか、 Java 1.3 ソースコード配布物から作ったそんな要約文書の抜粋がある。


/java/awt/peer

382 ButtonPeer ;;{;}
383 CanvasPeer ;;{}
384 CheckboxMenuItemPeer ;{;}
385 CheckboxPeer ;;{;;;}
386 ChoicePeer ;;{;;;;;}
387 ComponentPeer ;;;;;;;;{;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;}
388 ContainerPeer ;;{;;;;}
389 DialogPeer ;;{;;}
390 FileDialogPeer ;;;{;;;}
391 FontPeer ;{}
392 FramePeer ;;{;;;;;;}
393 LabelPeer ;;;{;;}
394 LightweightPeer ;{}
395 ListPeer ;;{;;;;;;;;;;;;;;;}
396 MenuBarPeer ;;{;;;}
397 MenuComponentPeer ;{;}
398 MenuItemPeer ;{;;;;}
399 MenuPeer ;;{;;;}

/java/awt/print

409 Book ;;{}{}{;{;}{;}{}{;}{}{;}{{"";}{"";};}{;}{;;;;;;{;}}{;}{;;{{;};;}{;}{;}}}
410 PageFormat {;{;;}{;;};}{;;{;}{;};}{;;{;}{;};}{;{;;;;;;"";};}{;{;;;;;;"";};}{;{;}{;};}{;{;}{;};}{}{}{;}{;}{{;}{;}}{;}{;;;;;;}{}{;{;;;;;;;;;;;;;;;;;;;;;;};}}
411 Pageable ;{}{}{{};;;;}
412 Paper ;;{;;;;;;{;;;}{;{;}{;;};}{;}{;;}{;};{;}{;}{;}{;}{;}}
413 Printable ;;{}{}{{};;{};{}{};}
414 PrinterAbortException ;{}{{;}{;}}
415 PrinterException ;{{}{;}}
416 PrinterGraphics ;{}{}{}{;}


このレポートは当該配布物中の1800のソースファイルに対応して1800行ある。 各ファイルごとに一行で(ブラウザが行を折り返しているかもしれないが)、番号と名前と要約がある。 この例では、要約は選ばれた区切り文字以外をすべて省いて作ってある。 私はこれをファイルの「シグネチャ(形跡)」と呼ぶ。区切り文字の量(シグネチャの長さ) はそのファイルのサイズについて直感的な感覚を与えてくれるし、 その区切り文字の並びの中に現れるパターンは複雑さと反復の感じを添える。 この抜粋の元になる完全なレポートはここから取り出せる(400k)。

もちろん、区切り文字シグネチャで提示されるこれほど過激な要約は、 使うのに時間を要する。この紙上の例を見ただけで多くが学べるとは期待しないでほしい。 むしろ、このレポートのための感覚を開発するプロセスが、並行して、点検しているコードに精通していくことと協調するのだ。 それは、以下のステップを繰り返す、対話的なプロセスだ。

  1. 点検. レポートを見て、はっとするほど規則的だったり不規則だったりする構造を探す。 それぞれの(不)規則性に関するソースコードを調べる。

  2. 投影. 要約プログラム(そのレポートを生成するcgiスクリプト)を書き換える。 手作業での調査に相当する点検対象コードの様相を表現するようにする。


点検のステップで、人はコードの全体についての考えを形づくる。 投影のステップでは、人は、そのコードについての行きあたりばったりの実験をもとに、 考えを開拓(または論破)する。プロセスは、人がコードに求める特性をそこそこの努力で見つけ、 理解したときに停止する。

このプロセスには注意を払うべき詳細がいくつもある。 まず、実際にコードを点検していくしくみである。 要約レポートの特性は、それが要約しているコードにハイパーリンクされることにある。 これが要約と詳細のすばやい切り替えを可能にし、視覚的なパターン認識と短期記憶の両方を利用するテクニックを使える。 迅速な、ハイパーリンク付けられたソースコードの取り出しは、ふたつめのcgiスクリプトで行われる。 そのスクリプトはファイル名と行(バイト)オフセットとを引数にとり、それらを使って必要なシステムコールを発行し、 適切なソースコードを目につくほどの遅れもなく取り出す。

同じソースコード取り出しcgiスクリプトは、いくつかすばやく読むための追加的な整形もできる。 以下は、Sunの Java 1.3 ディストリビューションから抽出したjavaソースをhtml整形した抜粋である。 完全なレポートはここから利用できる。 重要なテキストは拡大して強調してある。重要でないテキスト (この事例ではコメントといくつかの区切り「雑音」)は灰色で表示して弱めてある。


/java/awt/image/BandedSampleModel.java

/*
* @(#)BandedSampleModel.java 1.27 00/02/02
*
* Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved.
*
* This software is the proprietary information of Sun Microsystems, Inc.
* Use is subject to license terms.
*
*/

/* ****************************************************************
******************************************************************
******************************************************************
*** COPYRIGHT (c) Eastman Kodak Company, 1997
*** As an unpublished work pursuant to Title 17 of the United
*** States Code. All rights reserved.
******************************************************************
******************************************************************
******************************************************************/

package java.awt.image;

/**
* This class represents image data which is stored in a band interleaved
* fashion and for
* which each sample of a pixel occupies one data element of the DataBuffer.
* It subclasses ComponentSampleModel but provides a more efficent
* implementation for accessing band interleaved image data than is provided
* by ComponentSampleModel. This class should typically be used when working
* with images which store sample data for each band in a different bank of the
* DataBuffer. Accessor methods are provided so that image data can be
* manipulated directly. Pixel stride is the number of
* data array elements between two samples for the same band on the same
* scanline. The pixel stride for a BandedSampleModel is one.
* Scanline stride is the number of data array elements between
* a given sample and the corresponding sample in the same column of the next
* scanline. Band offsets denote the number
* of data array elements from the first data array element of the bank
* of the DataBuffer holding each band to the first sample of the band.
* The bands are numbered from 0 to N-1.
* Bank indices denote the correspondence between a bank of the data buffer
* and a band of image data. This class supports
* {@link DataBuffer#TYPE_BYTE TYPE_BYTE},
* {@link DataBuffer#TYPE_USHORT TYPE_USHORT},
* {@link DataBuffer#TYPE_SHORT TYPE_SHORT},
* {@link DataBuffer#TYPE_INT TYPE_INT} datatypes
*/


public final class BandedSampleModel extends ComponentSampleModel
{

 /**
 * Constructs a BandedSampleModel with the specified parameters.
 * The pixel stride will be one data element. The scanline stride
 * will be the same as the width. Each band will be stored in
 * a separate bank and all band offsets will be zero.
 * @param dataType The data type for storing samples.
 * @param w The width (in pixels) of the region of
 * image data described.
 * @param h The height (in pixels) of the region of image
 * data described.
 * @param numBands The number of bands for the image data.
 * @throws IllegalArgumentException if <code>dataType</code> is not
 * one of the supported data types
 */
 public BandedSampleModel(int dataType, int w, int h, int numBands) {
super(dataType, w, h, 1, w,
 BandedSampleModel.createIndiciesArray(numBands),
 BandedSampleModel.createOffsetArray(numBands));
 }

 /**
 * Constructs a BandedSampleModel with the specified parameters.
 * The number of bands will be inferred from the lengths of the
 * bandOffsets bankIndices arrays, which must be equal. The pixel
 * stride will be one data element.
 * @param dataType The data type for storing samples.
 * @param w The width (in pixels) of the region of
 * image data described.
 * @param h The height (in pixels) of the region of
 * image data described.
 * @param numBands The number of bands for the image data.
 * @param scanlineStride The line stride of the of the image data.
 * @param bankIndices The bank index for each band.
 * @param bandOffsets The band offset for each band.
 * @throws IllegalArgumentException if <code>dataType</code> is not
 * one of the supported data types
 */
 public BandedSampleModel(int dataType,
 int w, int h,
 int scanlineStride,
 int bankIndices[],
 int bandOffsets[]) {

 super(dataType, w, h, 1,scanlineStride, bankIndices, bandOffsets);
 }

 /**
 * Creates a new BandedSampleModel with the specified
 * width and height. The new BandedSampleModel will have the same
 * number of bands, storage data type, and bank indices
 * as this BandedSampleModel. The band offsets will be compressed
 * such that the offset between bands will be w*pixelStride and
 * the minimum of all of the band offsets is zero.
 * @param w the width of the resulting <code>BandedSampleModel</code>
 * @param h the height of the resulting <code>BandedSampleModel</code>
 * @return a new <code>BandedSampleModel</code> with the specified
 * width and height.
 * @throws IllegalArgumentException if <code>w</code> or
 * <code>h</code> equals either
 * <code>Integer.MAX_VALUE</code> or
 * <code>Integer.MIN_VALUE</code>
 * @throws IllegalArgumentException if <code>dataType</code> is not
 * one of the supported data types
 */
 public SampleModel createCompatibleSampleModel(int w, int h) {
 int[] bandOffs;

 if (numBanks == 1) {
 bandOffs = orderBands(bandOffsets, w*h);
 }
 else {
 bandOffs = new int[bandOffsets.length];
 }

 SampleModel sampleModel =
 new BandedSampleModel(dataType, w, h, w, bankIndices, bandOffs);
return sampleModel;
 }

 /**
 * Creates a new BandedSampleModel with a subset of the bands of this
 * BandedSampleModel. The new BandedSampleModel can be
 * used with any DataBuffer that the existing BandedSampleModel
 * can be used with. The new BandedSampleModel/DataBuffer
 * combination will represent an image with a subset of the bands
 * of the original BandedSampleModel/DataBuffer combination.
 * @throws RasterFormatException if the number of bands is greater than
 * the number of banks in this sample model.
 * @throws IllegalArgumentException if <code>dataType</code> is not
 * one of the supported data types
 */
 public SampleModel createSubsetSampleModel(int bands[]) {
if (bands.length > bankIndices.length)
 throw new RasterFormatException("There are only " +
 bankIndices.length +
 " bands");
int newBankIndices[] = new int[bands.length];
int newBandOffsets[] = new int[bands.length];

 for (int i=0; i<bands.length; i++) {
 newBankIndices[i] = bankIndices[bands[i]];
 newBandOffsets[i] = bandOffsets[bands[i]];
 }

 return new BandedSampleModel(this.dataType, width, height,
 this.scanlineStride,
 newBankIndices, newBandOffsets);
 }

 /**
 * Creates a DataBuffer that corresponds to this BandedSampleModel,
 * The DataBuffer's data type, number of banks, and size
 * will be consistent with this BandedSampleModel.
 * @throws IllegalArgumentException if <code>dataType</code> is not
 * either <code>DataBuffer.TYPE_BYTE</code>,
 * <code>DataBuffer.TYPE_USHORT</code>, or
 * <code>DataBuffer.TYPE_SHORT</code>, or
 * <code>DataBuffer.TYPE_INT</code>
 */
 public DataBuffer createDataBuffer() {
 DataBuffer dataBuffer = null;

 int size = scanlineStride * height;
 switch (dataType) {
 case DataBuffer.TYPE_BYTE:
 dataBuffer = new DataBufferByte(size, numBands);
 break;
 case DataBuffer.TYPE_USHORT:
 dataBuffer = new DataBufferUShort(size, numBands);
 break;
 case DataBuffer.TYPE_SHORT:
 dataBuffer = new DataBufferShort(size, numBands);
 break;
 case DataBuffer.TYPE_INT:
 dataBuffer = new DataBufferInt(size, numBands);
 break;
 }

 return dataBuffer;




要約レポートとあわせて、詳細レポートは追加的にレンダリングしたコードへの ハイパーリンクを提示する。この事例では、メソッド名が要約スクリプトへ戻るように ハイパーリンク付けてあるが、追加の検索パラメータがあれば、 要約シグネチャ中に各メソッドの参照指標を要約に追加する。 これらは色の対比でハイライトされているので、長い要約の中で見つけるのもたやすい。

要約と詳細の両方のcgiスクリプトは、大量のhtml出力を作り出す。 私は私のブラウザの持つこれら文書の表示能力に感銘を受け続けている。 (この作業は Netscape version 4.5 を使って行った。) 私は大スクリーンを高度に加工した情報で埋め、 意のままにスクロールさせることができる。 私は、往々にして走らせるのに時間のかかる要約cgiスクリプトを書いた。 私のクライアント-サーバ構成と私のブラウザの部分的な結果を表示する能力で、それでもこの機能を対話的なままにしておけた。 数メガバイトのhtml要約文書に対応する数百メガバイトのソースをスクロールさせながら閲覧しても、応答はがまんできるものだった。

cgiを短く単純に保つのも重要だ。さもないと、人は閲覧実験を十分簡単に試せなくなってしまう。 この理由から、私はよく、前回の版への投資を守ろうとするよりも、スクリプトを毎回新しく書いた。 同様に、たとえスクリプトが行うことが言語の構文解析の観点からは100%正確でなくても、 スクリプトが何をするかが確実に人にわかることも重要だ。 そんなわけで、私はいつもこういうスクリプトを書くのには Perl に頼っている。 Perl には卓越したファイル処理機能・テキスト処理機能がある。 上のような注意を頭に置いて、また試供品として、私は上の例を作るのに使った 要約スクリプト詳細スクリプトを提供する。 これらのスクリプトは、 WikiWikiWeb サーバの拡張として書いたが、 ほとんどのwebサーバのcgiフレームワークで利用できないような重大な機能はなにも使っていない。

それほどではないが、レポートの書式について、私はエドワード・R・タフティ(Edward R. Tufte)に触発された。 彼は情報の視覚的表現に関する本のシリーズで有名である。 とはいえ、彼の助言の多くは紙面にできるだけ反映したし、大量の文書と迅速なスクロールをがまんできるものに置き換えるべきことを知った。 私は、単一の表示が複数のスケールレベルで情報を提供するという彼の主張には特に触発された。 私たちがプログラミングツールを大規模プログラムの調査に使うとき、 私たちはあまりにも早く、著者やコンピュータが使っている「一度に一命令のモード」に引き寄せられる。 この紙上で述べたカスタム閲覧ツールはこの顕微鏡的な見方から逃れさせてくれる。そしてその代わりに多くの同時並列的な展望のレベルを提供してくれる。




ward@c2.com © 2001 [原文(英語):Signature Survey: A Method for Browsing Unfamiliar Code]
Japanese Translation: , OKI Software Co.,Ltd. 2007
[紹介ページに戻る]