i-Vinci TechBlog
株式会社i-Vinciの技術ブログ

オブジェクト指向で考えるデータ永続化

皆様こんにちは。i-Vinci の下位です。

ここ最近寒いなぁと思っていたら、東京では雪まで降ってしまいました。
私、雪があまり降らない地域出身なので、降雪までいくと少々テンションが上がります。
仕事休んで雪合戦したい

さて、今日のテーマです。
ビジネスアプリなら必ず遭遇するであろう「データ永続化」を題材に考えてみます。

データの永続化について考える

永続化について意味を調べてきました。

永続性とは、物事が長続きする性質やその度合い。IT 分野では、コンピュータプログラムの処理対象となるデータが、プログラムの終了やコンピュータの電源断後も消滅せずに存続する性質を指すことが多い。

引用元:IT 用語辞典 e-Words 永続性

ざっくりといえば、プログラム終了後も「データが消えないこと」を示します。

業務要件で出てくる「データの永続化」はプログラム終了後もデータが消えなければ OK ということです。

本日の例題

本日取り扱う仮想テーマです。
下記要件を満たすサービス処理について考えてみます。

1)汎用入力フォームを作りたい。
2)画面上に氏名、回答内容を設け、入力内容を永続化してほしい。

入力データは下記 JSON を想定するものとします。

// リクエストデータ
{
  "name": "テスト 太郎",
  "answer": "回答内容的ななにか"
}

サンプル実装 part 1

ということで、サンプルです。
サービスロジック以外は重要でないため、記載を省略します。

// リクエストデータのモデル
class Content {
    public string Name {get; set;}
    public string Answer {get; set;}
}

// データ永続化を担うサービスインターフェース
interface IContentService
{
    /**
    ** 引数オブジェクトの永続化を行う
    **/
    void Register(Content content);
}

// データ永続化を担うサービスロジック
class ContentService : IContentService
{
    public void Register(Content content)
    {
        // DB接続文字列を作成する処理
        var connectionString = CreateConnectionString();

        // DB接続取得
        using (var connection = new SqlConnection(connectionString))
        {
            try
            {
                connection.Open();
                // INSERT句の生成・実行
                var command = new SqlCommand(null, connection);
                connection.CommandText = "INSERT INTO Contents VALUES (@name, @answer)";

                var nameParam = new SqlParameter("@name", SqlDbType.Varchar, 20);
                var answerParam = new SqlParameter("@answer", SqlDbType.Varchar, 100);

                nameParam.Value = content.Name;
                answerParam.Value = content.Answer;

                command.Parameters.Add(nameParam);
                command.Parameters.Add(answerParam);

                command.Prepare();
                command.ExecuteNonQuery();
            }
            finally
            {
                connection.Close();
            }
        }
    }
}

はい。DB を使った永続化を前提とするサービスロジックを記載しました。

ビジネスロジックと永続化ロジック、密ではないですか

上記、業務では比較的遭遇しやすいロジックになります。
パラメータに応じて SQL を構築、その後 RDBMS に問い合わせをするロジックです。

因みに、このロジックは下記の問題点を持っています。

  1. 保存先は SQL が利用可能なミドルウェアであることを前提とする(データ永続化先の固定)
  2. サービスロジックが SQL という、ビジネスに直接関係ない知識を有している (データ操作ロジックとの密結合)
1.の問題点

データ永続化先が変更となる場合の変更容易性が保たれていません。
限定的なケースですが、ポータブルモードの様な挙動を実現する際に障壁となりえます。

# 例:ポータブルモードのイメージ
アプリケーション設定または、サーバー疎通状況に応じて、データの保存先を可変とする方法。

* ケース1:任意のミドルウェア(DB等)
* ケース2:テキスト保存(JSON等)
2.の問題点

こちらはビジネス観点で考えた際の問題です。
ビジネス要求は「データの永続化」のみです。サービスロジックは「永続化の手続き」を知っていてはいけません。

「永続化の手続きを知る」ということは、そのロジックを内包することです。
つまり、サービスロジックと永続化ロジックの密結合に陥ってしまうためです。

無論、モジュール同士が密結合になれば、変更容易性は低下します。
将来への不安を拭うのであれば、サービスロジックとデータ操作を分離したいところです。

ではどうすればよいのか

前述の問題点ではサービスロジックが SQL という具体的な手段を明示した為、問題となることを示しました。

ではどうするか。
具体的な手段を明示しなければよいのです。データの保存処理には抽象化を適用しましょう。

抽象化を使った実装

では抽象化を使った実装です。
現実装では MAX1 件しか登録できないですが、抽象化の説明とは関連しない為、省略とします。

まずはデータ永続化ロジックを作成します。
共通の interface を定義し、それぞれの永続化ロジックに実装することが重要です。

interface IContentRepository
{
    /**
     * データ永続化を行うメソッド
    **/
    void Save(Content content);
}

/**
 * DBへの永続化処理を担うロジック
**/
class DBContentRepository : IContentRepository
{
    public void Save(Content content)
    {
        // DB接続文字列を作成する処理
        var connectionString = CreateConnectionString();

        // DB接続取得
        using (var connection = new SqlConnection(connectionString))
        {
            try
            {
                connection.Open();
                // INSERT句の生成・実行
                var command = new SqlCommand(null, connection);
                connection.CommandText = "INSERT INTO Contents VALUES (@name, @answer)";

                var nameParam = new SqlParameter("@name", SqlDbType.Varchar, 20);
                var answerParam = new SqlParameter("@answer", SqlDbType.Varchar, 100);

                nameParam.Value = content.Name;
                answerParam.Value = content.Answer;

                command.Parameters.Add(nameParam);
                command.Parameters.Add(answerParam);

                command.Prepare();
                command.ExecuteNonQuery();
            }
            finally
            {
                connection.Close();
            }
        }
    }
}

/**
 * ローカルリソースへの永続化を担うロジック
 * 当記事ではJSONファイルへの保存を想定。
**/
class LocalContentRepository : IContentRepository
{
    private const string RESOURCE_PATH = "/app/store/resource.json";

    public void Save(Content content)
    {
        string json = JsonSerializer.Serialize(content);
        File.WriteAllText(RESOURCE_PATH, json);
    }
}

では、作成した永続化ロジックをサービスロジックに組み込みます。

// データ永続化を担うサービスロジック
class ContentService : IContentService
{
    public void Register(Content content)
    {
        // 永続化サービスインスタンスを生成。
        // 引数は仮置きとしているが、アプリケーション特性に応じて外部入力を受け入れられるようにすると良い。(App.Settings等)
        IContentRepository repository = CreateRepository("file");

        // 永続化を実行。
        repository.Save();
    }

    /**
     * 引数に応じて永続化サービスインスタンスを生成する
    **/
    private IContentRepository CreateRepository(string type)
    {
        return type == "file"
            ? new LocalContentRepository()
            : new DBContentRepository();
    }
}

はい。実装完了です。
無事、サービスロジックから永続化の手続きを除去できました。

ついでにサービスロジックの複雑度も低減させることに成功しています。一石二鳥ですね。

まとめ

いかがでしたでしょうか。
今回はデータ永続化処理を例に上げて、オブジェクト指向を利用した改善案を提示させていただきました。

今回も記事を執筆して感じましたが、抽象化は非常に強力なツールです。
やり過ぎは危険ですが、適切に利用することでモジュールの安定性を一歩底上げしてくれる機能と強く感じます。

皆様も是非、抽象化などの強力機能を駆使してコーディングに望んでみてください。
簡潔なプログラムは作る側、見る側双方を幸せにしてくれる...筈です!

最後に、今回採用したものは「リポジトリパターン」と呼ばれる、永続化処理の隠蔽を担うデザインパターンになります。
他にも様々なデザインパターンが存在し、どれもモジュール簡潔化に一役買ってくれる素敵な手法になります。
興味のある方は是非、調べてみてください。

参考にさせて頂いたサイト:GoF のデザインパターンまとめ