asatoの技術的な日常日記

「成長に最大の責任をもつ者は、本人であって組織ではない。自らと組織を成長させるためには何に集中すべきかを、自らに問わなければならない」  非営利組織の経営 - ピーター・ドラッカー

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

ソフトウェア進化:進化ユニット

更新履歴
2008.02.20:図を追加。
2008.02.20:分かりにくい部分を少し修正。

はじめに


要求仕様の変更や保守性の向上など品質特性の変更に対応するために、ソフトウェアの変更が必要な場合があります。通常、以下の理由のため、ソフトウェアの変更は集団的、つまり連続的な変更です。

 (1)仕様変更や品質特性の変化の粒度の単位が、プログラムの粒度の単位と一対一で対応することは少ない。

req_and_program_gra2.png


 (2)ソフトウェアを構成する要素は、独立ではなく互いに依存しあっているため、ある要素の変更は他の要素の変更を要求する。
program_depend2.png


ここでは、要素間の依存関係に伴う変化を考えます。

明示的な依存関係に伴う変化:1つは、プログラム要素間に形式的な依存関係がある場合です。その場合、依存されている側の要素を変更すると、その要素に依存している要素にも変更が及びます。たとえば、変数名を変更すると、その変数を参照しているコード要素も変更が必要です。変更が要求されるかどうかは、コンパイラやインタプリタ(もしくは依存関係を形式的に定義し、検証できるシステム)がエラーを出すかどうかで分かります。

特徴は、依存関係の定義は、形式的かつ客観的であるため、どんな変更が必要なのかがわかることです。

暗黙的な依存関係に伴う変化:プログラム要素間に明示的な依存関係がないのにもかからわず、あるプログラム要素を変更するとき、その変更に伴って他の要素を変更する場合があります。この種の変化を、暗黙的な依存関係に伴う変化と呼ぶことにします。

この記事では、暗黙的な依存関係に伴う変化に対して考察してみます。特に、暗黙的な依存関係に伴って変化するコード群を、ここでは 進化ユニットと呼びたいと思います。この進化ユニットの観点から考察します。

進化ユニットの定義は今のところ曖昧です。ユニットという言葉が適切かどうかもわかりません。しかし、設計者もしくはプログラマーがなぜこの種の変更を行うのかを原理などの観点から適切に説明するのは困難であると感じています。

実例



実例を紹介します。次のようなコードがあるとします。ゲーム画面を描画するコードです。

コード状態A:


public class StatusViewDrawer implements GameViewDrawer {

// フィールド
public void draw(Graphics g) {
// .. コード

// ここから、
drawStringWithShadow(g, "武器  ", 250, baseY + 30);
if (player.getStrItem() instanceof NullStrItem == false) {
drawStringWithShadow(g, strItem.getName(), 360, baseY + 30);
}

drawStringWithShadow(g, "防具  ", 250, baseY + 60);
if (player.getDefItem() instanceof NullDefItem == false) {
drawStringWithShadow(g, defItem.getName(), 360, baseY + 60);
}

drawStringWithShadow(g, "人工精霊", 250, baseY + 90);
if (player.getSpiritItem() instanceof NullSpiritItem == false) {
drawStringWithShadow(g, spiritItem.getName(), 360, baseY + 90);
}
// ここまでが進化ユニット

// .. コード&その他のメソッド
}
}

public class NullStrItem extends StrItem { // Null Object

// ... その他のメソッド

@Override
public int getStrPoint() {
return 0;
}
}

public class NullDefItem extends DefItem {
// ... 省略
}

public class NullSpiritItem extends SpiritItem {
// ... 省略
}



次の図は、上記のコードによって描画されたゲーム画面を表します。
suisei_status_view2.png


NullStrItem といったクラス名から想像できるように、このコードでは Null Object パターンが適用されています。 ここで、Null Object パターンが適用されているのに、あるオブジェクトが Null Object かどうかを判別するコードがあるのは良くない兆候だと考えたとします(実際にこれが適切な判断かどうかは後の議論に関係ありません)。



if (player.getStrItem() instanceof NullStrItem == false) { // Null Object かどうか
drawStringWithShadow(g, strItem.getName(), 360, baseY + 30);
}



そのため、次のようにコードを変更することにしました。

コード状態B:


public class StatusViewDrawer implements GameViewDrawer {

// フィールド
public void draw(Graphics g) {
// .. コード
// ここから、

drawStringWithShadow(g, "武器  ", 250, baseY + 30);
drawStringWithShadow(g, strItem.getName(), 360, baseY + 30);

drawStringWithShadow(g, "防具  ", 250, baseY + 60);
drawStringWithShadow(g, defItem.getName(), 360, baseY + 60);

drawStringWithShadow(g, "人工精霊", 250, baseY + 90);
drawStringWithShadow(g, spiritItem.getName(), 360, baseY + 90);

// ここまでが変更された

// .. コード&その他のメソッド
}

}

public class NullStrItem extends StrItem {
// ... 他のメソッド
@Override
public int getStrPoint() {
return 0;
}

@Override
public String getName() {
return "";
}

}

public class NullDefItem extends DefItem { // 同じように getName を追加
// ... 省略
}

public class NullSpiritItem extends SpiritItem { // 同じように getName を追加
// ... 省略
}



code_state_a_b.png


コード状態AからBへの変更の過程は上の図に示しめしている通りです。メソッドレベルの変更粒度で見た場合、変更過程は次のようになります。ただし、順番は考慮しません。

 (変更a) NullStrItemクラスへgetNameメソッドを追加

 (変更b) NullDefItemクラスへgetNameメソッドを追加

 (変更c) NullSpiritItemクラスへgetNameメソッドを追加

 (変更d) StatusViewDrawerクラスのdrawメソッドを変更


疑問



ここでの疑問は、なぜこのような変更が起こったのか、です。このような変更過程を引き起こす要因は何か、ということです。たとえば、NullStrItem クラスに getName を追加したからといってなぜその他の NullDefItem と NullSpiritItem も変更したのでしょうか?

この集団的な変更は、明示的な依存関係によって発生したものではないため、変更する人によっては異なる変更結果となりえます。たとえば、NullStrItem クラスにだけ getName を追加し、描画のコードも次のようにだけ変更される可能性もあります。

コード状態C:


public class StatusViewDrawer implements GameViewDrawer {

// フィールド
public void draw(Graphics g) {
// .. コード
// ここから、
drawStringWithShadow(g, "武器  ", 250, baseY + 30);
drawStringWithShadow(g, strItem.getName(), 360, baseY + 30);
// ここまでが変更された


drawStringWithShadow(g, "防具  ", 250, baseY + 60);
if (player.getDefItem() instanceof NullDefItem == false) {
drawStringWithShadow(g, defItem.getName(), 360, baseY + 60);
}

drawStringWithShadow(g, "人工精霊", 250, baseY + 90);
if (player.getSpiritItem() instanceof NullSpiritItem == false) {
drawStringWithShadow(g, spiritItem.getName(), 360, baseY + 90);
}
// .. コード&その他のメソッド
}
}

public class NullStrItem extends StrItem {
// ... 他のメソッド
@Override
public int getStrPoint() {
return 0;
}

@Override
public String getName() {
return "";
}

}



code_state_a_b_c.png



考察



NullStrItem に対して、getName をオーバーライドして定義しましたが、そのメソッドの導入が次のコードの変更を示唆することは分かりません。

変更前:

    drawStringWithShadow(g, "武器  ", 250, baseY + 30);    

if (player.getStrItem() instanceof NullStrItem == false) {
drawStringWithShadow(g, strItem.getName(), 360, baseY + 30);
}



変更後:

   drawStringWithShadow(g, "武器  ", 250, baseY + 30);
drawStringWithShadow(g, strItem.getName(), 360, baseY + 30);


また、NullStrItem に対する、getName メソッドの追加が、NullDefItem への getName の追加を示唆することは分かりません。

示唆がないのに、どうしてこのような変更を行うのでしょうか?


たとえば、次のようなシナリオを考えます。上記のようにコードを変更しようと思ったプログラマAと、変更の一部をまかされたプログラマBがいると考えてください。

次に、プログラマAが、次のコード状態Xから、コード状態Yに変更したとします。目標となるコード状態はコード状態Bだとします。
コード状態X:


public class StatusViewDrawer implements GameViewDrawer {

// フィールド
public void draw(Graphics g) {
// .. コード

drawStringWithShadow(g, "武器  ", 250, baseY + 30);

if (player.getStrItem() instanceof NullStrItem == false) {
drawStringWithShadow(g, strItem.getName(), 360, baseY + 30);
}

drawStringWithShadow(g, "防具  ", 250, baseY + 60);
if (player.getDefItem() instanceof NullDefItem == false) {
drawStringWithShadow(g, defItem.getName(), 360, baseY + 60);
}

drawStringWithShadow(g, "人工精霊", 250, baseY + 90);
if (player.getSpiritItem() instanceof NullSpiritItem == false) {
drawStringWithShadow(g, spiritItem.getName(), 360, baseY + 90);
}

// .. コード&その他のメソッド
}
}

public class NullStrItem extends StrItem { // Null Object

// ... その他のメソッド

@Override
public int getStrPoint() {
return 0;
}
}




コード状態Y:


public class StatusViewDrawer implements GameViewDrawer {

// フィールド
public void draw(Graphics g) {

// ..その他の コード

drawStringWithShadow(g, "武器  ", 250, baseY + 30);

if (player.getStrItem() instanceof NullStrItem == false) {
drawStringWithShadow(g, strItem.getName(), 360, baseY + 30);
}

drawStringWithShadow(g, "防具  ", 250, baseY + 60);
if (player.getDefItem() instanceof NullDefItem == false) {
drawStringWithShadow(g, defItem.getName(), 360, baseY + 60);
}

drawStringWithShadow(g, "人工精霊", 250, baseY + 90);
if (player.getSpiritItem() instanceof NullSpiritItem == false) {
drawStringWithShadow(g, spiritItem.getName(), 360, baseY + 90);
}
// .. コード&その他のメソッド
}

}

public class NullStrItem extends StrItem {
// ... 他のメソッド
@Override
public int getStrPoint() {
return 0;
}

@Override
public String getName() { // この部分を追加
return "";
}

}



次に、プログラマBはプログラマAと、直接的・間接的に、会話もしくは文書によりコミュニケーションをとっていないとします。

次に、プログラマBにコード状態Yが渡されたとします。プログラマBは、コード状態Bに到達するでしょうか?


分かるのは、プログラマBはプログラマAからの何らかの情報が必要だということです。ここでの疑問は、その情報が形式的にどのような情報なのか、ということです。つまり、単に「このコードをこんな感じにこうやって変更しておいて」というような曖昧なやりとりではないということです。

今のところの仮説としては、以下のものです。

 (仮説1)コードの構造、もしくはより抽象的な設計の構造が存在するとき、その構造には、変更対する変更ルールが存在する。

 (仮説2)プログラム要素は、このような変更ルールとの依存関係がある。

 (仮説3)プログラム要素と変更ルールとの関係が、進化ユニットを形成する。


設計の構造とは、ここでは、デザインパターンレベルの抽象度の構造をさします。上記の例では、Null Object パターンを適用しています。

変更ルール(抽象度の低い場合には、コーディングルール、高い場合には設計ルール)が存在するのは当然だと思われるかもしれません。設計書が必要な理由もそこにあるとも言えます。しかし、次のような基本的なことはそれほど明らかではありません。

 (A)ある(部分的な)設計構造が与えられたとき、どのような変更ルールが存在しうるのか。

 (B)そしてその変更ルールはどのような原理・原則に基づいているのか。

たとえば、あるデザインパターンが適用された設計構造があるとして、その設計構造の変化にはどのような種類があるのでしょうか? 具体的は、たとえば、Factory パターンが適用された構造に対してどのような変化が起こりうるのでしょうか? 1つは、concrete factory の役割を持つクラスの追加です。

変更前:
add_concrete_factory_before.jpg


変更後:
add_concrete_factory_after.jpg


Factory パターンのような良く知られたパターンに対してさえ、どのような変更が存在し、その変更がどのような変更ルールに従っており、そしてまたそれらの変更ルールがどのような原理・原則から発生するのかはわかっていません。


まとめと今後の課題



この記事では、進化のユニット という概念を提案しました。進化ユニットが形成される仮説を議論しました。今後は仮説の検証を行います。

スポンサーサイト

ソフトウェア進化:進化の単位と進化のパターン化

一つ前の記事 では「Javaでのコードの進化のパターン」ということで「列挙型への定数の追加」というのを紹介した。

このパターンは、コードの変化だけを書くならこんな感じで難しくはない。

before:

public enum EnumType {
TypeA,
TypeB
}



after:

public enum EnumType {
TypeA,
TypeB,
TypeC
}




この進化パターンに限らず、三年以上こんな進化パターンを収集し続けてきた僕が悩んでいる問題の一つに 進化の単位 の扱いというのがある。

先ほどの「列挙型への定数の追加」の進化パターン例を考えてみても、これだけで進化が終わるのではない。途中経過に過ぎない。つまり、何らかの要求変化(機能追加や変更など)があったとして、この進化パターンだけでこの要求変化に対応できることはほとんど考えられない。考えられない理由は以下の点のため。
(1)定数の追加の処理の追加ではない
(2)追加した定数を参照するコードを追加していない。

ほとんど したのは、もしかするとリフレクションを用いて列挙型の情報を読み取ってなんとかする場合があるかもしれないため。

とりあえず、言いたいことを図で表すとこうなる。

evo_enum.png

実際には、次の図の場合がほとんどだと思う。

evo_enum2.png



さて、以上のことを踏まえて、今回の記事の本題に戻りたい。ここで、進化単位をどう取り扱えばいいだろう?

いくつかの候補が考えられる。
(1)ある要求の変化を満たすコードの変化

(2)僕の提案している進化パターンのように、要求の変化に関わらない粒度のコード変化

(3)依存するコードの変化。たとえば、クラス名を変更すれば、そのクラスを参照しているコードすべてを変更する必要がある。


そもそも、進化の単位を考えるのがなぜ重要なのかも問わないといけない。進化パターンの文脈で言えば、「列挙型への定数の追加」のレベルで進化のパターンの粒度を扱ってもたいして面白くないということがある。

僕が作っているゲームでの具体例を紹介すると、「列挙型への定数の追加」は次の一連の進化の流れの一ステップにすぎなかった。要求の変化は、新しいゲームモード(正確にはゲームの状態)を追加するというもの。

このゲームでは、ゲームモードという列挙型を定義している(ゲームプログラミングとしてこれが適切な設計なのかは不明)。

(1)列挙定数の追加

public enum GameMode {
TITLE,
OPENING,
MAP_VIEW,
// 省略
LOOK_VIEW // 新規追加
}



(2)ゲームモードに対応するキーハンドラの追加


public class LookViewModeKeyHandler implements GameModeKeyHandler {
//省略
}




(3)キー管理クラスの変更(変更の一部としてメソッドの追加)


public class KeyManager {
//省略
public boolean isLookKeyPressed() {
return isLookKeyPressed;
}


//省略
}



(4)ゲーム状態遷移の変更


public class NormalModeKeyHandler implements GameModeKeyHandler {

public void handleKey(KeyManager keyManager, final GameManager gameManager) {

if (...) {

// 省略

} else if (keyManager.isLookKeyPressed()) {
gameManager.setGameMode(GameMode.LOOK_VIEW);
}

}
}



(5)ゲームモードとキーハンドラの対応付けの追加


public class MainPanel extends JPanel implements Runnable {

// 省略

private Map<GameMode, GameModeKeyHandler> keyHandlers = // ...

private void initKeyHandlers() {

keyHandlers.put(GameMode.TITLE, new TitleModeKeyHandler(titleMenu));
keyHandlers.put(GameMode.OPENING, new OpeningModeKeyHandler(openingMenu));
// 省略
keyHandlers.put(GameMode.LOOK_VIEW, new LookViewModeKeyHandler());
}
// 省略
}




このようなコード進化の流れは大きなパターンとして存在するかもしれない。しかし、以下の点が数ステップからなる進化単位のレベルでパターン化することを困難にする気がする。


(1)パターンとしての信頼性:進化の粒度が多きすぎ、パターンとして信頼できるほど遭遇できるコード進化ではないかもしれない。
evo_pattern.png


(2)抽象化:進化のパターンとして役立つ形で文書化するには、進化のキーとなる側面から抽象化して表現する必要がある。しかし、数ステップからなる進化単位で抽象化することは難しい。
evo_pattern2.png



一方、複数のステップではなく、より粒度の小さい一つのステップ(たとえば「列挙定数の追加」のステップ)だけを進化のパターン化の対象にすれば、

(1)その進化がパターンだと認識できるぐらい実際に遭遇できるかもしれないし、

(2)抽象化もそれほど困難でなくなる。

と考えられる。

evo_pattern3.png


FC2Ad

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。