ページ番号とインデックス

細かい話なんですが、どうインターフェースを定義しようか頭を悩ませてきたので、書きながら整理。

何らかのファイルの「複数のページ情報を管理」するためのクラスを書いています。内部では何らかのコレクションクラスで管理する事になるのですが、管理している単一ページ、または複数ページの情報へのアクセス方法を提供する際に、ユーザには「ページ番号」で指定させるのか「インデックス」で指定させるのかと言う事で悩み始めました。

ステップ 1

最初は、「インデックスなんて実装内部の事情でしかないのだから、ページ番号で指定させるべきだ」と言う気がしたので、ページ番号で指定させていました。

public class Document {
    public IDictionay<int, Page> Pages { get; }
    public void TransformPage(int pagenum) { /* ... */ }
}

まぁいいかなと思ってたのですが、Pages プロパティの返すコレクションがちょっとまずかったようで、実際に使っているとフラストレーションが溜まる場面も出てきました。

var doc = new Document();
var page = doc.Pages[1]; // 1 ページ目の情報にアクセス
doc.TransformPage(1);    // 1 ページ目の情報に何らかの変更を加える

// foreach 問題
foreach (var page in doc.Pages) { SomeMethod(page.Value); }  // こうか
foreach (var page in doc.Pages.Values) { SomeMethod(page); } // こうしか書けない
// foreach (var page in doc.Pages) { SomeMethod(page); }     // こう書きたい

ステップ 2

これは IDictionary<int, Page> (中身は SortedDictionary<int, Page>) で管理して、それをそのまま公開しているのは間違いだったなとの思いに達し、SortedDictionary から List で管理する形に改めました。

public class Document {
    public IList<int> Pages { get; }
    public void TransformPage(int pagenum) { /* ... */ }
}

これで、foreach 等によるアクセスは直感的になりました。しかし、次の問題として「ページ番号によるアクセスとインデックスによるアクセスが混在する」と言う問題が発生しました。

var doc = new Document();
var page = doc.Pages[0]; // こっちはインデックスによるアクセス
doc.TransformPage(1);    // こっちはページ番号によるアクセス

これは、そのうち、絶対にミスを犯す予感がします。しかし、この状態で統一させるためには TransformPage メソッドを「ページ番号」指定から「インデックス」指定に変更するのが素直な方法のような気がするのですが、添え字を記述する際ならまだしも、このメソッド名だと間違って「ページ番号」を記述してしまう事も十分考えられます。

ステップ 3

さて、どうしたものかと考えていたのですが、「ランダムアクセスは潰してしまうか」と言う考えが頭をよぎりました。

public class Document {
    public ICollection<int> Pages { get; } // もしくは IEnumerable<int> か
    public void TransformPage(int pagenum) { /* ... */ }
}

これで foreach の問題(ステップ 1)とページ番号指定とインデックス指定が混在する問題(ステップ 2)が解決しました。ただ、やはり各ページ情報へランダムアクセスを行う方法が存在しないのは辛いので、ランダムアクセス専用のメソッドを追加してみます。

public class Document {
    private List<Page> _pages = new List<Page>();
    public ICollection<int> Pages { get { return _pages }; }
    public Page GetPage(int pagenum) { return _pages[pagenum - 1]; }
    public void TransformPage(int pagenum) { /* ... */ }
}

ユーザコードは以下のような形。

var doc = new Document();
var page = doc.GetPage(1); // ページ情報にランダムアクセス(ページ番号指定)
doc.TransformPage(1);      // 1 ページ目の情報に何らかの変更を加える

foreach (var page in doc.Pages) { SomeMethod(page); }

まだ、どうなんだろうなぁ……と言うもやもやした気持ちが取れないのですが、今のところは取りあえずここまで。