Archive | 07:55

規約、実装、アルゴリズム

14 12月

今日は抽象化のお話です。

オブジェクト指向っぽいことしようぜぇ?

コレクションと列挙

昨日・一昨日で説明したコレクションですが、そのうちいくつかの概要を、1枚の絵で表してみましょう。

並べてみると、共通する性質が見えてきます。このくくりでいうと、要素の列挙がすべてのコレクションに共通しています。そこを抜き出してみましょう。

「オブジェクト指向入門」的な記事でよく見かける図が出来上がりました。共通部分はインターフェイス化して実装しましょう!

抽象化

こういう、共通部分のくくりだしのことを抽象化(abstraction)と言います。

数学でよく使う手口です。どうしてそんなことをするのかというと、一見全然違うように見えるものに対して、同じ定理・同じアルゴリズムを適用できるから。証明が1回で済むわけです。

まあ、2例ほど出しましょう。

素因数分解

因数分解、高校で嫌というほどやらされたと思います。やってる最中の方とかもいらっしゃるかな。まだ中学生以下の人はごめんなさい。先に高校の範囲を勉強すると後が楽ですよ!

整数の因数分解(素数の積に分解)ってのと、多項式の因数分解(1次式の積に分解)っての、両方やったと思います。同じことができるってことは、何らかの同じ性質を持っているということです。

満たすべき条件までは真剣に読まなくてもいいですが、その整数と多項式に共通する性質ってのをくくりだしたのがユークリッド整域という抽象概念です。

さて、ユークリッド整域であれば、ユークリッド互除法というアルゴリズムを使って、最大公約数を求めることができます。整数の最大公約数の例はよく見ると思いますが、あのアルゴリズムは実は、多項式とか、別のものにも使えます。

内積

ベクトルとか複素数を勉強したとき、「なんか似てる」と思ったことはありませんか。あるいは、人によっては、「数列や実関数もベクトルだ」と思うかもしれません。そう感じる程度には、共通の性質があります。その1つが内積です。

「内積を定義できる」という共通項でくくりだした結果、どれでも成り立つ共通の定理が見つかります。三角不等式やシュワルツの不等式がそうなんですが、ベクトルだけでなく、数列や複素数でも、適宜内積が定義すれば成立します。

※スペースの都合でいくつか条件等をはしょっています。

規約、実体、そして、アルゴリズム

さて、話を戻しましょう。

最初に提示した、クラスとインターフェイスの図だと欠けているものがあります。数学の例との対比でいうと、クラスが具象概念、インターフェイスが抽象概念です。つまるところ、付随した定理・アルゴリズムの部分が抜けています。

ということで、改めて、図に追加してみましょう。

 

インターフェイス(抽象概念=規約)

プログラミングにおける抽象化は、規約(どういうメソッドを持つべきかという宣言)だけを定めることで行います。

public interface IEnumerable<out T>
{
    IEnumerator<T> GetEnumerator(); //
規約だけ
}

 

クラス(具象概念=実体)

具象概念は、実体(メソッドの中身)を持ったクラスです。

public class List<T> : IEnumerable<T>
{
    public IEnumerator<T> GetEnumerator()
    {
//
実体
        for (int i = 0; i < count; i++)
            yield return data[i];
    }
後略
}

 

実体を伴わないと、具体的なもの(インスタンス)を作れません。

関数(アルゴリズム)

そして、オブジェクト指向の教科書で抜けがちなのがこの概念。インターフェイスの定める規約を応用したアルゴリズムの類は、”純粋な”関数で実装できます。

C#の場合は、クラスに属さない関数を作れないので、静的メソッドを使います。

public static IEnumerable<T> Where<T>(
    this IEnumerable<T> items, Func<T, bool> condition)
{
    foreach (var item in items)
        if (condition(item))
            yield return item;
}

 

純粋な関数

ここで、「純粋な関数」(pure function)という表現を使ったのには理由があります。静的メソッドは、静的なフィールドを参照するかどうかで性質が異なります。

// 純粋な関数(pure function)。
// 
無害な静的メソッド。
// 
同じ引数を与えれば、常に同じ結果が返ってくる。
// 
テストしやすい。
static int Square(int x) { return x * x; }

// 状態を持った静的メソッド
// 
呼び出しのたびに結果が変わる。
// 
しかも、自分のあずかり知らぬところで誰かが呼ぶことで、結果が変わることがある。
// 
テストしにくい。
static int GetNext() { return count++; }
static int count = 0;

 

静的なフィールドを一切参照せず、同じ引数を与えれば常に同じ結果が返ってくるもののことを「純粋な関数」と呼びます。テストのしやすさを考えると、静的メソッドは純粋な関数として作る方が好ましいです。

静的メソッドにする意味

アルゴリズムをクラスのインスタンス メンバーとしてではなく静的メソッドで実装しているのには理由があります。インターフェイス(規約)とクラス(実体)とアルゴリズムを独立させたいからです。

あるインターフェイスに対して、それを実装したクラスはいろんな場所で作られます。アセンブリやプロジェクトをまたいで、全然違うところで全然違う人が実装することもあります(というか、その方が普通です)。アルゴリズムも同様で、インターフェイスを決めた人でもなければ、クラスを実装した人でもない、全然別の人が実装します。

そのことを考えると、特定のクラスのインスタンス メンバーとして実装するよりも、別途静的メソッドを用意した方が、依存関係がなくてすっきりします。