RSS RSS feed | Atom Atom feed

魂のプログラミング

魂のプログラミング

魂のプログラミングとはシンプルな信念である。

プログラムコードは一行一行すべてに魂がこもっていなければならない

という信念である。

以前プログラム製造の現場で若手プログラマを指導している際に, つい「この行には魂がこもっていない」という言い方を何度かしていたところ, 周囲の数人の SE にそのような物言いが伝染してしまった。後日, その中の一人がソースコードレビューに参加したときに「このソースコードには魂がこもっていない」と指摘し, 指摘されたプログラマが反応に窮したというエピソードを聞いた。「魂のプログラミング」という言葉は, このエピソードを聞いたときにふと私の頭の中に浮かんだ言葉である。

「プログラムコードの一行に魂がこもる」とは, 別に宗教めいた意味ではない。一言で言うならば,

プログラムコードのあらゆる行に目的がある

とか

どの一行についてもその目的が必ず説明できる

というような意味である。空行にさえ説明可能な目的があるべきだと私は考えている。

本稿では魂のプログラミングを実践する上での教訓を, 例を挙げながら解説していくことにする。まだまとまっておらず, 多分に断片的であるが, 魂のプログラミングの一端は感じられるのではないだろうか。

やりたいことを簡潔に表現する

やりたいことを正確に理解する

唐突ではあるが, 「endian (エンディアン)」という言葉を聴いたことがあるだろうか。ガリバー旅行記に由来するこの言葉は, 多桁数値を表現する際, 上位桁から下位桁に向かって記述する (big endian) か, 下位桁から上位桁に向かって記述する (little endian) かの約束事を意味している。ちなみに算用数字による 123 などの表記は big endian で, 百二十三を敢えて little endian で書けば 321 となる。太古の昔から多くのコンピュータはどちらかの宗派に属している。(少数派として変態 endian の VAX などや, endianess 切替機能付きコンピュータもある。)

昔, このような質問を受けたことがある。

プログラムを実行しているコンピュータが big endian であるか調べ, もしそうでなかったら 32 ビット値のバイト順を逆転させたい。C でどう書くのがスマートですか

この質問にもし条件反射的に回答するとすれば, たとえば以下のようになる。

/* big endian であるか調べ, そうでなかったらバイト順を逆転するコード。*/
    ...
    static short one = 1;
    unsigned v = ...;
    ...
    if (*(char*)&one != 0) // 注意: 少し危険な判定!
      {
        v = v >> 16 & 0x0000FFFF | v << 16 & 0xFFFF0000;
        v = v >>  8 & 0x00FF00FF | v <<  8 & 0xFF00FF00;
      }

しかし, そう回答する前にこの質問者が本当は何がしたかったのか考えてみよう。3 分ほどじっくり考えると, 実はこの質問者は客先のコンピュータの endianess を研究したいわけでも, バイト順を逆転させるトリックを知りたいわけでもないことに気付く。質問者が本当にやりたかったことは,

4 バイト big endian で格納された 32 ビット値を取出す

という処理である。

/** 4 バイト big endian で格納された 32 ビット値を取出すコード。*/
    unsigned v;
    unsigned char bytes[4];
    ...
    v = bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3];

処理は 1 行で完結する。こちらの方がスマートで分かりやすいコードであるが, これは別にプログラミングスキルの問題ではなく, それ以前の問題であったことが分かる。すなわち, 自分が本当は何をしたいのか理解していなかったのである。

最適な手段で表現する

短いコードは常に長いコードより優れている。より良いコードを書きたいなら, 第一に短く簡潔なコードを目指すべきである。

プログラミング言語にはそれぞれ特有の考え方や機能がある。そのため, 最適なコードは利用するプログラミング言語によって変わってくる。この観点からもプログラミング言語に関する正確な知識は重要である。

例として, まず JAVA で素朴な線形リストクラスを実装してみよう。

/** JAVA 版線形リストの実装クラス。*/
public class SinglyLinkedList<T>
  {
    /** 各ノードを表すクラス。*/
    private static class Node<U>
      {
        private Node(U data) { this(null, data); }

        private Node(Node<U> next, U data)
          {
            this.next = next;
            this.data = data;
          }

        private Node<U> next;
        private U data;
      }

    ...

    /**
     * このリストの末尾に指定された要素を追加する。
     *
     * @param o 追加する要素
     */
    public void pushBack(T o)
      {
        Node<T> node = head;
        if (node == null)
          {
            // ノードが一つもなかった場合, 追加する要素のノードが先頭となる。
            head = new Node<T>(o);
          }
        else
          { // 最後のノードを探す。
            while (node.next != null)
                node = node.next;
            // 最後のノードの次に追加。
            node.next = new Node<T>(o);
          }
      }

    ...

    private Node<T> head;
  }

同様な実装を C++ のテンプレートで行う。C/C++ のポインタを効果的に利用すると pushBack メソッド (C++ では push_back メンバ関数) の条件分岐が不要となる。

/** C++ 版線形リストクラステンプレート。*/
template<class T> class singly_linked_list
  {
    /** 各ノードを表す構造体。*/
    struct node
      {
        explicit node(const T& data = T(), node* next = 0):
            m_next(next), m_data(data) { }

        node* m_next;
        T m_data;
      };

  public:

    ...

    /**
     * このリストの末尾に指定された値の要素を追加する。
     *
     * @param o 追加する要素の値
     */
    void push_back(const T& o)
      {
        node** cptr = &m_head;
        for (node* curr = *cptr; curr != 0; curr = *cptr)
            cptr = &curr->m_next;
        *cptr = new node(o);
      }

    ...

  private:
    node* m_head;
  }

なぜこれでよいのかは, じっくり考えてみて欲しい。

アイディアや疑問は実験してみる

次のコード片はコンパイルできるだろうか。もしできたとして実行すると何が起こるのだろうか。

// ふと疑問に思った JAVA のコード片。
    String[] s = { "Hello", "World" };
    Object[] o = s;
    o[0] = new Integer(0);
    System.out.println(s[0].getClass().getName());

JAVA である程度経験を積んだプログラマなら, 答を知っていると思うが, 分からなくても実際やってみることは 3 分もあればできる。

前節で紹介した例の通り, 少なくともポインタがある分 C++ は JAVA よりも表現力で勝っているといえる。しかし, JAVA でも C++ のポインタを真似すれば同様の表現が可能なのではないか, というアイディアを持ったとしよう。

早速頑張ってみよう。

よく JAVA にはポインタがないと言われたり, JAVA にはポインタしかないと言われたりする。JAVA のオブジェクト参照と C/C++ のポインタ (の値) の考え方は似ているが, 決定的に違うのは C/C++ はポインタ型を含めてあらゆる型のデータオブジェクトへのポインタが & 演算子で簡単に作成できるという点である。そこで & 演算子を適用する可能性のあるポインタ (オブジェクト参照) を予め Pointer オブジェクトとしてラップしておけばうまく真似できるのではないかと予想が立つ。詳細な説明は省略するが, その考え方で前節の例を書き直すと, 次のコードとなった。

/** C++ 版の真似をした JAVA 版線形リスト。*/
public class SinglyLinkedListCPlusPlus<T>
  {
    /** X へのポインタを真似るクラス。*/
    private static class Pointer<X>
      {
        private Pointer(X pointee) { this.pointee = pointee; }

        private X pointee;
      }

    /** 各ノードを表すクラス。*/
    private static class Node<U>
      {
        private Node(U data) { this(null, data); }

        private Node(Node<U> next, U data)
          {
            this.next = new Pointer<Node<U>>(next);
            this.data = data;
          }

        private Pointer<Node<U>> next;
        private U data;
      }

    ...

    /**
     * このリストの末尾に指定された要素を追加する。
     *
     * @param o 追加する要素
     */
    public void pushBack(T o)
      {
        Pointer<Node<T>> cptr = head;
        for (Node<T> curr = cptr.pointee; curr != null; curr = cptr.pointee)
            cptr = curr.next;
        cptr.pointee = new Node<T>(o);
      }

    ...

    private Pointer<Node<T>> head;
  }

ひとまず実験は成功である。しかし実験は実験であって, こんな冗談コードを実際の現場では利用するべきではない。この Pointe クラスは if 文を一つ減らすための JAVA での最適な手段とは決して言えない。

境界に注意する

次の中途半端に魂のこもったコードを見てもらいたい。バグが 3 つほど巧妙に仕込んであるのだが, 気付くだろうか。

/* 関数名と引数で関数値を計算する C のコード。注意: バグ入り。*/
#include <math.h>
#include <string.h>

static double func_floor(double a) { return floor(a); }
static double func_ceil(double a) { return ceil(a); }
static double func_sqrt(double a) { return sqrt(a); }
static double func_sin(double a) { return sin(a); }
static double func_cos(double a) { return cos(a); }
static double func_tan(double a) { return tan(a); }
static double func_exp(double a) { return exp(a); }
static double func_log(double a) { return log(a); }

static const struct
  {
    const char* keyword;
    double (*function)(double x);
  } function_table[] =
  {
      { "ceil", func_ceil },
      { "cos", func_cos },
      { "exp", func_exp },
      { "floor", func_floor },
      { "log", func_log },
      { "sin", func_sin },
      { "sqrt", func_sqrt },
      { "tan", func_tan }
  };

static const unsigned MAX_FUNCTION_INDEX = sizeof function_table / sizeof function_table[0] - 1;

double calc_function(const char* keyword, double a)
  {
    unsigned lo = 0;
    unsigned hi = MAX_FUNCTION_INDEX;
    while (lo < hi)
      {
        unsigned k = (lo + hi) / 2;
        int c = strcmp(function_table[k].keyword, keyword);
        if (c < 0)
            lo = k + 1;
        else if (c > 0)
            hi = k;
        else
            return function_table[k].function(a);
      }
    return 0;
  }

慎重に考えれば calc_function は次のように修正しなければならないことが分かるだろう。

double calc_function(const char* keyword, double a)
  {
    unsigned lo = 0;
    unsigned hi = MAX_FUNCTION_INDEX;
    while (lo <= hi)
      {
        unsigned k = lo + (hi - lo) / 2;
        int c = strcmp(function_table[k].keyword, keyword);
        if (c < 0)
            lo = k + 1;
        else if (c > 0)
          {
            if (k == 0)
                break;
            hi = k - 1;
          }
        else
            return function_table[k].function(a);
      }
    return 0;
  }

とかく境界の判断や更新は慎重に検討すべきである。

車輪を再発明する

「車輪を再発明する愚」という人がよくいるが, 再発明自体は大いによいことである。

ただし, そのための工数を確保するには, 説得力のある理由が必要である。

動作したという理由だけで満足しない

プログラムが動作したからといって完成したと思ってはいけない。

より汎用化できないか考える

汎用的なコードは, 再利用できる可能性が大きいため共通化が進み, プログラムの肥大化を防ぐことに繋がる。しかしより重要な利点は, 汎用的なコードからは特殊な要件が排除されるため, コードをシンプルに保ち, 品質を上げやすいというところにある。

具体的には, アルゴリズムやデータ構造を表現するコードから特殊な業務要件を分離し, パラメータや派生クラスとして実現することを考えるべきである。必要なら Facade を用意し, I/F をシンプルに保つ工夫も役に立つ。

前節で登場したバイナリサーチを汎用化してみよう。

#include <stddef.h>

const void* binary_search(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t lo = 0;
    size_t hi = n - 1;
    while (lo <= hi)
      {
        size_t k = lo + (hi - lo) / 2;
        const void* center = (const char*)base + k * size;
        int c = compare(center, key);
        if (c < 0)
            lo = k + 1;
        else if (c > 0)
          {
            if (k == 0)
                break;
            hi = k - 1;
          }
        else
            return center;
      }
    return NULL;
  }

この仕様は, 標準ライブラリの bsearch という関数ほとんどそのままである。車輪の再発明というわけである。

より簡単な方法がないか考える

動作するコードを書いてみて初めて, 設計フェーズでは気付かなかった抽象化や共通化の方法に気が付くことがある。書いたコードを目視確認していて複数回登場する似たようなパターンに気付いたり, I/F をよりシンプルにできることに気付いたりすることは決して珍しいことではない。

やや作為的だが, ここでは汎用バイナリサーチの仕様を少し変更し, 特定の値以上の最初の位置を返す lower_bound を実装してみる。 こちらの方が応用範囲は広がる。

#include <stddef.h>

const void* lower_bound(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t lo = 0;
    size_t hi = n;
    while (lo < hi)
      {
        size_t k = lo + (hi - lo) / 2;
        if (compare((const char*)base + k * size, key) < 0)
            lo = k + 1;
        else
            hi = k;
      }
    return (const char*)base + lo * size;
  }

バイナリサーチのコードの変数 hi の役割を微妙に変更している。バイナリサーチでは hi は対象範囲の最大インデックスを保持していたが, 今回の lower_bound では最大インデックス+1 を保持している。

また, さらに作為的だがバイナリサーチもループ内が極力短くなるように少し変形してみる。ただし変数 hi の役割は元のままである。

#include <stddef.h>

const void* binary_search(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t lo = 0;
    size_t hi = n - 1;
    while (lo < hi)
      {
        size_t k = lo + (hi - lo) / 2;
        if (compare((const char*)base + k * size, key) < 0)
            lo = k + 1;
        else
            hi = k;
      }
    return compare((const char*)base + lo * size, key) == 0 ? (const char*)base + lo * size : NULL;
  }

こう変形してみると, 変数 hi の役割が違っているにもかかわらず, ループ部分のコードは lower_bound と全く同じコードとなった。実際にコードを書いてみる前にこのような事実に気付くのは難しい。

より効率化できないか考える

変数 lo, hi で対象範囲を絞込んでいくのではなく, 変数 lo と区間幅 n で対象範囲を絞り込んでいくことを考え, この 2 変数の動きを慎重に検討すると, else 節をなくすことができることが分かる。さらに変数 base と n で制御すればより簡単になるが JAVA ではできない。

#include <stddef.h>

const void* lower_bound(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t lo = 0;
    while (n > 0)
      {
        size_t k = n / 2;
        if (compare((const char*)base + (lo + k) * size, key) < 0)
          {
            lo += k + 1;
            --n;
          }
        n /= 2;
      }
    return (const char*)base + lo * size;
  }

乗算は, 加減算と比べると効率が悪いことが多い。工夫するとコードは長くなるがループ内から乗算を追い出すことができる。ここから先は C 専用の最適化を行っている。

#include <stddef.h>

const void* lower_bound(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t d = n * size;
    while (n > 0)
      {
        if (n % 2 == 0)
          {
            const char* p = (const char*)base + (d /= 2);
            if (compare(p, key) < 0)
              {
                base = p + size;
                d -= size;
                --n;
              }
          }
        else
          {
            const char* p = (const char*)base + (d = (d - size) / 2);
            if (compare(p, key) < 0)
                base = p + size;
          }
        n /= 2;
      }
    return base;
  }

さらに魂をこめる。(x & -x) という式で x のビットパターン中の最下位の 1 だけを取出すことができる。このことを利用すると, 上のコードの変数 n を省略することができる。

#include <stddef.h>

const void* lower_bound(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t odd_bit = size & -size;
    n *= size;
    while (n > 0)
      {
        if ((n & odd_bit) == 0)
          {
            const char* p = (const char*)base + (n /= 2);
            if (compare(p, key) < 0)
              {
                base = p + size;
                n -= size;
              }
          }
        else
          {
            const char* p = (const char*)base + (n = (n - size) / 2);
            if (compare(p, key) < 0)
                base = p + size;
          }
      }
    return base;
  }

ややマニアックな話題だっただろうか。

主義主張を持つ

プログラミングスタイルを確立する

自分なりのプログラミングスタイルを確立するのは重要である。自分のスタイルで美しいコードには愛着が沸く。何より一定のポリシーに従ったプログラムコードは読みやすい。

プログラミング哲学に触れる

R. C. Martin らによってオブジェクト指向設計の原則がまとめられている。特に目新しい哲学とは言えないが, 設計の様々な段階でチェックリストとして用いると便利である。

原則名 英語名 略記 意味
リスコフの置換原則 The Liskov Substitution Principle LSP 派生型はその基本型と置換可能でなければならない。
依存の非循環原則 The Acyclic Dependencies Principle ADP パッケージは循環依存してはならない。
依存関係逆転の原則 The Dependency Inversion Principle DIP 上位レベルモジュールは下位レベルモジュールに依存してはならない。どちらも抽象モジュールに依存する方がよい。抽象は詳細に依存してはならない。
I/F分離の原則 The Interface Segregation Principle ISP インターフェースは最小であるべき。クライアントが利用しないメソッドへの依存を強制してはならない。
安定依存の原則 The Stable Dependencies Principle SDP パッケージの依存方向は安定性の方向と一致すべきである。
安定抽象の原則 The Stable Abstraction Principle SAP 安定したパッケージは抽象的であるべきである。
開放/閉鎖の原則 The Open-Closed Principle OCP ソフトウェアの構成要素 (クラス、モジュール、関数など) は拡張に対して開いていなければならず、修正に対しては閉じていなければならない。
全再利用の原則 The Common Reuse Principle CRP 1パッケージ内のクラス群は同時に再利用される。
リリース-再利用等価の原則 The Release-Reuse Equivalency Principle REP 再利用の粒度はリリースの粒度と一致する。
閉鎖性共通の原則 The Common Closure Principle CCP 1パッケージ内のクラス群は同種の変更に対して閉じている。パッケージに影響する変更はパッケージ内のすべてのクラスに影響を及ぼすが、ほかのパッケージには影響しない。
単一責務の原則 The Single Responsibility Principle SRP 1つのクラスに1つの責務。クラスを変更する理由は1つ以上存在してはならない。(責務=変更理由)

魂を抜く

「一行入魂」を実践することはそれほど難しいことではないが, これが「万行入魂」とかになってくると, 必ずしもすべての行に同じ集中力で魂を込めるというのは現実的でなくなってくる。

イディオムを使いこなす

たとえば多くのプログラマは C++ や JAVA で n 回だけ繰返したいと思えば

        ...
        for (int k = 0; k < n; ++k)
            ...

というコードが特に考えなくても出てくるだろう。C/C++ でリスト構造を先頭から末尾まで辿りながら処理したいと思えば

        ...
        for (p = head; p != NULL; p = p->next)
            ...

である。この程度のコードならほとんど反射的にタイプする人もいる。

私の場合, その昔 i8080 系の CPU の全盛期に, アセンブリ言語で大量にコードを書いた経験があるが, このテの CPU はアドレッシングが貧弱で, 小さなテーブルルックアップだけでも次のようなコードを必要とした。しかし, この一連の流れはもう指が覚えてしまっていて, 最初のステップをタイプし始めながら, 6 ステップ先以降のコードを考えるというような芸当をこなしていた。高級言語の 1 ステップが数ステップから数十ステップにも膨らむアセンブリ言語で大量のコードを書くには, このような熟練はほとんど必須と言える。

        LHLD    base
        LDA     index
        MOV     E, A
        MVI     D, 0
        DAD     DE
        MOV     A, M

いや, 今はもうほとんど覚えていない。

もちろんイディオムを構成する一連のコードは, 一度は魂を込めて考える必要がある。いわば一度込めた魂を再利用しているわけである。

メリハリを意識する

矛盾することを言うようだが, プログラム全体の中には密度が薄く, 再利用可能性をほとんど考える余地のないコードが大量に含まれることになる。魂を削って心血注ぐべきコードの割合が大きくなるようでは, あまりバランスのよいプログラム設計とは言えない。

設計書を妄信しない

設計者も間違う

信用しないと言うと語弊があるが, プログラマが渡された設計書は絶対的なものではないし, まして完全なものだと信じてはいけない。自分で納得できないプログラムは書くべきではない。プログラマはキーパンチャではないのだから。

設計へのフィードバック

私は昔物理学を勉強していたのだが, 物理学の根底には, 「正しい理論はシンプルであり, 数学的にも美しい理論である」という漠然とした信念がある。やたら前提条件が複雑であったり, 表現するのに複雑な数式が大量に必要であったりすると, その理論は別の大きな理論の一部を歪に切り取ったものなのではないか, とか, その理論をシンプルに表現できる数学が発明されるべきだと考えるわけである。

プログラムコードについても似たような考え方が成り立たなくもない。ある単純な処理を行うコードがむやみに複雑だったりした場合, 処理仕様が歪んでいるのではないのかとか, プログラミング言語や標準ライブラリに何か問題があるのではないかと疑ってみるのだ。

前者の場合, 処理仕様の改訂が提案できるような状況では大いに提案すべきである。

後者の場合, プログラミング言語の選定をやり直すというのは状況的に受入れられないことが多いが, たとえば非 OOP 言語で OOP の真似をする規約を提案するなど, 工夫によっては何とかなる可能性もある。

推薦図書

書名 著者 内容
プログラミング言語C Brian W. Kernighan
Dennis M. Ritchie
C言語のバイブル。コード例の質が高く, C言語に興味がなくても為になる。著者の頭文字をとって「K&R」とよく省略される。
プログラミング言語C++ Bjarne Stroustrup C++言語のバイブル。分量も密度も高く, 示唆に富んでいる。
プログラム書法 Brian W. Kernighan
P. J. Plauger
古典的な名著。良いプログラムを書くためのべからず集。
文芸的プログラミング Donald E. Knuth プログラムは文芸作品であるという哲学を実践する本。Javadoc にも影響を与えている。
ハッカーの楽しみ
―本物のプログラマはいかにして問題を解くか
Henry S. Warren Jr. さまざまなプログラミングテクニック集。内容は非常に濃い。
達人プログラマー
―システム開発の職人から名匠への道
Andrew Hunt
David Thomas
良いソフトウェアを開発/設計/製造するための指針。
Javaによるアルゴリズム事典 奥村晴彦 アルゴリズムの事典。「Javaによる」という部分には期待しない方がよい。解説は薄いため, 本格的な勉強用には向かない。

.NET データベースプログラミング

ADO.NETの基本

1. ADOとADO.NET

ADO とは,“ActiveX Data Object” の略であり,.NET 以前にデータベースにアクセスするための技術でした。ADO.NET は,この ADO をさらに進化させたものとなっています。

2. 構成

ADO.NET を構成するクラス群は,大きく2種類の部分に分けることができます。一つのグループは実際にデータベースシステムを操作するためのクラス群であり,もう一つのグループはデータを扱うためのモデルクラス群であるということができます。

前者はデータプロバイダと呼ばれていて,利用するデータベースシステムやドライバに応じていくつかのクラス群を選択して使用することになります。

後者は特に名前が付けられていないようですが,このクラス群の代表的クラスの DataSet の名前で呼ばれることがあるようです。この DataSet クラス群が,直接データベースに依存していない,つまりデータプロバイダと完全に分離されていることが,ADO.NET の大きな特徴となっています。この構成は非接続型アーキテクチャという言葉で呼ばれています。

次の図は,データプロバイダと DataSet の構造の概要を示しています。

上記以外にも,NET Framework の多くの GUI 部品は,ADO.NET と接続して簡単に GUI を構築するための機能を持っています。

3. データプロバイダ

標準のデータプロバイダには,以下の4種類があります。

・.NET Framework Data Provider for SQL Server

Microsoft SQL ServerのVersion 7.0 以降で利用するためのデータプロバイダです。このデータプロバイダは直接データベースシステムにアクセスするため,軽量かつ高速であるとされています。

名前空間は System.Data.SqlClient となっています。

・.NET Framework Data Provider for Oracle

Oracle クライアントを通じて Oracle にアクセスするためのデータプロバイダです。これは .NET Framework 1.1 になってから導入されました。

Oracle 自身も独自のデータプロバイダを配布しているため,Oracle を利用する場合はどちらかを選択することになります。

名前空間は System.Data.OracleClient となっています。

・NET Framework Data Provider for OLE DB

以前から利用されていた OLE DB を利用するためのデータプロバイダです。要するに OLE DB ブリッジです。OLE DB を経由して間接的にデータベースシステムにアクセスするため,上記のデータプロバイダと比べると効率が悪くなります。

名前空間は System.Data.OleDB です。

・.NET Framework Data Provider for ODBC

ODBC 経由でデータベースにアクセスするためのデータプロバイダです。.NET Framework 1.1 から標準となっています。

名前空間は System.Data.Odbc ですが,.NET Framework 1.0 で Microsoft の配布サイトからダウンロードして利用する場合は Microsoft.Data.Odbc となります。

4. 基本的なコード例

(1) 単純な SQL の実行

早速,SQL Server を例に,C# の簡単なコードを書いてみます。

 1: using System;
 2: using System.Data.SqlClient;
 3:
 4: public class SanCorporation
 5: {
 6:    public static void Main()
 7:    {
 8:        using (SqlConnection connection =
 9:               new SqlConnection("Data Source=.\\SQLEXPRESS; " +
10:                                 "Integrated Security=True; " +
11:                                 "Database=San"))
12:        {
13:            connection.Open();
14:            SqlCommand command =
15:                new SqlCommand("UPDATE bonus SET amount = amount * 2",
16:                               connection);
17:            command.ExecuteNonQuery();
18:        }
19:    }
20: }

DB 接続 (connection) の扱い方は C# の using 構文を使うと面倒が少なくてすみます (8~12, 18行)。SQL の実行は,コマンド (command) の生成と ExecuteNonQuery メソッドの実行のたった2ステップで完了してしまいます。

このようにパラメータを持たない,更新系の SQL は非常に簡単ですが,実際にはパラメータの定義とバインド,検索系であれば,取得結果の読み取りなど,もう少し複雑になるのが普通です。

(2) パラメータつき SQL の実行

次は,パラメータの例です。

 1: using System;
 2: using System.Data.SqlClient;
 3:
 4: public class SanCorporation
 5: {
 6:     public static void Main()
 7:     {
 8:         using (SqlConnection connection =
 9:                new SqlConnection("Data Source=.\\SQLEXPRESS; " +
10:                                  "Integrated Security=True; " +
11:                                  "Database=San"))
12:         {
13:             connection.Open();
14:             SqlCommand command =
15:                 new SqlCommand("UPDATE bonus SET amount = amount * @rate " +
16:                                "WHERE rank >= @rank",
17:                                connection);
18:             command.Parameters.Add("@rate", 3);  // .NET 2.0 では Add → AddWithValue
19:             command.Parameters.Add("@rank", 7);  // .NET 2.0 では Add → AddWithValue
20:             command.ExecuteNonQuery();
21:         }
22:     }
23: }

SQL に @変数名 の形でパラメータを埋め込んでおき,後に ParametersCollection を通じて値をセットしています (18~19行)。

(3) DataReader による結果の取得

検索系の SQL を発行し,データベースからデータを取得する方法として,大きく DataReader を用いて読み取る方法と,DataSet を用いる方法の二通りがあります。まずは DataReader を用いて取得する例です。

 1: using System;
 2: using System.Data.SqlClient;
 3:
 4: public class SanCorporation
 5: {
 6:     public static void Main()
 7:     {
 8:         using (SqlConnection connection =
 9:                new SqlConnection("Data Source=.\\SQLEXPRESS; " +
10:                                  "Integrated Security=True; " +
11:                                  "Database=San"))
12:         {
13:             connection.Open();
14:             SqlCommand command =
15:                 new SqlCommand("SELECT * FROM bonus WHERE amount >= 50000 " +
16:                                "ORDER BY amount DESC",
17:                                connection);
18:             SqlDataReader reader = command.ExecuteReader();
19:             while (reader.Read())
20:             {
21:                 Console.WriteLine("id=" + reader["id"] + ", " +
22:                                   "amount=" + reader["amount"]);
23:             }
24:             reader.Close();
25:         }
26:     }
27: }

上記例のように,DataReader を用いてデータを取得するには,コマンドの ExecuteReader メソッドによって SQL を発行します (18行)。また,Read メソッドによって1行1行読み進めながら,インデクサを用いて個々の値を取得することができます。

(4) DataSet による結果の取得

DataSet とデータベースの間でデータをやり取りするには,DataAdapter を用います。DataAdapter は,DataReader を使用して結果を読取り,DataSet にデータを充填するという処理を行います。

DataSet にも大きく2種類の使い方があります。ひとつは DataSet をそのまま利用する方法で,もうひとつは DataSet から専用のクラスを派生させて利用する方法です。前者を「型指定されていない DataSet」,後者を「型指定された DataSet」と呼びます。

型指定された DataSet は DataSet を派生した専用クラスですが,実際にプログラマがコーディングする必要はなく,Visual Studio のウィザードで簡単に作成することができるようになっています。今回は,もっぱら型指定されていない DataSet を使用しますが,コード例は後ほど示します。

(5) データプロバイダ依存のコーディングについて

ADO.NET のサンプルプログラムは,このセミナーで提示しているものも含めて,データプロバイダに依存するコードを多用しているものがほとんどです。が,少しの手間を惜しまなければ,汎用性を持たせることも可能です。そのためには,SqlCommand などの具象クラス名の代わりに System.Data 名前空間に用意されている,IDbCommand などのインターフェース名を使用してプログラムを記述します。ただし,.NET 2.0 以前はファクトリが用意されていなかったので,そこだけはプログラマが Abstract Factory パターンを応用するなどして何とかしなければなりませんでした。この欠点は .NET 2.0 では解消されています。(System.Data.DbProviderFactoryクラス)

ただし,プログラムコードを共通化しても,SQL やパラメータ名などの制約がデータベースシステム毎に異なりますので,結局は何らかの依存性が残ることになります。

・トランザクション

同一のデータを複数のプログラムあるいはオペレータが無秩序に操作すれば,さまざまな不都合が発生します。また,ある一連の操作を実行中にシステムがダウンした場合,データが矛盾した状態になってしまうことがあります。このような不都合が発生しないようにするためには,他のプログラムの影響を受けたくない一連の独立した処理単位を定義し,その実行中は他のプログラムを待ち合わせたり,いつシステムダウンしても,元に戻せるようなバックアップを残しておくなどの制御が必要となります。この独立した処理単位のことをトランザクションと呼びます。

たとえば,ある銀行の当座預金から普通預金に 100万円を移し変えるという処理を考えて見ます。この処理と前後して,ちょうど会社から給与20万円の振込の処理も発生したとしましょう。これを何の工夫もなしに行ってしまうと,たとえば次のような順序で処理が進むかも知れません。何が起こるか考えてみてください。

順序 当座→普通処理 給与振込処理
1 当座預金残高をAに読込む  
2 Aが100万以上かチェック  
3 Aから100万円を減算  
4   普通預金残高をXに読込む
5 Aを当座預金残高に書込む  
6   Xに振込金額20万円を加算
7 普通預金残高をBに読込む  
8   Xを普通預金残高に書込む
9 Bに100万円を加算  
10 Bを普通預金残高に書込む  

1. トランザクションとは

(1) ACID特性

トランザクションは,次の4つの特性を満たさなければならないとされています。

・原子性 (Atomicity)

トランザクションは,分割することができない処理単位です。トランザクションが成功したとき,含まれるすべての処理が成功したことになりますし,トランザクションが失敗したときは,すべての処理が一切行われていない状態になっています。途中まで成功した状態でトランザクションが終わることはできません。

・一貫性 (Consistency)

トランザクションの実行前も実行後も,データの整合性を保っていなければなりません。トランザクションの実行の前後で矛盾があってはならないということです。

・隔離性 (Isolation)

トランザクションは互いに他のトランザクションに影響を与えないということです。前述の例のようなことがあってはならないということです。

・持続性 (Durability)

トランザクションの結果は保存されなければなりません。トランザクションの完了後,いつのまにかトランザクションが取消されているなどということがあってはならないということです。

(2) ローカルトランザクションとグローバルトランザクション

通常,データベースシステムには自分自身の管理するデータに対するトランザクションを保証する仕組みを備えています。このような自分だけに限定したトランザクションのことをローカルトランザクションと呼ぶことがあります。

それに対して,たとえば銀行口座から引出す処理と,発注テーブルにレコードを挿入する処理を不可分に行いたいというような場合,複数のシステムにまたがったトランザクションが必要となります。このようなトランザクションをグローバルトランザクションと呼びます。

(3) プログラマティックなトランザクションと宣言的トランザクション

明示的トランザクションと暗黙的なトランザクションという言い方をすることもあります。

・プログラマティックなトランザクション

トランザクション制御の開始と終了をプログラムの処理として実行するという意味です。実際にコーディングするときは C# の using キーワードが役に立ちます。

・宣言的トランザクション

たとえば,このメソッドはトランザクションとして実行する,というような宣言に基づいて制御されるトランザクションのことです。トランザクションの設計からシームレスに実装に繋がるし,何よりトランザクションの終了し忘れなどを未然に防止することができるというメリットがあります。

2. データプロバイダのトランザクション

たとえば .NET Framework Data Provider for SQL Server の場合,SqlTransaction というクラスが用意されています。このオブジェクトを用いてトランザクション制御を行うことができますが,この場合,データベースシステム (SQL Server) のローカルトランザクションをプログラマティックに制御することになります。

3. EnterpriseServices (COM+)

.NET 以前から Windows には COM+ という仕組みが用意されており,VisualBasic などで宣言的トランザクションを利用することができました。.NET でもこの仕組みを利用することができ,「属性」でトランザクションを宣言することが可能です。

この方式には欠点もいくつかあります。

・この仕組みは MS DTC (Microsoft Distributed Transaction Coordinator) を利用しており,不必要に分散トランザクションが発生することがあります。このため,効率が低下しがちです。

・COM+ サービスを利用するためには,コンポーネント化が必須です。アセンブリを分離した上で,署名する必要があります。

ターゲットが Windows 2003 Server の場合は後者の欠点は克服することができます。Windows 2003 Server では,アプリケーションプログラムの一部をコンポーネントとして実行するという機能が使えます。この機能は System.EnterpriseServices.ServiceDomain クラスで制御することができます。ただし,この場合「宣言的なトランザクション」とは言えなくなります。

4. TransactionScope

.NET 2.0 の新機能として追加された仕組みです。この機能は,プログラマティックなトランザクションと宣言的トランザクションの中間的な制御,いわば宣言的トランザクションをプログラマティックに制御する機能となっています。また,ローカルトランザクションと分散トランザクションを適切に自動的に切り替える機能を持っています。

.NET 2.0 以降では,この機能以外は忘れてしまって構わないのではないかと思います。

トランザクションのスコープとして,次の3種類が用意されています。

・Required … このスコープには,トランザクションが必要です。アンビエント トランザクションが既に存在する場合は,アンビエント トランザクションを使用します。それ以外の場合は,スコープに入る前に新しいトランザクションが作成されます。これは既定値です。

・RequiresNew … スコープに対して,常に新しいトランザクションが作成されます。

・Suppress … スコープ内では,アンビエント トランザクションのコンテキストは抑制されます。スコープ内のすべての操作は,アンビエント トランザクションのコンテキストを使用せずに行われます。

エンタープライズアプリケーション・アーキテクチャーパターン入門

 はじめに

 本資料は、株式会社アクシオン・ソフトウェアが主催するSAN(SaturdayActiveNetwork)で行う、勉強会用の資料である。 「エンタープライズアプリケーションアーキテクチャ入門」の書籍から、勉強会用にピックアップしたものになります。

対象読者としては、ビジネスアプリケーションに携わるプログラマ、設計者、およびアーキテクトとしている。 また、JavaC#の知識があることが必要である。

本資料では、開発者が早期に確認したいと思っている決定事項やアプリケーションの主要となるパーツについて、パターンを通じて説明する。 また、エンタープライズアプリケーションとしているため、給与計算・診療記録・出荷管理・会計・顧客サービスなどのアプリケーションを対象とする。

アプリケーションアーキテクチャ

アーキテクチャは、システムを開発・存続していく上で、重要な要素である。 しかし、なかなか決定できない要素でもある。 アーキテクチャには少なくとも2つの要素があると考えられる。 1つは、システムから個々のパーツへブレークダウンできること、1つは、簡単には変更できない決定事項であるということである。 また、1つのシステムには、複数のアーキテクチャが存在してもよく、システムの存続期間中に変わることもありえる。

レイヤ化

レイヤ化は、ソフトウェアシステムの分割のために使用する一般的な技法である。

以下のようなメリットがある。

      各レイヤは、その下位のレイヤについてのみ知っていればよく、さらにその下位のことは知らなくてもソフトウェアが構築できるのである。 たとえば、イーサネットの動作を知らなくても、TCPの上のFTPサービスを構築できる。

      レイヤを入れ替えることも可能となり、柔軟なシステムを構築することができる。 たとえば、FTPサービスは、イーサネット・PPP・ケーブル会社が変わっても、実行に支障はない。

また、デメリットもある。

レイヤを追加するとパフォーマンスを損ねる可能性がある。

 

        

 

1.        3層アプリケーションアーキテクチャ

 これまでのクライアント/サーバシステムでは、ファットなクライアントがUIおよびコントロールをつかさどり、データベースサーバが存在する形であった。 Webの発展に伴い、UIはブラウザで行うことが主流となり、また、オブジェクト指向の普及に伴って、3層のアーキテクチャが注目されるようになった。 ここでの3層とは、プレゼンテーションレイヤ(UI)・ドメインレイヤ・データソースレイヤとなる。 プレゼンテーションレイヤでは、画面の入力・表示を主体とし、ドメインレイヤでは、そのシステムのビジネスロジックを担当する。 データソースレイヤは、DBMSまたはメッセージングシステム、トランザクションモニタ、ファイルシステム、その他のパッケージとの通信などを行う。 プレゼンテーションレイヤとドメインレイヤが分離したことで、プレゼンテーションレイヤに依存しないドメインレイヤの構築が可能となった。 また、クライアント/サーバシステムでは、必然的に、クライアントとサーバの構成をとることになるが、3層のアーキテクチャでは、クライアントであるブラウザとサーバサイドのプレゼンテーションレイヤが分離されるほか、DCOMJ2EEWebサービスなどの普及にも伴い、ドメインレイヤの物理的な配置にも自由度があがっている。

図1を参照。

 

 


1 レイヤと物理的配置

 

 



2.        ドメインレイヤ

 ドメインレイヤの構築について、3つの主要なパターンに分類して考える。

 

 

2.1.       トランザクションスクリプト

 プレゼンテーションレイヤからの入力に従い、妥当性検証・計算やデータベースアクセス、他システムの操作の呼び出しなどを行い多くのデータを伴ってプレゼンテーションレイヤに応答する。 

 

 トランザクションスクリプトには、以下の長所がある。

      開発者の大半が理解できる手続き型のモデルである。

      トランザクションの境界がはっきりしている。 処理の開始と終了がその境界となる。

      後に述べる行データゲートウェイやテーブルデータゲートウェイなどのシンプルなデータソースレイヤと連結する。

 また、短所を挙げる。

      同じような機能では、コードが重複することが多い。

      上述より保守性にかける。 これは、開発者が増えれば増えるほど、保守費用の増大を招く。


 

 

 

 

 



2.2.       ドメインモデル

 主にドメイン内(ビジネス内)の名詞について体系化したモデル。

たとえば、賃貸システムなら、賃貸借契約、資産などのクラス分けを行い、妥当性確認や計算のロジックは各クラスに配置する。

 

 ドメインモデルには、以下の長所がある。

      ドメインに変更や追加があったときに、対象オブジェクトの変更や追加を行うことで対応可能となる。

      ドメインモデルになれてくれば、複雑なロジックでも体系化された多くの技法(継承やストラテジ、デザインパターンなど)が存在する。

      関連する複数のドメインから、利用することができる。

 また、短所を挙げる。

      手続き型からのパラダイムシフトとなり、慣れるまでに時間を要する。

 


 

 

 

 



2.3.       テーブルモジュール

 データベーステーブルやビューのレコードセットに関するビジネスロジックを扱うひとつのインスタンス。

 ドメインモデルでは、契約ごとにインスタンスがあるのに対して、テーブルモジュールでは、1つのインスタンスしかない。 クライアントは、契約に関する操作を呼び出していろいろなことが行える。 個別の契約に対しての操作では契約IDを渡さなければならない。

 テーブルモジュールは、さまざまな意味でトランザクションスクリプトとドメインモデルの中間に位置する。

 

テーブルモジュールの長所を述べる。

      直線的な手続きではなく、テーブルを中心にドメインロジックを構築することで、構造がわかりやすく、重複の発見と削除が容易になる。

      多くのGUI環境は、レコードセットにまとめられたSQLクエリの結果にしたがって動作する。テーブルモジュールは、レコードセット上で動作するため、容易に処理できる。

      MicrosoftCOM.NETは、この開発スタイルを採用している。

また、短所を挙げる。

      ドメインロジックが持つ多くの技法(継承やストラテジ、デザインパターンなど)は使用できない。

      大きなレコードセットを扱ってしまうことがある。

 


 



      選択

 3つのパターンからどうやって選ぶか。 主に、ドメインロジックの複雑度によって決まることと考えられる。

 ドメインモデルはシンプルなドメインロジックに対しては魅力的ではない。 しかし、他の手法では、ドメインロジックが複雑になると飛躍的に難しくなる傾向がある。

 図に、科学的ではないが3つのパターンに関するドメインロジックの複雑性と労力の関係を表す。


 

 


 ドメインモデルに習熟したチームであれば、初期コストがかからないため、ドメインモデルを採用することが望ましいと考えられる。 テーブルモジュールの魅力は、共通のレコードセット構造を環境がサポートしているかどうかで異なる。 レコードセット用の多くのツールが存在する.NETの場合、とても魅力的である。

 これら3つのパターンは、相互に排他的な選択肢ではない。 ドメインロジックの一部にトランザクションスクリプトを使用し、それ以外にテーブルモジュールまたはドメインモデルを使用することは珍しくない。

 


3.        サービスレイヤ

 ドメインレイヤを2つに分割する共通の手法がある。 サービスレイヤは、ドメインモデルまたはテーブルモジュールの上に配置される手法である。 プレゼンテーションロジックは、アプリケーションのAPIの役割を果たすサービスレイヤだけを通してドメインとやり取りする。

 サービスレイヤは、明確なAPIを提供するだけでなく、トランザクション制御やセキュリティの確保のための最適な場所となりうる。 .NETでは属性で直接記述することができる。

 振る舞いに関して2つのパターンが存在する。

サービスレイヤをファサードとするパターンでは、実際の振る舞いはすべて下位のオブジェクトにあり、サービスレイヤは、下位のオブジェクトに呼び出しを転送する。トランザクションラッパーやセキュリティチェックなどを追加するのに便利である。

 次に、サービスレイヤ内に、トランザクションスクリプトを置き、シンプルなドメインモデルを呼び出すパターンがある。 ドメインモデルがシンプルなためデータベースと11の関係になり、シンプルなデータソースレイヤを使用することができる。

 

 


4.        リレーショナルデータベースへのマッピング

 現在のシステムの大半においてデータベースとはリレーショナルデータベースを指している。 このため、ドメインロジックがリレーショナルデータベースとやり取りするためのアーキテクチャに関するパターンを説明する。

 


4.1.       ゲートウェイ

 SQLをプログラミング言語に組み込む手法はあるが、チューニングやインデックスの配置などにかかわるトラブルが発生する。 このため、ドメインロジックとSQLアクセスを分離し、異なるクラスに配置することは懸命である。

 これらのクラスを組織化する適切な方法は、データベースの構造に基づいて、テーブル毎に1つのクラスを持つように設計することである。 これらのクラスはそのテーブルに対するゲートウェイを形成する。 これにより、アプリケーションはSQLについて知る必要はなく、SQLはすべて容易に見つけられる。

 

4.2.       行データゲートウェイ

 クエリから返された行ごとにインスタンスを持つオブジェクトである。 オブジェクト指向の考え方に自然に適合する手法である。


 

 


4.3.       テーブルデータゲートウェイ


 レコードセットを持つゲートウェイ。 テーブルデータゲートウェイは、データベースを検索し、レコードセットを返すメソッドを持つ。 また、ストアドプロシージャのコレクションをテーブル用のテーブルデータゲートウェイと考えることもできる。 この場合、テーブルデータゲートウェイは、ストアドプロシージャの呼び出しをラップする。

 

 

 

 



4.4.       アクティブレコード

テーブルまたは行をラップし、データベースアクセスをカプセル化してデータにドメインロジックを追加するオブジェクト。 シンプルなドメインロジックでは、この手法は有効であ る。