次の方法で共有


働くプログラマ

マルチパラダイムと .NET (第 2 部)

Ted Neward

このシリーズの第 1 部である前回のコラム (msdn.microsoft.com/magazine/ff955611) で、Microsoft .NET Framework の中心となる 2 つの言語、C# と Visual Basic は、C++ と同様、マルチパラダイム言語であると説明しました。C++ は C# の構文上の前身であり、Visual Basic の概念上の前身です。マルチパラダイム言語を使用すると、特に、パラダイムの目的が明確でないときは、わかりにくくなり、混乱を招く可能性があります。

共通性と可変性

これらの言語におけるさまざまなパラダイムについて詳しく説明する前に、1 つの大きな疑問点について考えてみましょう。それは、「ソフトウェア システムを設計するときは、正確には何を行おうとしているのか」という疑問点です。ここでは目標となる "最終結果" (モジュール性、拡張性、単純性など) は横に置き、言語のかかわり方に注目します。つまり、「このような目標となる "最終結果" すべてを、正確にはどのようにして実現しようとしているのでしょうか。」

この問題については、James O. Coplien の著書『マルチパラダイム デザイン』(ピアソンエデュケーション、2001 年) で、次のような 1 つの答えが示されています。

「物事を抽象的にとらえるときは、細かいことにはとらわれないで、共通する事象に注目します。ソフトウェアの抽象化を正しく行うには、課題全体を十分に理解し、対象となる関連項目の共通点と、項目ごとに異なる詳細を把握する必要があります。対象となる関連項目をまとめてファミリと呼び、個々のアプリケーションではなく、このファミリをアーキテクチャと設計の対象範囲にします。ファミリのメンバーが、モジュール、クラス、関数、プロセス、型のいずれであっても、共通性/可変性のモデルを使用できます。このモデルは、どのようなパラダイムでも機能します。共通性と可変性が、大半の設計手法の中核です。」

ここで、従来のオブジェクト パラダイムに目を向けてみましょう。オブジェクト指向の開発者は、早い段階から、システム内の "名詞を特定" して、特定のエンティティを構成する要素を調べるように教わります。たとえば、システム内で "teacher" に関連する要素をすべて洗い出し、それらを Teacher という名前のクラスに配置します。しかし、いくつかの "名詞" が関連する動作を重複して含む場合 (たとえば、"student" には "person" と重複するデータと操作が含まれているが、明確な違いもいくつかある場合など)、共通するコードを複製するのではなく、その共通性を基本クラスに昇格し、継承を利用して型を相互に関連付けることを教わります。つまり、共通性はクラス内に集約し、可変性はそのクラスから拡張してバリエーションを取り込むことでキャプチャします。システム内の共通性と可変性を見つけ出して表現することが、設計の中核となります。

共通性は、多くの場合、明確に特定するのが難しい部分です。これは、共通性を見分けられないからではなく、非常に簡単かつ直感的に見分けることができても、見極めるのが難しいためです。たとえば、"乗り物" と言ったときに、何が頭に浮かびますか。あるグループでこの実験をしたら、皆それぞれ異なったイメージを思い浮かべるでしょうが、それらすべてには大まかな共通性があります。しかし、思い浮かべた乗り物を一覧にすると、さまざまな可変性が明らかになり、分類されます (分類されるはずです)。それでも、それぞれの乗り物の間になんらかの共通性を見いだすことはできます。

正の可変性と負の可変性

可変性には 2 つの基本的な形式があり、一方は認識が容易で、もう一方は困難です。正の可変性は、基本となる共通性に追加する形式で生じる可変性です。たとえば、SOAP メッセージ、電子メールなどのメッセージの抽象化を行おうとしているとします。Message 型にヘッダーと本文を含め、さまざまな種類のメッセージがこの型を共通性として使用すると決めた場合、ヘッダーで特定の値 (送信日時など) を送信するメッセージが正の可変性になります。正の可変性は、通常、言語構造 (オブジェクト指向パラダイム) に簡単に取り込むことができます。たとえば、送信日時のサポートを追加する Message サブクラスを作成するのは、比較的簡単です。

しかし、負の可変性は厄介です。おわかりかもしれませんが、負の可変性では、共通性のある側面が削除されたり、その側面に相反したりします。たとえば、ヘッダーを含むが本文は含まない Message 型 (メッセージング インフラストラクチャで使用される確認メッセージなど) は、負の可変性の形式です。また、既にお気付きでしょうが、負の可変性を言語構造に取り込むときには問題が生じます。C# でも Visual Basic でも、基本クラスで宣言されているメンバーを削除することは簡単ではありません。この場合にできるとしたら、Body メンバーから null または nothing を返すことぐらいです。しかし、Body が適切に転送されたことを確認するために Body で CRC を実行する検証ルーチンなど、Body が存在することを前提とするコードでは大きな問題が発生する可能性があります。

(興味深いことに、XML スキーマ型は、スキーマ検証の定義で負の可変性を提供します。この定義は、主流となっているプログラミング言語では提供されないので、XML スキーマ定義がプログラミング言語に適合しない理由の 1 つになっています。これが今後、まだ作成されていないプログラミング言語の機能になるかどうか、そして含めるに値するかどうかは、ビールを飲みながら話すには最高に面白い話題です。)

多くのシステムでは、クライアント レベルで明示的なコード構造を使用して負の可変性を処理することがほとんどです。つまり、Body を検証する前に if/else などのテストを実行して、どのような Message 型 であるかを確認するのは、Message 型のユーザーの役割で、Message ファミリに格納されているものから無関係なものは除外して処理します。通常、設計の例外となる負の可変性があまりに多いと、開発者が "すべて投げ出してもう一度やり直す" ことになる根本的な原因になります。

共通性と可変性のバインド

実際に共通性と可変性を設定するタイミングは、各パラダイムに応じて異なります。通常、これらの決定のバインドが実行時に近いほど、顧客やユーザーのシステムの進化全般に対する制御がいっそう強化されることになります。特定のパラダイムまたはパラダイム内の手法について説明するには、可変性を次の 4 つの "バインド タイミング" のいずれで適用するかを認識することが重要です。

  1. ソース作成時: これは、コンパイラを起動する前に、開発者 (またはその他のエンティティ) が、最終的にコンパイラに入力されるソース ファイルを作成するタイミングです。コード生成手法 (T4 テンプレート エンジンなど) や、ASP.NET システム (それほど重要性は高くありません) は、ソース作成時のバインドで操作します。
  2. コンパイル時: 名前からわかるように、このバインドは、ソース コードを処理するコンパイラの実行中に行われ、コンパイルされたバイトコードまたは実行可能な CPU 命令に反映されます。このタイミングでは、すべてではありませんが、大量の決定が完了します。
  3. リンク/読み込み時: プログラムを読み込んで実行する際、読み込まれる特定のモジュール (.NET の場合はアセンブリ、ネイティブ Windows コードの場合は DLL など) に基づいて、適用する可変性を追加します。通常、このような手法がプログラム全体にわたって適用されるとき、プラグインまたはアドイン スタイルのアーキテクチャと呼ばれます。
  4. 実行時: プログラムの実行中、ユーザーの入力や決定に基づいて、特定の可変性を取り込むことができます。また、その決定や入力に基づいて、別のコードを実行する (または生成する) こともできます。

場合によっては、設計プロセスをこのような "バインド タイミング" から開始し、要件をサポートできる言語構造を考えるというように、逆方向に実行します。たとえば、ユーザーは、コンパイル サイクルを繰り返したり、コードを再度読み込んだりする必要がないように、実行時に可変性を追加、削除、および変更できるようにすることも考えます。これは、設計者が使用するパラダイムにかかわらず、パラダイムでは実行時に可変性のバインドをサポートする必要があることを意味します。

課題

前回の記事で、皆さんに次のような問題を提起したままでした。

「では問題です。ジェネリック (パラメーター化された型) が、.NET Framework 2.0 で導入されました。なぜでしょう。設計の観点から見た場合、ジェネリックが導入された目的は何でしょう (念のために記しておくと、「タイプ セーフなコレクションを使用できるようにするため」という答えは間違いです。Windows Communication Foundation では幅広くジェネリックが使用されており、明らかにタイプ セーフなコレクションではない方法でも使用されています)。」

少し押し進めて考えてみましょう。図 1 の Point クラスの実装 (一部) をご覧ください。画面のピクセル座標や従来のグラフのように、デカルト座標の X と Y で位置を表しています。

図 1 Point クラスの実装の一部

Public Class Point
  Public Sub New(ByVal XX As Integer, ByVal YY As Integer)
    Me.X = XX
    Me.y = YY
  End Sub

  Public Property X() As Integer
  Public Property y() As Integer

  Public Function Distance(ByVal other As Point) As Double
    Dim XDiff = Me.X - other.X
    Dim YDiff = Me.y - other.y
    Return System.Math.Sqrt((XDiff * XDiff) + (YDiff * YDiff))
  End Function

  Public Overrides Function Equals(ByVal obj As Object) As Boolean
    ' Are these the same type?
    If Me.GetType() = obj.GetType() Then
      Dim other As Point = obj
      Return other.X = Me.X And other.y = Me.y
    End If
    Return False
  End Function

  Public Overrides Function ToString() As String
    Return String.Format("({0},{1}", Me.X, Me.y)
  End Function

End Class

これ自体は、それほど興味深いものではありません。実装の他の部分は、ここでは重要ではないので、皆さんのご想像にお任せします。

この Point クラスの実装では、Point の使用方法にいくつか前提があることに注目してください。たとえば、Point の X と Y の要素は整数なので、この Point クラスで (0.5,0.5) などの小数位置を表すことはできません。最初はこの決定が受け入れられるかもしれませんが、必然的に、(理由は何であれ) "Point を小数で表す" ことができるようにしたいという要求が高まりますそこで、開発者は興味深い課題に直面します。この新しい要件を実現するには、どうすればよいでしょうか。

基本から説明すると、「そんなことをしてはいけません」というような処理を実行し、整数ではなく浮動小数点数を使用する新しい Point クラスを作成するだけです。このコードを見てみましょう (図 2 を参照してください。PointD は "Point-Double" の略称で、Double を使用することを意味します)。完全にお気付きでしょうが、この 2 つの Point 型の間には、概念上重複する部分がたくさんあります。これは、設計の共通性/可変性の理論から考えると、共通部分をある程度取り込み、可変性を適用する必要があることを意味します。従来のオブジェクト指向であれば、継承を使用してこれを実現するでしょう。つまり、共通性を基本クラスまたはインターフェイス (Point) に昇格して、サブクラス (PointI、PointD など) に実装します。

図 2 浮動小数点数を使用した新しい Point クラス

Public Class PointD
  Public Sub New(ByVal XX As Double, ByVal YY As Double)
    Me.X = XX
    Me.y = YY
  End Sub

  Public Property X() As Double
  Public Property y() As Double

  Public Function Distance(ByVal other As Point) As Double
    Dim XDiff = Me.X - other.X
    Dim YDiff = Me.y - other.y
    Return System.Math.Sqrt((XDiff * XDiff) + (YDiff * YDiff))
  End Function

  Public Overrides Function Equals(ByVal obj As Object) As Boolean
    ' Are these the same type?
    If Me.GetType() = obj.GetType() Then
      Dim other As PointD = obj
      Return other.X = Me.X And other.y = Me.y
    End If
    Return False
  End Function

  Public Overrides Function ToString() As String
    Return String.Format("({0},{1}", Me.X, Me.y)
  End Function

End Class

ただし、これを実行すると、興味深い問題が発生します。まず、X プロパティと Y プロパティには関連する型が必要になりますが、2 つの異なるサブクラスの可変性は、ユーザーが X 座標と Y 座標をどのように格納して表すかに左右されます。設計者は常に、最も規模が大きく、広範で、包括的な表現を選択するだけです。それが今回は Double です。ただし、Double を選択することで、整数値のみを含む Point を使用する選択肢が失われ、継承で許可する予定だったすべての処理を無意味にすることになります。また、Point の継承による 2 つの実装は、継承で関係付けられているため、相互に置き換えることが可能だと思われるようになります。そのため、PointD を PointI の Distance メソッドに渡すことができますが、これは望ましい場合と望ましくない場合があります。そして、PointD の (0.0, 0.0) は、PointI の (0,0) と (Equals で示されるように) 同等なのでしょうか。このような問題はすべて検討する必要があります。

このような問題はある程度解決でき、扱いやすい部類ですが、他にも問題が発生します。後になって、Integer に格納できる値よりも大きな値を格納する Point が必要になる可能性があります。つまり、正の絶対値 (座標の左下隅を原点とします) のみ渡すことができると見なします。このようなさまざまな要件は、Point の新しいサブクラスを作成する必要があることを意味します。

少し前に戻ってみると、本来の目的としては、Point の実装の共通性を再利用して、Point を構成する値の型や表現に可変性を適用することでした。理想としては、使用するグラフに応じて、Point の作成時に表現を選択できるようにして、その表現自体を明確に異なる型として表すことで、ジェネリックとしての役割を正確に果たします。

ただし、これを実現するには問題があります。コンパイラが、"Rep" 型に必ずしも "+" 演算子と "-" 演算子を定義する必要がないと判断します。ここには可能な型 (Integer、Long、String、Button、DatabaseConnection など、思い付くあらゆる型) であれば何でも渡され、間違いなく変数のように扱われると判断するためです。そこで、もう一度、"Rep" 型に含まれるジェネリック制約に基づいて、ここで使用できる型に対して共通性を表現する必要があります (図 3 参照)。

図 3 型のジェネリック制約

Public Class GPoint(Of Rep As {IComparable, IConvertible})
  Public Sub New(ByVal XX As Rep, ByVal YY As Rep)
    Me.X = XX
    Me.Y = YY
  End Sub

  Public Property X() As Rep
  Public Property Y() As Rep

  Public Function Distance(ByVal other As GPoint(Of Rep)) As Double
    Dim XDiff = (Me.X.ToDouble(Nothing)) - (other.X.ToDouble(Nothing))
    Dim YDiff = (Me.Y.ToDouble(Nothing)) - (other.Y.ToDouble(Nothing))
    Return System.Math.Sqrt((XDiff * XDiff) + (YDiff * YDiff))
  End Function

  Public Overrides Function Equals(ByVal obj As Object) As Boolean
    ' Are these the same type?
    If Me.GetType() = obj.GetType() Then
      Dim other As GPoint(Of Rep) = obj
      Return (other.X.CompareTo(Me.X) = 0) And (other.y.CompareTo(Me.Y) = 0)
    End If
    Return False
  End Function

  Public Overrides Function ToString() As String
    Return String.Format("({0},{1}", Me.X, Me.Y)
  End Function

End Class

この場合、2 つの制約が課せられています。一方の制約では、(2 つの位置の距離を計算するため) "Rep" 型を double 値に変換できるようにします。もう一方の制約では、2 つの値のどちらが大きいか、小さいか、または 2 つの値が等しいかを確認するために、構成要素の X 値と Y 値を比較できるようにします。

ジェネリックが導入された理由が明らかになってきました。ジェネリックは、設計に対して可変性の異なる "軸" をサポートします。これは、従来の継承ベースの軸とは大きく異なります。設計者はこの軸を利用して、実装を共通性として、実装に基づいて操作できる型を可変性として扱うことができます。

この実装では、リンク/読み込み時や実行時ではなく、コンパイル時に可変性が発生することを前提としていることに注意してください。ユーザーが Point の X/Y メンバーの型を実行時に指定する必要がある場合は、別のソリューションが必要になります。

まだ終わっていません

すべてのソフトウェア設計を共通性と可変性における主要な演習と考えれば、マルチパラダイム設計について理解する必要があることは明らかです。この共通性と可変性を実現するため、各パラダイムにはそれぞれ異なる方法が用意されているため、複数のパラダイムを混在させると、混乱を招いたり、全面的な書き直しが必要になったりします。頭の中で 3 次元の構造を 4 次元や 5 次元に当てはめようとすると、脳が混乱し始めるように、ソフトウェアの多方面にわたる可変性は、混乱を引き起こします。

この先 6 回程度のコラムで、C# と Visual Basic でサポートされている各パラダイム (プリンシパルに基づいた、構造、オブジェクト指向、メタプログラミング、機能的、および動的の各パラダイム) で、共通性を取り込み可変性を適用する機能を提供する方法をいくつか紹介します。これらすべてを紹介するときに、モジュール化でき、拡張可能で、管理しやすいといったメリットを備えた設計を構築するために、これらのパラダイムを組み合わせる方法について、興味深い方法で調べていきます。

コーディングを楽しんでください。

Ted Neward は、Microsoft .NET Framework および Java のエンタープライズ プラットフォーム システムを専門とする独立企業 Neward & Associates の社長を務めています。これまでに 100 個を超える記事を執筆している Ted は、C# MVP であり、INETA の講演者でもあります。さまざまな書籍を執筆および共同執筆していて、『Professional F# 2.0』(Wrox、2010年、英語) もその 1 つです。彼は定期的にコンサルティングを行い、開発者を指導しています。質問やコンサルティングの依頼については、ted@tedneward.com (英語のみ) に電子メールを送信してください。ブログを blogs.tedneward.com (英語) に公開しています。

この記事のレビューに協力してくれた技術スタッフの Anthony Green に心より感謝いたします。