DB 操作の視点からオブジェクト指向を考える2013年06月23日 22時18分58秒

シナリオ

出席簿をつけるアプリケーションを想定してみることにします。まずマスターとして、学生、講師、講義の 3つが存在するものとします。

  • 学生

    • 学籍番号 (PK)
    • 氏名
    • 性別
    • 学年
    • etc...
  • 講師

    • 教員番号 (PK)
    • 氏名
    • 性別
    • 役職
    • etc...
  • 講義

    • 講義識別番号 (PK)
    • 講義名
    • 担当講師 (教員番号)
    • 場所
    • 時間割
    • 期間
    • 単位数
    • 概説
    • etc...

それから、マスター以外のテーブルとして、受講者リストと出席記録があるものとしましょう。

  • 受講者リスト

    • 講義識別番号 (PK)
    • 受講者 (学籍番号) (PK)
    • 点数
    • 成績 (優|良|可|不可)
    • etc...
  • 出席記録

    • 講義識別番号 (PK)
    • 受講者 (学籍番号) (PK)
    • 講義回数 (N回目) (PK)
    • 遅刻 (時刻)
    • etc...

さて、ログインした講師が受け持つ講義の 1つを選択し、新たに出欠をとる画面を開いたとします (講義回数は自動でカウントされるべきでしょう)。その画面には講義に参加する全ての学生が学籍番号順にリストアップされており、点呼を受けて講師自らが学生たちの出欠状況を入力していくものとします。これをどうプログラムで表現し、実装するべきか。

ありがちな過ち

オブジェクト指向をメタファー (隠喩) の表現のために用いること自体は必ずしも間違いではありませんが、メタファー自体も手段の一つと捉えるべきです。メタファー自体が目的になってしまうと、パフォーマンスが犠牲になることが多くなります。

例えば学生と講師はどちらも人なので、 Person クラスで抽象化して StudentTeacher に派生しよう、などと考えること自体は悪くないかも知れません。

// 人を表す抽象クラス
public abstruct class Human
{
    // 人を特定する番号 (学籍番号、教員番号等)
    public long Number { get; protected set; }
    
    // 名前
    public string Name { get; protected set; }
    
    // 性別 ("M": 男性 / "F": 女性 / "O": その他)
    public string Sex { get; protected set; }
    
    // etc...
}

// 学生クラス
public class Student : Human
{
    // 学年
    public int? Year { get; private set; }
    
    // ...
}

// 講師クラス
public class Teacher : Human
{
    // 役職 (11:助教 / 16:講師 / 21:准教授 / 26:教授 / 51:名誉教授 / 56:客員講師 / 99:その他)
    public int Roll { get; private set; }
    
    // ...
}

あー、ちなみに言語は C# で書いてます。テストしてませんが。

で、ここまでは良いんですが、ありがちな誤りとして、それぞれのクラスのコンストラクタに DB からの読み込み機能を持たせちゃうという試みがあります。

using Db = MyNameSpace.DbAccessor;

public class Student : Human
{
    // ...
    
    // デフォルトコンストラクタ (マスタ登録時用)
    public Student() { }
    
    // コンストラクタ (学籍番号から情報を取得)
    public Student(long number)
    {
        DataTable table = Db.StudentTable.SelectByPk(number);
        if (table == null || table.Rows.Count == 0)
            return;
        
        DataRow row = table.Rows[0];
        Number = number;
        Name = row["STUDENT_NAME"] as string;
        Sex = row["STUDENT_SEX"] as string;
        Year = row["STUDENT_YEAR"] as int?;
        // ...
    }
    
    // ...
}

さすがに面倒くさいので Teacher の方の実装例は割愛。

何故にこれがアカンのかと言いますと、まぁ例外との兼ね合いというのもあるのですが、何よりあまりにも気兼ねなく使え過ぎちゃって、知らず知らずのうちに無駄な DB アクセスが増えてしまう要因になりうるからです。あと、実際の使用に際しては実はあまり使われることのない存在になる可能性も小さくありません。

「データ取得用」のクラスは別に設ける

シナリオを読み返しましょう。講師が出欠をとるために開いた画面では、その講義への参加を登録している全ての学生が、学籍番号順にリストアップされるとしています。そのような複数の学生を特定する処理は、DB 側での SQL に任せてしまった方が効率がよいのは当然です。そこで、特定の講義に参加する学生のリストを取得するファクトリクラスを考えます。

using Db = MyNameSpace.DbAccessor;

public class Student : Human
{
    // ...
    
    // コンストラクタ
    internal Student(DataRow row)
    {
        Number = (long)row["STUDENT_NUMBER"];
        Name = row["STUDENT_NAME"] as string;
        Sex = row["STUDENT_SEX"] as string;
        Year = row["STUDENT_YEAR"] as int?;
        // ...
    }
    
    // ...
}

// 学生情報管理クラス - とりあえずファクトリとして実装
public class StudentManager
{
    // 学籍番号を指定して学生情報を取得
    public static Student Get(long number)
    {
        DataTable table = Db.StudentTable.SelectByPk(number);
        if (table == null || table.Rows.Count == 0)
            return null;
        
        return new Student(table.Rows[0]);
    }
    
    // 講義を指定して学生情報のリストを取得
    public static Student[] ListUpByLecture(int number)
    {
        // 特定の講義番号で登録されている受講者の学籍番号を受講者リストテーブルから
        // リストアップし、その学籍番号を用いて学生テーブルから学生の情報を引っ張る、
        // までを全部 SQL でやっつけてデータを返すメソッドを想定
        DataTable table = Db.LectureMembersTable.SelectByLecture(number);
        if (table == null)
            return new Student[] {};
        
        var students = new List<Student>();
        foreach (DataRow row in table.Rows)
            students.Add(new Student(row));
        
        return students.ToArray();
    }
}

Student のコンストラクタを internal にするのがミソです。こうすることで、同一ファイル以外からのアクセスがコンパイラによって禁止されます。それによって、 StudentManager クラスのメソッドを介さなければ Student のコンストラクタは呼べない、すなわち Student クラスのインスタンスを生成できないようになるわけです。同様のことは、 Java ならクラス内クラスにして protected で、 C++ なら friend を使う等で実現可能でしょう。

デフォルトコンストラクタは省いたのではなく敢えて書いていません。多分、マスタへの変更をする目的でわざわざ Student インスタンスを生成するべきではありません。理由はいくつかあって、例えばマスタ登録のためだけにアクセサのセッターまで public にしてしまうとメンテナンス性が損なわれるのではとか、そうではなくて全部の情報を引数に渡すコンストラクタを設けるぐらいならデータ管理用のクラス (まさにここで言うところの StudentManager クラス) にて全部の情報を引数に渡すメソッドを書くんでも変わらないよねとか。ついでに INSERT はもとより UPDATE するためにわざわざ SELECT で情報を全部引っ張ってきてからそれをコード上で上書きして UPDATE を呼ぶとかするんでなしに最初っから PK だけ指定して UPDATE だけ呼んだ方が速いに決まってるよねとか。

NULL 可能なフィールドとかもあるし全部を引数に渡すメソッドは現実的じゃないだろうとか言うんであれば、内部データだけを持つデータ構造を定義して分離可能にしてしまっても良いと思う。こんな感じで。

public class HumanData
{
    public string name;
    public string sex;
    // ...
}

public class StudentData : HumanData
{
    public int year;
    // ...
}

public abstruct class Human
{
    // フィールド構造体アクセサ
    protected HumanData Fields { get; set; }
    
    public long Number { get; protected set; }
    
    public string Name
    {
        get { return Fields.name; }
        protected set { Fields.name = value; }
    }
    
    public string Sex
    {
        get { return Fields.sex; }
        protected set { Fields.sex = value; }
    }
    
    // ...
}

public class Student : Human
{
    public int Year
    {
        // こういうダウンキャストは酷くダサイですが…
        get { return (Fields as StudentData).year; }
    }
    
    // ...
    
    internal Student(DataRow row)
    {
        Number = (long)row["STUDENT_NUMBER"];
        
        var fields = new StudentData();
        fields.name = row["STUDENT_NAME"] as string;
        fields.sex = row["STUDENT_SEX"] as string;
        fields.year = (int)row["STUDENT_YEAR"];
        // ...
        Fields = fields;
    }
}

public class StudentManager
{
    // ...
    
    // 学生の情報をマスタに登録し、学籍番号を返す
    long RegistNewStudent(StudentData data)
    {
        // ...
    }
}

まぁ、この辺は好き好きで…。

マスタデータをプールする

頻繁に見には行くけどそうそう書き換わることのないデータであり、それをプールさせておく時間をそれほど長くないセッション単位に限定するのであれば、マスタデータのメモリ上でのプールはそれほど悪くないアイデアだと思います。

public class StudentManager
{
    // プール用コンテナ
    Dictionary<long, Student> pool = new Dictionary<long, Student>();
    
    // プールをクリアする
    public static void ClearPool()
    {
        pool.Clear();
    }
    
    // 学籍番号を指定して学生情報を取得
    public static Student Get(long number)
    {
        if (pool.ContainsKey(number))
            return pool[number];
        
        DataTable table = Db.StudentTable.SelectByPk(number);
        if (table == null || table.Rows.Count == 0)
            return null;
        
        return pool[number] = new Student(table.Rows[0]);
    }
    
    // 講義を指定して学生情報のリストを取得
    public static Student[] ListUpByLecture(int number)
    {
        // 特定の講義番号で登録されている受講者の学籍番号を受講者リストテーブルから
        // リストアップし、その学籍番号を用いて学生テーブルから学生の情報を引っ張る、
        // までを全部 SQL でやっつけてデータを返すメソッドを想定
        DataTable table = Db.LectureMembersTable.SelectByLecture(number);
        if (table == null)
            return new Student[] {};
        
        var students = new List<Student>();
        foreach (DataRow row in table.Rows)
            students.Add(pool[(long)row["STUDENT_NUMBER"]] = new Student(row));
        
        return students.ToArray();
    }
}

但し、どういうタイミングで StudentManager.ClearPool() を呼ぶべきかをちゃんと考えて使わないと、サーバー上で起動しっぱなしのプロセスでプールにデータがたまりっぱなし、しかもマスタメンテ画面で学生情報を修正したのに (学年とか) その変更が画面に反映されないぞー何でだーみたいなバグを誘引する原因になったりもするので、気をつけて使う必要はあると思います。特にグループ開発では。

まとめ

そんなわけで、結局のところ何が言いたいのかというと、オブジェクト指向というのは必ずしもメタファーのためだけにあるわけではないよということです。最近ではよく、「オブジェクト=物である、という説明を真に受けてはいけない」などと言われるようになりましたが、それは何でかというと、業務シナリオの中に出てくるような、実在する物だけをオブジェクトとして表現しようとすると、そのオブジェクトのクラスの中だけで何でもかんでもやり遂げなきゃならないような気になってしまうからです。学生を表現したいからと言って、必ずしも学生クラスの中だけで DB アクセスまで完結する必要はないわけです。あくまで、プログラム上での目的に応じたオブジェクト設計を心がけるべきなのです。