読者です 読者をやめる 読者になる 読者になる

astah*で作成されたモデルをJavaのプログラムで解析・編集する

組み込みのモデリングツールとして一般的なEnterprise Architectのファイルは、zipファイルとして解凍すると、テキストベースのXMLとして、モデルデータの各種解析や編集が可能になるのが一部で知られています。
一方で同じく組み込み一般的なAstah*の方は、zipファイルとして解凍は可能なものの、中身はバイナリファイルでEAのような解析編集が難しくなっています。ただAPIが提供されていて、その活用でEAよりもっと柔軟な処理を行えるようになることを前に教えて頂きました。今回はそのAPIの使い方について触れたいと思います。

概要

astah* APIJavaAPIです。最新版だと無料のCommunity版でもモデルの解析や編集が可能になっています。
http://astah.change-vision.com/ja/astah-api.html などに情報がまとまっています。

astah* APIライブラリの追加

astah* APIを使用する際は、Community版の場合、astah-communityとastah-apiの2つが必要です。Mac環境のIntelliJだと、ProjectSettingのLibrariesに以下を追加します。

/Applications/astah community/astah community.app/Contents/Java/astah-community.jar
/Applications/astah community/astah community.app/Contents/Java/astah-api.jar

実装

ProjectAccessorを使ってastahのファイルを操作します。細かな仕様は以下などで情報が提供されています。

http://astah.change-vision.com/ja/astah-api.html
http://members.change-vision.com/javadoc/astah-api/7_1_0/api/ja/doc/index.html

実装として、テスト設計の補助を想定して、astah*の状態遷移図のトリガイベントを網羅的にピックアップするコードを書いてみました。

//TARGET_FILE内で作成されている、TARGET_SMDの名前のステートマシン図のトリガイベントをすべて表示
import com.change_vision.jude.api.inf.model.*;
import com.change_vision.jude.api.inf.project.ProjectAccessor;
import com.change_vision.jude.api.inf.AstahAPI;

public class PrintStateTransitions {
    private static final String TARGET_FILE = "/Users/ih/Desktop/sample.asta";
    private static final String TARGET_SMD = "sampleStateMachine";

    public static void main(String[] args) {
        ProjectAccessor prjAccessor = null;
        try {
            prjAccessor = AstahAPI.getAstahAPI().getProjectAccessor();
            prjAccessor.open(TARGET_FILE, true, false, true);
            checkStateTransitions(prjAccessor.getProject());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (prjAccessor != null) {
                prjAccessor.close();
            }
        }
    }
    private static void checkStateTransitions(IPackage iPackage) {
        INamedElement[] ine = iPackage.getOwnedElements();
        for (int ip = 0; ip < ine.length; ip++) {
            INamedElement[] idg = ine[ip].getDiagrams();
            for (int id = 0; id < idg.length; id++) {
                if (idg[id] instanceof IStateMachineDiagram && idg[id].getName().equals(TARGET_SMD)) {
                    printAllStateTransitions((IStateMachineDiagram)idg[id]);
                }
            }
            if (ine[ip] instanceof IPackage) {
                // 入れ子のパッケージ構造を再帰処理
                System.out.println();
                IPackage iChildPackage = (IPackage)ine[ip];
                checkStateTransitions(iChildPackage);
            }
        }
    }

    private static void printAllStateTransitions(IStateMachineDiagram ismd) {
        ITransition[] it = ismd.getStateMachine().getTransitions();
        System.out.print(ismd.getName() + ":");
        for (int i = 0; i < it.length; i++) {
            System.out.println("  " + it[i].getName());
        }
    }
}

(C++)constexpr & static_assertによるコンパイル時テストの用途

 これは ソフトウェアテスト Advent Calendar 2016 - Qiita の4日目の記事です。

 C++では、C++11から以下の言語仕様が追加されました。
・違反するとコンパイルを失敗させる表明構文:static_assert
・指定対象をコンパイル時に処理させる指定子:constexpr

 このstatic_assertとconstexprを組み合わせると、コンパイル時テストを柔軟に構築できるようになります。
 コンパイル時テストは、コンパイルの際に実行され、テストに失敗したらコンパイルエラーを発生させるものです。これはC++のようなコンパイラ方式言語で、テストの選択肢を広げる助けとなります。今回は組み込み向けを想定して、コンパイル時テストの用途を紹介したいと思います。

コンパイル時テストの用途1:コンパイル時処理のテスト

 constexprで記述されたコンパイル時処理なら、大抵コンパイル時にテストできます。そのテストをコンパイル時テストとして書けば、テストを失敗させるコードをコンパイル不能にして、不具合混入を防止できます。
 単純な例ですが、例えば以下のような2つの値の差の絶対値を計算する、constexpr関数get_diffを考えます。

template <typename T>
constexpr auto get_diff(T x, T y) {
    return (x < y) ? y - x: x - y;
}
//今回のエントリのコードは全体的にC++14依存です。動作確認時は「clang++ -std=c++14 ソースファイル」などで

 ここで以下のように、static_assertとconstexprで記述したテストを用意します。

constexpr void test_get_diff(void) {
    static_assert(3 == get_diff(5, 2),"test get_diff: arg_left > arg_right, int");
    static_assert(3 == get_diff(2, 5),"testget_diff: arg_left < arg_right, int");
    static_assert(0.4 == get_diff(0.6, 0.2),"test get_diff:double");
}

 すると、上記test_get_diffのテストを失敗させるようなコードはコンパイルエラーとなります。
 組み込みではC++14を使える環境は少ないですが、コンパイル時処理の強化はC++の言語やライブラリの時流となっています。そのため今後活用の余地が増えていくと思います。

コンパイル時テストの用途2:テストの効率性の改善

 組み込みでは「処理時間(プロセッサリソースの使用量)」「RAM/ROM使用量」「コードの可読性」がしばしばトレードオフの関係になります。ターゲット環境向けに大量のテストデータを扱うテストを実装する際には、そのトレードオフの折り合いをどうするか、悩まされる事があります(テストが遅すぎて処理タイミングが変わったり、ROM/RAM上限を超えたり、と言った感じです)。

 例えば、解説しやすい例として、sin計算テストの期待値生成の実装を考えます。
 sin計算の実装の一つとして、FPGA等でたまにある、以下のように入出力テーブルを用意する方法があります。

//sin計算テストの期待値を生成
double get_expected_sin_value(const int degree) 
{
    const double sin_table[360] = {
        0.0,
        0.017452392,
        0.034899467,
        0.052335912,
        0.069756415,
        0.087155669,
        //略。359まで続く
    };
    return sin_table[degree % 360];
}

void test_my_sin(void) //sin計算のテスト
{
    ...
    EXPECT_EQ(my_sin(5), get_expected_sin_value(5));
    ...
}

 もう一つの実装としては、アセンブラ言語等で定番の、マクローリン展開を行って実装する方法があります。以下のようなコードです。

const double pi = 3.14159;
double taylor_numer(const int n) {
    auto result {1.0};
    for (auto i = 2.0; i <= n; i++) {result *= i;}
    return result;
}

double taylor_denom(const double x, const int n) {
    auto result {x};
    for (auto i = 0; i < n; i++) {result *= x;}
    return result;
}

//sin計算テストの期待値を生成
double get_expected_sin_value(const int degree) {
    auto result {0.0};
    auto rad {(double)(degree % 360) * pi / 180.0};
    
    for (auto n = 0; n <= 8; n++) {
        auto const taylor_summand {taylor_denom(rad, 2 *n) / taylor_numer(2 * n + 1)};
        result += ((n % 2) ? -taylor_summand : taylor_summand);
        if (taylor_summand < 0.000001) {
            break;
        }
    }
    return result;
}

 上記の2つでは、処理時間(プロセッサリソースの使用量)、RAM/ROM使用量、コードの可読性がトレードオフの関係にあります。

  • テーブル方式ですと、処理時間を短くできます。RAM/ROM使用量が増大します。(sinでなくもっと複雑な計算式を使う場合に問題化しますが)テーブル格納値の導出方法がコードから読み取れず、コードの可読性が悪くなることがあります。
  • マクローリン展開方式ですと、処理時間が長くなります。RAM/ROM使用量は少ないです。sin計算の過程(マクローリン展開で、8項あるいは1項0.000001未満になるまで計算)がコードから読み取れるようになります。

 ここで、これらテストの記述をconstexpr指定すれば、処理時間、RAM/ROM使用量、コードの可読性のトレードオフを壊せる場合があります。
 例えばマクローリン展開方式を以下のようにconstexprで実装します。

constexpr double pi = 3.14159;
constexpr double taylor_numer(const int n) {
    auto result {1.0};
    for (auto i = 2.0; i <= n; i++) {result *= i;}
    return result;
}

constexpr double taylor_denom(const double rad, const int n) {
    auto result {rad};
    for (auto i = 0; i < n; i++) {result *= rad;}
    return result;
}

//sin計算テストの期待値を生成
constexpr double get_expected_sin_value(const int degree) {
    auto result {0.0};
    auto rad {(double)(degree % 360) * pi / 180.0};
    
    for (auto n = 0; n <= 8; n++) {
        auto const taylor_summand {taylor_denom(rad, 2 * n) / taylor_numer(2 * n + 1)};
        result += ((n % 2) ? -taylor_summand : taylor_summand);
        
        if (taylor_summand < 0.000001) {
            break;
        }
    }
    return result;
}

 こうすると、sin計算はコンパイル時で済まされます。実行環境ではマクローリン展開の処理時間がなくなり、テーブルのようなROM/RAM使用量も不要になります。期待値の計算過程もコードで表現できます。処理時間、RAM/ROM使用量、可読性の3方面を改善しています(テーブル方式をconstexprで実装しても同様です。テーブル生成をconstexprで行えば、テーブル値の計算過程の明示も可能になります)。

 このように、コンパイル時テストやコンパイル時処理はテスト効率化の一手段となります。
 なおコンパイル時処理を行う場合、各種パラメータはコンパイル時に確定できなければならないという制約がつきます。ただテストデータであればその制約がクリアされる場合が多いため、活用しやすいです。

コンパイル時テストの用途3:契約による設計での、事前条件の達成の確認

 コンパイル時テストは契約による設計と親和性が高いです。事前条件の契約遵守のチェックをコンパイル時テストで記述すれば、(テスト範囲限定ですが)契約違反コードをコンパイル不能にします。契約による設計のアプローチ通り、以降の実行やテストでは、事前条件が満たされた場合の処理に集中できるようになります。


 例えばオープンアーキテクチャのAUTOSARを例にとります。AUTOSARは契約による設計を全面的に取り入れています。
 その一つの例ですが、AUTOSARではリンク時、プレビルド時に確定する静的コンフィグコードと本体コードを分けています。そこでは本体コードにとって、静的コンフィグコードは正しいことが事前条件となっています(コンフィグコードが間違っていると、不正メモリアクセスやNULL参照などが発生します)。
 その事前条件では、結構複雑なコンフィグの一貫性が求められます。例えば以下のようなコードで「BlockDescriptorList::NvBlockNumの値は、Ea_BlockConfigData::EaBlockNumで定義されているものを使用しなければならない」といったようなルールがたくさん出てきます。

const NvM_BlockDescriptorType BlockDescriptorList[] = {
    {
        ...
        0x16,//NvBlockNum  
        ...
    },
    {
        ...
        0x20,//NvBlockNum
        ...
    },
};
const Ea_BlockConfigType Ea_BlockConfigData[] = {
    {
        ...
        0x01, //EaBlockNum
        ...
    },
    ...
};

 一応、コンフィグの正しさについては、DETエラーチェックによって、一般的な表明構文と同じように事前条件チェックを行う仕組みを規定しています。ただDETエラーチェックはかなり簡易的である上、以下のような制約を持ちます。

  • 製品版とDETエラーチェック版でビルドが別れる。
  • 各関数呼び出しタイミングで、入力値が事前条件を満たしているかしか見ない。実行時に事前条件違反を発生させるような条件を実現しないと、不具合を検出できない。


 そこで、チェックを以下のようにコンパイル時テストとして記述しておくと、複雑で包括的な静的コンフィグコードのチェックを行えます。静的コンフィグコードに契約違反があればそもそもコンパイル不能になるため、あとは契約による設計の意図通り、本体コードではコンフィグコードが正しい時の処理にフォーカスできます。

constexpr bool check_memory_block_num_config()
{
    auto iea {0};
    for (auto invm = 0; invm < num_nvm_block; invm++) {
        for (; iea < num_ea_block; iea++) {
            if (Ea_BlockConfigData[iea].EaBlockNum == BlockDescriptorList[invm].NvBlockNum) {
                break;
            }
        }
        if (iea == num_ea_block) {
            return false;
        }
    }
    return true;
}

constexpr void test_memory_config()
{
    ...
    static_assert(check_memory_block_num_config(), "test_memory_config: memory block number");
    ...
}

ツリーで分析するときはクラスベースでも考えよう

 以下でツリーモデルで物事を分析する難しさが少し触れられていたので、ツリーモデルの記法上の制約について書きたい(一応の前置きとして、今回書くのは主に記法上の制約のみで、ツリーによる分析の進め方にはあまり言及しない)。

http://togetter.com/li/1047939

 クラシフィケーションツリー法等を使っていて、ツリーモデルで分析を行う際は、クラスベースモデルでも考えたほうが良い場合が多い。お互いメリット・デメリット両方あり、うまく補完しあえるためだ。
 具体的には、複雑なis-aの関係性を持つもの、has-a/is-aの関係性が混在するものは、ツリーでは表現しにくいが、クラス図ではすっきり表現できることが多い。例えば具体例としては以下。

 他にも、前述の例と似てるけれど、概念モデルでも設計モデルでもよく出てくるBridgeパターンも、ツリーによる表現のしにくさが問題化する。
 これらの問題は概ね、「抽象的なhas-a/is-aの関係を、抽象的に表現できない記法上の制約」に起因して、「要素や関係性の重複が発生」した形になる。

 ツリーベースのモデリングにはこういう制約があるので、モデリングのやりにくさを感じたら、クラスベースで考えるのは有効だと思う。より本質的な要素や関係性を適切に表現できて、分析がやりやすくなることがある。

 なおツリーを使ってうまくモデリングできないという問題は、ソフトウェアテストのテスト条件分析でよく見る。失礼な表現になってしまうかもしれないが、これはモデリング力不足でクラスベースのモデルをかけないことに起因している場合が多いように思う。

対象のドメインを意識してモデリングしよう

 またモデルの記法上の制約以外の原因として、プラクティスや慣習の蓄積も少しあると感じる。

 例えば、分析モデリングがごちゃごちゃになってしまう原因の一つに、複数のドメインをごちゃごちゃにまとめて扱ってしまう問題がある。
 クラスベースモデルでは、この原因を避ける慣習が一般化している。クラスベースモデルの実質的標準となっているUMLで、ドメインモデルと設計モデルの区別、システムモデルとサブシステムモデルの区別を明確にしてモデリングする方法論やプロセス、プラクティスが普及しているためだ。
 対してツリーベースモデルではUMLとくらべてそのような慣習が弱くて、適切なモデリング・不適切なモデリングの基準が曖昧なまま、各人自由にやることが多くなっていると思う。

 そのため、ツリーベースでモデリングする際も、「ドメインごとに分けてモデリングする」「ドメインモデルと設計モデルを分けてモデリングする」といった一般的なクラスベースモデリングの心がけを適用すると、やりやすくなる場合があると感じる。

組み込みUMLでの、分析モデリングでのクラスの識別アプローチについて文献まとめ

 組み込み向けのUML解説本で、概念モデルのクラスの識別をどのように行っているか、今回まとめた。
 まとめた本は以下の本+α。組み込みUMLの技術書5冊と、組み込みではないが、クラス識別に関する名著(オブジェクトデザイン、実践UMLユースケース駆動開発実践ガイド)をピックアップした。

f:id:goyoki:20161103200548j:plain

 初学者向けの個人的な推薦図書としては、「組込みソフトウェアのためのオブジェクト指向モデリング」で入門し、「リアルタイムUML」「リアルタイムUMLワークショップ」で具体的な解説を学び、「オブジェクトデザイン」で知見を深めるのが良いと思う。

組込みソフトウェアのためのオブジェクト指向モデリング

組込みソフトウェア開発のための オブジェクト指向モデリング (組込みエンジニア教科書)

本の概要

 現時点での組み込みUMLの定番の入門書。読みやすく、要点をシンプルにまとめていて、初学者の一冊目としておすすめできる。なお入門書としては参考文献の提示がほとんどないので、他書にも触れたほうが良いと思う。

オブジェクトやクラスの識別方法

 主に責務抽出法を扱う。大まかな流れは以下の通り。これらはイテレーティブに実施してモデルを洗練させる。

  1. コンテキストダイアグラムで対象のインターフェースを明確化する。
  2. クラス候補を抽出する。
    • クラス候補を「有形物」「役割」「出来事」「相互作用」「仕様」の観点で抽出する。
    • システムの本質(要求仕様の中でも重要で基本的な機能)を抽出する。
    • 本質を構成するクラス候補をキー抽象(本質の概念モデルを構成するクラス)として抽出する。
  3. モデル化指針に従って、クラス候補をクラス図として関連付ける。
  4. 本質となるモデルを詳細化・補強して、要求仕様を実現する分析モデルを得る。また静的な分析モデルは、動解析(状態遷移モデルとコミュニケーション図の分析が主)やシナリオに対する妥当性確認を通して洗練する。

 なお本書は教科書的な解説を行っており、クラス識別のアプローチも複数解説している。責務抽出法以外にも、名詞句抽出法、図解抽出法、直感抽出法を扱っている。

オブジェクトやクラスの識別のインプット

 責務駆動設計と同じ。本質を考えるために要求仕様を再解釈しているため、要求モデルの形式に縛りはない。UML本なので一応書籍ではユースケース記述をインプットにしているが、他の要求モデル(たとえばUSDMやユーザストーリーなど)にも適用可能だと思う。

リアルタイムUML第2版

リアルタイムUML オブジェクト指向による組込みシステム開発入門 (Object Oriented Selection)

本の概要

 古くからの組み込みUMLの定番書。組み込み特有の分析の観点やパターンの解説が豊富でおすすめできる。組み込み開発で重要になるシステムモデリングアーキテクチャ設計の解説が充実しているのにも好感を持てる。
 ただ古いのが問題。差分プログラミングを肯定的に解説してるなど今となっては微妙な記述がある。UML2.0啓蒙期に書かれた本のため、UMLの記法の説明にかなり紙面を消費している。

オブジェクトやクラスの識別方法

 大まかに、名詞句抽出法でオブジェクト・クラスを識別する。RUP/UPのように、最初は重要な課題について分析し、それからイテレーティブにモデルを洗練させていく方法を取る(著者はこのやり方をHarmonyという独自のUML活用プロセスとして提唱している)。
 オブジェクトの識別では、名詞句抽出法を使う。名詞句に下線を引き「関心あるオブジェクト」「関心のないオブジェクト」「アクター」「オブジェクトの属性」でカテゴリ分けして、オブジェクト候補を抽出する。次に候補となる名詞句に対し、「要因(アクション、イベント、メッセージの出処)」「サービス」「実世界の事象」「物理デバイス」「キー概念」「トランザクション」「永続情報」「視覚的要素」「制御要素」という観点を用いて、オブジェクトを識別する。
 オブジェクトを識別したらクラスを識別する。クラスの識別では、同じ性質を持つオブジェクトを共通化する、オブジェクトを抽象化するなどしてクラスを識別する。ここでは組み込みに特化した観点を用いる。例えばRTOSならば「タスク」「RTOSカーネル」「I/Oチャンネル」などで構成した典型的なクラスモデルを参考にして、クラスを識別する。
 なお識別したクラスは、ふるまいの分析や要求モデルに対する妥当性の確認を通して洗練させる。

オブジェクトやクラスの識別のインプット

 要求モデルはユースケース、システムモデル(システムを主にクラス図とシーケンス図で表現)が中心。名詞句抽出法で分析モデリングを正しく進められるように、システムモデルと要求モデルには正しさ・具体性が求められる。

リアルタイムUMLワークショップ

リアルタイムUMLワークショップ

本の概要

 著者の提唱するUML活用プロセス、Harmony(和)プロセスの解説本である。著者が同じということもあり、前述のリアルタイムUML第2版と内容はほぼ同じ。ただ演習問題と回答の形式で解説する形式を取る。事例を使った具体的な解説をしているほか、現場実践にあたっての名詞句抽出法の制約とその対策を扱っていて、今回のテーマにとっては有用な書籍。

オブジェクトやクラスの識別方法

 リアルタイムUML第2版と同じ流れを取る。
 細かな差分として、名詞句を抽出した後のオブジェクトの抽出では「原因エージェント」「サービス」「メッセージや情報フロー」「実世界のアイテム」「物理装置」「キー概念」「トランザクション」「永続情報」「ビジュアル要素」「制御要素」の観点を用いる(ただ微妙に違うのは翻訳揺れと思われる)。
 また名詞句抽出方法の課題として、実際に現場のドキュメントに適用すると同義語が多数されるほか分析が大規模になる点をあげている。それについて、名詞句の整理や、重要なユースケースの名詞句に絞って分析しその成果を他に展開するアプローチが解説されている。

オブジェクトやクラスの識別のインプット

 リアルタイムUML第2版と基本同じだった。具体的な事例の解説を行っているため、インプットとなるシステムズモデル、サブシステムモデルをかなり詳細に作成している。それらはブラックボックスユースケース、シナリオ)、ホワイトボックス(シーケンス、内部ブロック図)の2方向で表現する。

組み込みUML eUMLによるオブジェクト指向組み込みシステム開発

組み込みUML eUMLによるオブジェクト指向組み込みシステム開発 (OOP Foundations)

本の概要

 UMLを活用するプラクティスを体系化したeUMLという開発方法を解説している。メーカーの本棚には大抵置いてあるのでこれも定番書と言って良いかもしれない。概念的な解説というより、プロセス定義を解説しているような内容。管理、デバッグまで含めたアクティビティを広く浅く扱っている。

オブジェクトやクラスの識別方法

 分析モデルは、事前に分類したアプリケーションドメインユースケースから作成する。
 おおかかに、本書も重要な対象のモデリングを行って、イテレーティブに洗練させていく方針を取る。ただ具体的なクラス識別の方法を指定していない。参考例として、クラスカテゴリを観点に抽出する方法が解説されている。クラスカテゴリは「制御の対象となるもの(デバイス、温度など現実世界の対象物)」「業務知識(工程、データテーブルなど業務に関する情報)」からなるエンティティ、といったカテゴリ。
 またクラスを抽出した後の、クラスの詳細化や関連付けにデータモデリングを活用している。そこでは将来的な変更も加味した業務データを正規化し、そのデータモデルを概念モデルに展開している。

オブジェクトやクラスの識別のインプットにするもの

 ユースケースユースケースの補助(状態遷移モデル等)、アーキテクチャ要求をインプットとする。
 また事前にドメイン構造を作成する。ドメイン構造は、共通のルールやデータを使用する問題領域ごとに、ユースケースを分類したもの。ドメイン構造の大まかな分類として、アプリケーション、ユーザーインターフェース、メカニズム、デバイスの4つを上げている。これらは機能数や複雑さに応じて分割されていく。ドメイン構造はパッケージ図で表現する。
 アプリケーションのドメインが明確化されていれば、インプットの形式に制約は少ないと思われる。

UML動的モデルによる組み込み開発

UML動的モデルによる組み込み開発 分析・設計・実装・テスト

本の概要

 主に状態遷移モデルを使ったモデル駆動開発の解説書。本書はPLCやLSIといったソフトから離れた話も触れており、純粋なソフトウェアのモデリングの解説は限定的。UMLを使った分析モデルについても触れているのと、オブジェクトの識別で他とは少し違ったUML活用方法を取っているためピックアップした。

オブジェクトやクラスの識別方法

 UMLを使ったモデリングの手法は、eUMLを参照している。説明もeUMLを踏襲したものになっている。
 なお無線制御ソフトウェアの事例解説での分析モデリングのやり方が独特。ユースケース図でオブジェクトを導き出している。
 具体的には、構造化設計手法でDFDの代わりにユースケース図をかく方法を取っている。最初にコンテキストダイアグラムをユースケース図で書く。そしてユースケース内には、内部のデータフローをユースケースで記述する(includeやextedのような関係でなくデータフローのようにユースケースを記述)。構造化設計と同じようにユースケースを適切な粒度まで詳細化して、ユースケースをオブジェクトとする。

オブジェクトやクラスの識別のインプットにするもの

 eUMLと同じく、特に要求モデルに制約はないと思われる。無線制御の例ではユースケース図を前提にしているが、コンテキストダイアグラムでも問題ないと思う。
 なお本書は状態遷移のモデル駆動開発をテーマにしていることから、要求分析からステートマシン図を重視しているけれど、今回のテーマから外れるので触れていない。

オブジェクトデザイン

オブジェクトデザイン (Object Oriented SELECTION)

本の概要

 組み込み以外のおまけその1。組み込みの本ではないが、オブジェクトやクラスの識別についてかなり有名な名著なので今回紹介する。責務駆動設計のバイブル的書籍。今回のテーマについて学ぶなら必読だと思う。

オブジェクトやクラスの識別方法

 本書の代名詞ともいえる責務駆動設計を使う。大まかな流れは以下の通り。

  1. 重要な要求をストーリで表現する。その分析を幹に分析モデルを洗練させていく。重要な要求はアプリケーションの中心的な関心事や、開発の重大な課題となるコア設計である。
  2. ストーリから、分析のテーマを抽出する。
  3. そのテーマを実現するためのロール、責務を分析し、オブジェクトを発見する。ロールや責務の分析では、ロールのステレオタイプという典型的なロールのパターンや、責務のパターンを観点に、オブジェクトのロールや責務を考えていく。とっかかりには「システムが行う作業」「ソフトウェアが直接影響するもの」「ソフトウェア内を流れる情報」「判断、制御、調整アクティビティ」「オブジェクトの構造のグループ」「実世界の物事を表現するもの」といった観点をテーマに適用して分析候補を洗い出す解説が行われている。
オブジェクトやクラスの識別のインプットにするもの

 本書のやり方では、モデル分析しやすいように、一旦要求をストーリに再構築するので、要求モデルに制約はない。ユースケースでも、それ以外の方法でも問題ないと思う。ストーリ形式を用いていることから、ユーザストーリで整理されているとやりやすいかもしれない。別に、アプリケーションの中心的な関心事や、開発上の制約が分かる情報(計画書など)が要求される。

実践UML第3版

実践UML 第3版 オブジェクト指向分析設計と反復型開発入門

本の概要

 組み込み以外のおまけその2。責務駆動設計の一種であるGRASPのバイブル的な著作。書名はUML入門書のような印象だけれど、中身はガチのUP(ユニファイドプロセス)の解説書。今回のテーマについて学ぶならおすすめできる。ただ有名なGRASPは設計モデル向けの話なので使うので、今回は扱わない。

オブジェクトやクラスの識別方法

 問題領域モデルのクラス抽出方法として、既存モデルの再利用、カテゴリリストの使用、名詞句識別の3つを取り上げている
 「カテゴリリストの使用」では、クラスの候補リスト(分析の観点の一種)を用意し、それを要求に適用してクラスを識別する。候補リストとしては、ウェブの例として「ビジネス取引」「取引の明細」「取引が記録される場所」「物理的なオブジェクト」「協調する他のシステム」などからなるリストを紹介している。
 名詞句識別は、他書の名詞句抽出法と同じである。

オブジェクトやクラスの識別のインプットにするもの

 UPの解説なので、インプットはUPの形式であることが求められる。UPではユースケース記述が主なインプットになる。他にユースケース図、アクティビティ図、用語集等で要求を表現する。

ユースケース駆動開発実践ガイド

ユースケース駆動開発実践ガイド

本の概要

 組み込み以外のおまけその3。ICONIXロバストネス分析バイブル的著作。設計モデリングでのオブジェクト・クラスの識別に関して著名な本。なお有名なロバストネス分析は設計モデリングの話なので今回は触れない。

オブジェクトやクラスの識別方法

 他と同じく、反復的にモデルを洗練させるアプローチを取る。概念モデルを作るタイミングは、ユースケース記述の前。
 ICONIXでの概念モデルの扱いはかなりさっぱりしている。ユースケース記述、ロバストネス分析のための語彙を整理・明示するために作成する。使用する語彙が書かれていればよく、最初は、多重度、操作、属性の記述はやめるように解説されているほか、抜け漏れも許容される(後のユースケース記述、ロバストネス分析の中で加筆していく)。大まかな作成の流れは以下の通り。

  1. 要求モデリングで使用する名詞、名詞句を抽出する。
  2. 名詞・名詞句を整理する。同じものをまとめる、適切な命名をする、抽象度・具体度を揃える、不要なものを除くなど。
  3. ざっとクラス図でまとめる。
オブジェクトやクラスの識別のインプットにするもの

 概念モデルはユースケース記述を行う前から作る。そのためインプットには、ユースケースを作る前のより上流の要求も用いる。
 なおICONIXの概念モデルの記述はゆるいので、インプットも縛りはなく自由度がある。

大まかなまとめ

 古い本は名詞句抽出法、新しい本は責務抽出法をつかってクラスを識別している。
 識別では、組み込みに合せた分析の観点や、標準的なクラスモデル、責務パターンを用いている。
 識別のインプットは、責務抽出法では特に制約がない。名詞句抽出法ではシステムモデルや要求モデルに正しさ・完成度をもとめる。

契約プログラミングでのオーバーライドの実現(D、C++、C#の実装比較)

 オブジェクト指向の定番の入門書OOSCでは、オブジェクト指向契約プログラミングの間に密接な関連付けを行っている。例えば継承によってメソッドをオーバーライドする場合では、Assertion Redeclaration ruleとして以下を示している。

  • 事前条件は、継承元と同等かより弱いもの(or-ed)に置き換えられる。
  • 事後条件は、継承元と同等かより強いもの(and-ed)に置き換えられる。

 今回は、このルールの実現度について、D、C++C#の3つぞれぞれで確認してみる。

結果まとめ

 Dは、OOSCのルール通りにオーバーライドを実装できる。記述も簡潔で、今回のテーマに限定すれば本家Eiffelのように契約プログラミングを行える。
 C++C++11の言語機能を使ったBoost.Contract(Boostに将来マージ予定)によって、OOCS通りの実装ができる。ただし違和感のある実装で、無理やり実現している感が否めない。
 C#Visual Studio拡張機能であるCode Contracts for .NET extensionを追加することで、契約プログラミングを簡潔に実装できるようになる。ただオーバーライド時の事前条件の評価方法が、OOSCのルールやDなどと異なる。

サンプルコード

 最低限の確認として、今回は以下を実装して動かす。

  • Bar::run()が、Foo::run()をオーバーライドする。
  • Bar::run()、Foo::run()はそれぞれ異なる事前条件、事後条件を持つ。
  • 「すべての事前条件・事後条件を満たす」「Bar::run()の事前条件のみ違反」「Bar::run()の事後条件のみ違反」「Foo::run()の事前条件のみ違反」「Foo::run()の事後条件のみ違反」の5パターンの動作を試す。

Dでの実装

 D言語契約プログラミングのサポートに注力しており、コードもすっきり書ける。コードは以下の通り。

import std.stdio;

class Foo
{
public:
    int run(int arg)
        in { //事前条件
            assert(arg % 3 == 0);
        }
        out (result) { //事後条件
            assert(result < 100);
        }
        body { //関数本体
            return arg * 3;
        }
}

class Bar : Foo 
{
public:
    override int run(int arg)
        in { //事前条件
            assert(arg % 2 == 0);
        }
        out (result) { //事後条件
            assert(result > 0);
        }
        body { //関数本体
            return arg * 2;
        }
}

void main()
{
    Foo foo = new Bar();
    writeln(foo.run(48));//success すべての事前条件・事後条件を満たす
    //writeln(foo.run(3));//success サブクラスの事前条件違反
    //writeln(foo.run(-6));//failure サブクラスの事後条件違反
    //writeln(foo.run(2));//success スーパークラスの事前条件違反
    //writeln(foo.run(51));//failure スーパークラスの事後条件違反
}

 Bar::run()の引数に応じた動作は、main()のコメントの「success」「failure」の通り。
 Bar::run()、Foo::run()の事前条件はORで結合されて評価されており、事後条件はANDで結合されて評価されているのがわかる。OOSCに忠実な動作となっている。

C++での実装

 C++では、契約プログラミングのライブラリとしてBoost.Contractが開発されている(正式なBoostへのマージは今後の予定。https://github.com/lcaminiti/boost-contract)。今回はそれを使用して実装した。
 なおC++では直接的にOOSCの契約プログラミングをサポートしていないため、記述がかなり煩雑になる。

#include <iostream>
#include <boost/contract.hpp>

class Foo 
{
protected:
    int result_;
public:
    virtual int run(int arg, boost::contract::virtual_* v = 0) {
        boost::contract::guard c = boost::contract::public_function(v, this)
            .precondition([&] { //事前条件
                BOOST_CONTRACT_ASSERT(arg % 3 == 0);
            })
            .postcondition([&] { //事後条件
                BOOST_CONTRACT_ASSERT(result_ < 100);
            })
        ;
        return (result_ = arg * 3); //本体
    }
};

class Bar 
    #define BASES public Foo
    : BASES
{
public:
    typedef BOOST_CONTRACT_BASE_TYPES(BASES) base_types;
    #undef BASES
    
    virtual int run(int arg, boost::contract::virtual_* v = 0) {
        boost::contract::guard c = boost::contract::public_function<override_run>(v, &Bar::run, this, arg)
            .precondition([&] { // 事前条件
                BOOST_CONTRACT_ASSERT(arg % 2 == 0);
            })
            .postcondition([&] { //事後条件
                BOOST_CONTRACT_ASSERT(result_ > 0);
            })
        ;
        return (result_ = arg * 2); //本体
    }
    BOOST_CONTRACT_OVERRIDE(run)
};

int main()
{
    Bar foo;
    std::cout << foo.run(48) << std::endl;//success すべての事前条件・事後条件満たす
    //std::cout << foo.run(3) << std::endl;//success サブクラスの事前条件違反
    //std::cout << foo.run(-6) << std::endl;//failure サブクラスの事後条件違反
    //std::cout << foo.run(2) << std::endl;//success スーパークラスの事前条件違反
    //std::cout << foo.run(51) << std::endl;//failure スーパークラスの事後条件違反

    return 0;
}

 Bar::run()への引数に応じた挙動は、D言語のものと同じになる。
 そのためC++でもオーバーライドでは契約プログラミングのルールを実現できている。

C#での実装

 C#も直接的にOOSCの契約プログラミングをサポートしていない。ただCode Contracts for .NET extensionを使用することによって、実装が容易になる(なおこのextensionはVC++でもVB.NETでも使える)。

using System;
using System.Diagnostics.Contracts;

class Foo
{
    public virtual int run(int arg)
    {
        Contract.Requires(arg % 3 == 0); // 事前条件
        Contract.Ensures(Contract.Result<int>() < 100); // 事後条件
        return arg * 3;
    }
}
class Bar : Foo
{
    public override int run(int arg)
    {
        Contract.Requires(arg % 2 == 0); // 事前条件
        Contract.Ensures(Contract.Result<int>() > 0); // 事後条件
        return arg * 2;
    }
}

class Program
{
    static void Main(string[] args)
    {
        int ret;
        ret = new Bar().run(48);//success すべての事前条件・事後条件を満たす
        //ret = new Bar().run(3);//failure サブクラスの事前条件違反
        //ret = new Bar().run(-6);//failure サブクラスの事後条件違反
        //ret = new Bar().run(2);//failure スーパークラスの事前条件違反
        //ret = new Bar().run(51);//failure スーパークラスの事後条件違反        
        Console.WriteLine(ret);
    }
}

 Code Contracts for .NETを使った場合、事後条件、事前条件ともに、AND結合されて評価されるようだ。「サブクラスの事前条件のみ違反」「スーパークラスの事前条件のみ違反」のときに、OOSCのルールと異なる判定結果を出している。

Stateflowでの状態遷移モデルの動的シミュレーション

ソフトウェアのモデル駆動開発ツールの一つに、Stateflowがある。

Stateflowは、MATLAB/Simulink上で状態遷移モデルと制御フローモデルを動かすための環境に該当する。
この環境、母体となるMATLAB/Simulinkが組み込み制御システムならあらかたシミュレートできる超高機能ツールなので、ソフトウェア設計モデルのツールとしてはオーバースペックと言えるかもしれない。ただMBSEのようにシステムレベルからモデル駆動やモデルベースの開発を行っている場合は、モデル駆動開発環境として有望な選択肢になると思う。

あまり情報がないので今回簡単に紹介したい。

モデルの記述

題材として、黒線を伝って走るライントレーサーを扱う。
なおMATLAB/Simulinkは、制御理論や信号処理のライブラリが充実しているので制御システムレベルからシミュレートしてこそ価値が出るけれど、今回はあくまで例示としてソフトウェア設計モデルのみを扱う。

全体のシステムのモデルは以下。

f:id:goyoki:20160414005646p:plain

「Line Sensor」がライントレーサーの右側センサ・左側センサの入力で、「Motor Signal」が(現実からかなり簡略化しているけれど)右側モータの駆動信号、左側モータの駆動信号を示す。
真ん中の「Line Tracer」がソフトウェア処理のモデルになる。このモデルを状態遷移モデルで記述・実行できるようにするのがStateflowになる。


「Line Tracer」の中のモデルは以下。ソフトウェアの状態遷移モデルを記述している。なおStateflowでは、状態遷移モデルを状態遷移図でも状態遷移表でも書けるようになっている。

f:id:goyoki:20160418005501p:plain

処理としては、一定周期ごとにセンサ入力を判定し、直進、左折、右折を実行するようにしている。またセンサが両側とも反応しない異常状態では人手で位置直しすることを期待して停止する。

モデルのシミュレーション

システムの「Line Sensor」はSignal Builderと呼ばれるもので、Excelで入力パターンを指定できる。
例として、停止、右折、左折、直線それぞれを網羅するセンサ入力データのパターンを0秒から8秒分まで指定し、その間のモデルの動きをSimulinkでシミュレートする。
シミュレート結果である入出力の推移は以下の様に表示される。

f:id:goyoki:20160418005616p:plain

指定したセンサ入力パターンに応じて、状態遷移モデルが動作し、モータが駆動されていることがわかる。
なおこうした入出力のプローブだけでなく、状態遷移をアニメーションで示したり、構造的な検査を行ったりすることもできる。

憂鬱なExcel作業をPythonで紛らわす

自分の組み込み業界ではやたらExcelが多くて、Excelドキュメントのレビューの機会が度々ある。その中には、ファイル間のトレーサビリティ目視チェックといった、時に刺身タンポポと揶揄されるような気の滅入る作業も少なくない。


こういった作業は、周知の通りだと思うけれど、マイクロソフト系の言語や、PythonRubyなど様々なプログラミング言語Excel操作のライブラリを提供しているおかげで、自動化できることが多い。
そのため基本姿勢として自動化に手を付けてみるのは良いと思う。生産性が上がることが多いのもある。また何より、例えば「Excelの目視レビューでなく、Pythonのコーディングをしている」と思えば気を紛らわせられる、ような気がする。ソフトウェア開発者として精神衛生的に良い。

今回はその自動化の実現手段の一つとして、Pythonのopenpyxlを使ったExcelドキュメントのチェックについて書きたい。

openpyxlの大まかな使い方

openpyxlはPythonExcelの読み書きを行うためのライブラリ。今回は読み取りのみ扱っているけれど、Excelファイルの生成や内容更新もできる。

インストール

インストールはpipで可能(pip install openpyxl実行)

ファイルのロードの内容の表示

openpyxl.load_workbookでファイル名を指定してワークブックを読み出し操作する。
例えばファイル名「仕様書2.xlsx」の、仕様項目定義シートのB3セルの値を表示する場合、以下のように記述する。

from openpyxl import load_workbook

wb = load_workbook(filename = "仕様書2.xlsx", read_only=True)
print(wb["仕様項目定義"]["b3"].value)


複数のセルを扱いたい場合は、以下のようにiter_rows等を使用する

from openpyxl import load_workbook

wb = load_workbook(filename = "仕様書2.xlsx", read_only=True)
#仕様項目定義シートのB1からB20までのセルの値を表示
for id_area in wb["仕様項目定義"].iter_rows("B1:B20"):
    for data in id_area:
        print(data.value)

全シートを参照する場合は、ワークブックオブジェクトのworksheetsに対してfor inループを回す。

from openpyxl import load_workbook
wb = load_workbook(filename = "仕様書2.xlsx", read_only=True)
#全シートのタイトルとA1セルの値を表示
for sheet in wb:
    print(sheet.title)
    print(sheet["A1"].value)

サンプル

複数間のファイルでIDの整合性がとれるかという、よくありがちで憂鬱なチェックをopenpyxlで行う。
具体的には、「仕様書2.xlsx」と「トレーサビリティマトリクス.xlsx」の仕様項目IDが一致しているかを確認する。

仕様項目2.xlsx 仕様項目定義シート
●仕様項目定義
ID 仕様項目詳細
SPEC1 AAA
SPEC2 BBB
SPEC13 CCC
SPEC4 DDD
SPEC5 EEE
SPEC6 FFF
トレーサビリティマトリクス.xlsx トレーサビリティマトリクスシート
仕様項目
SPEC1 SPEC2 SPEC3
要件 REQ1
REQ2
REQ3

確認コード

# coding: shift_jis
from openpyxl import load_workbook

if __name__ == '__main__':

    # トレーサビリティマトリクスから仕様項目IDを抽出
    wb_tm = load_workbook(filename = "トレーサビリティマトリクス.xlsx", read_only=True)
    
    tm_id_list = []
    for id_area in wb_tm["トレーサビリティマトリクス"].iter_rows('D4:AO4'):
        for data in id_area:
            #最初の文字列から空欄までの文字列をピックアップ
            if data.value != "" and data.value != None:
                tm_id_list.append(data.value)
            if data.value == "" and len(tm_id_list) > 0:
                break


    # 仕様書から仕様項目IDを抽出
    wb = load_workbook(filename = "仕様書2.xlsx", read_only=True)

    spec_id_list = []
    in_idarea = False
    for id_area in wb["仕様項目定義"].iter_rows("B1:B500"):
        for data in id_area:
            # タイトル"●仕様項目定義"から空欄までの文字列をピックアップ
            if in_idarea:
                if data.value == "":
                    break;
                if data.value != "ID":
                    spec_id_list.append(data.value)
                        
            if data.value =="●仕様項目定義":
                # 仕様項目IDの記述欄開始
                in_idarea = True
    
    
    if sorted(spec_id_list) == sorted(tm_id_list):
        print("整合")
    else:
        print("不整合")