nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

オブジェクトのライフタイム パターンを整理してみた

システムやアプリケーションを構築していると、様々なパターンのオブジェクトの生成~消滅に遭遇します。

  • ユーザーが何らかのアクションを起こし、それに伴うインタラクションが完了するまでの間だけ生存するオブジェクト。
  • アプリケーション起動から終了までの間、常に生存するオブジェクト。

私は常々、オブジェクトの生成~消滅までのライフタイムは、いくつかのパターンに分類できると考えてきました。そのパターンはユーザーインターフェースのアーキテクチャが異なっても、概ね類似したパターンの内に収束すると考えています。スマートデバイスのネイティブ実装でも、Windowsアプリでも、Webアプリでも、です。

そこで改めてオブジェクトの生成~消滅までのライフタイム パターンをまとめ、その留意点や基本原則について、自分の経験に基づいて整理してみました。異論・反論もちろんあると思います。きっとそれは本エントリーを洗練化する一助になると思いますので、ぜひコメントなりTwitter(@nuits_jp) なりにご意見ください。

なお本エントリーのライフタイムの名称は完全に(もしくは一部は)オレオレ用語で一般的な概念とは限りませんので、取り扱いにご注意ください。

さて、アプリケーション内で取り扱うオブジェクトの多くは(もしくは全ては)以下の何れかに分類することができると考えています。

これらのライフタイムは「インタラクション」が最も短く、下に行くほど長くなります。

ここにさっそく、第一の基本原則があります。

「オブジェクトのライフタイムは必要な範囲で、可能な限り短いものを選ぶべきである」

これは、純粋にコード上の変数のライフタイムが狭い方が良い事と同義です。では具体的に個別のライフタイムを見ていきましょう。

インタラクション

ユーザーもしくは何らかのアクションに対して、システムがリアクションを返すまでの間に必要とされるオブジェクトのライフタイムです。

たとえば、「ボタンが押された、何らかの処理をして画面に結果を表示する」といったライフタイムを指します。

多くは次のいずれかの形で利用されます。

  • メソッドの引数
  • メソッドのローカル変数

逆に、次のようなコードはアンチパターンです。

ここではユーザーを登録するpublicメソッドが存在したとします。そのメソッドでは、まずはパスワードの整合性をチェックした上で以降の処理を行うものとします。では実際にコードを見てください。

public class UserManager
{
    private string _password;

    public bool RegisterUser(string userName, string password)
    {
        _password = password;
        if (!CheckPassword()) return false;
        // 以降登録処理
    }

    private bool CheckPassword()
    {
        if (8 < _password.Length)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

CheckPasswordメソッドで必要とするパスワード値をUserManagerクラスのフィールドに保持して処理しています。こういったコードを見かける事がありますが、明らかなアンチパターンです。これは幾つかの問題を引き起こす可能性があります。

  • RegisterUserがマルチスレッドで実行される可能性がある場合、タイミングによりパスワードチェックが正しく動作しない
  • 不要となったタイミングで再初期化されておらず、意図しないタイミングで利用すべきではない値が利用される

他にもあるかもしれません。「インタラクション」ライフタイムで扱われるオブジェクトは、基本的にメソッドの引数か、ローカル変数として利用するべきです。

今回の場合、引数として渡すべきでフィールドへ保管すべきではないでしょう。つぎのように実装すべきです。

public void RegisterUser(string userName, string password)
{
    if (!CheckPassword(password)) return false;
    // 以降登録処理
}

private static bool CheckPassword(string password)
{
    if (8 < password.Length)
    {
        return true;
    }
    else
    {
        return false;
    }
}

なお、例えばローカル変数上で構造化されたオブジェクトを利用し、そのオブジェクトのメンバとして値を保持していると言った場合は問題としません。ローカル変数上に確保されているオブジェクトと同じライフタイムとなるからです。

ビュー

ここでは「ビュー」と表現していますが、WebのページやWindowアプリケーションのウィンドウやダイアログも同様です。その画面内の状態を表すオブジェクトのライフタイムです。

たとえば、MVVMパターンの場合、Viewと1:1に対応するトップレベルのViewModelと、そのViewModelに保持・管理されているメンバーが該当します。

トップレベルのViewModelは、対応するViewとライフタイムを合わせる必要があるでしょう。そうしないと

「前の操作で入力した値が、次の処理時に不正に表示される」

という状態が起こりえます。

とはいえ、それほど難しい話ではありません。例えばMVVMの場合は、Viewの生成時にViewModelのインスタンスも併せて生成してバインドすれば良いだけです。DIコンテナでViewModelを管理しているような場合は、シングルトンにせず、Viewの生成時にViewModelを割り当てる際に、都度インスタンスが生成されるように管理すればよいでしょう。

Webアプリケーションの場合、1ページが該当します。ビューを表示している間、ブラウザで表示されているHTMLやJavaScriptのオブジェクトが該当するでしょう。

ユースケース

さてユースケース ライフタイムの話をする前に、ユースケースそのものの定義が必要かもしれません。ここではUMLのユースケース図や、ユースケース駆動開発などで言われているユースケースを指しています。

つまりユースケースを「ユーザーに対して価値を提供するアクションのシーケンスの集合」として定義します。機能と一見近しいですが、例えば「顧客を登録する」はユースケースですが、「郵便番号から住所を検索する」はユースケースでは(通常は)ありません。前者はシステムのユーザーに対して、システムを利用する最終的な価値を提供しています。つまり顧客マスターの登録です。しかし後者は住所を検索するだけでは最終的な価値をユーザーに提供しません。これは単純に「機能」でありユースケースではありません。つまりユースケースは、一つ以上の機能の集合であるともいえます。

ユースケースを発見するにあたり「利用者がその処理をしたら、しばらくシステムから離れても問題のない単位」を考えると分かりやすいかもしれません。

さてアプリケーションでは、一つのユースケースを実現するにあたって、複数のViewの組み合わせによって実現する事もあるでしょう。
ユースケース ライフタイムとは、ユースケースが開始してから完了するまでに保持するものを指します。

これは、UIのアーキテクチャによって実現方法に注意が必要です。

たとえばクライアントアプリケーションで、DIコンテナを利用しているような場合、ビュー単位のオブジェクト(MVVMではViewModel)にユースケースを表すオブジェクトをインジェクションすることで実現する事になるでしょう。

この時、ユースケース オブジェクトは、画面をまたがって再利用される必要があるため、コンテナのライフタイムとしてはシングルトンを採用するケースがままあります。そのこと自体は問題ありませんが、ユースケース完了時にどのようにユースケースを破棄するか、十分に検討する必要があるでしょう。ユースケース オブジェクトのフィールドを全てクリアする方法でも構わないのですが、フィールドが追加された時にクリアし忘れると、前の作業で利用した値が、次の作業で表示されてしまうといった不具合を招きがちです。過去にこのケースのバグで何度煮え湯を飲まされたか分かりません。

コンテナからいったんインスタンスを削除してしまい、必要となったら再作成するのが理想的でしょう。具体的な実装はUIの実装方式やフレームワーク、利用するDIコンテナによって異なりますので、別の機会に紹介したいと思います。

さて、Webアプリケーションの最近の潮流は良く分かりません。しばらくまともにWebアプリケーション作ってません。太古の昔は、値自体はHTTP Sessionにいれておいて、HTTP Sessionから値を出し入れするFacadeをユースケース毎に作り、ユースケース完了時にHTTP Sessionからまとめて削除するのもFacadeに任せていましたが、今はもっとスマートな方法があるかも知れませんね。最近では再規模なサイトではHTTP Sessionではなく、Redisとか使うのかな?

またWebアプリケーションの場合、HTTP Sessionに(多分Redisもかな?)保持させる値の扱いには十分気を配る必要があります。あまりに便利な箱過ぎて、明確な方針無く使っているとすぐにリソースの開放漏れが発生したり、ユーザーから見るとStaticなKey-Valueストアのため、Keyの重複などにも気を配る必要があります。また、セッションはサーバーサイドのメモリー上に維持されるため、安易に使うとすぐにリソース不足に悩まされることになりますし、大規模なシステムではセッションのレプリケーションなども考慮する必要があるでしょう。(私自身Webアプリ開発から離れすぎているのもあるので)ここでは詳細を述べることはできませんが、十分に注意の上利用してください。

セッション

ユーザーがアプリケーションを利用開始してから終了するまで維持されるライフタイムです。Webアプリケーションの場合のHTTP Sessionとほぼ等価でしょう。

  • ログインユーザー情報
  • ユースケースを跨って再利用されるキャッシュ情報(マスターとか)

などが該当するでしょう。後者はアプリケーションを終了してもストレージに保持しておくこともありますが、その場合はアプリケーション ライフタイムに該当します。

クライアントアプリケーションの場合、DIコンテナにシングルトンとして突っ込めばいいんじゃないですかね(適当、いや適切な意味での適当ですよ)。

Webアプリケーションの場合、ログインユーザー情報のような個人に紐づくものは、単純にHTTP Sessionに保持してもよいでしょうし、ユーザー間で共有して利用できるようなオブジェクトのキャッシュは、Redisやそれ以外のアーキテクチャも利用し、リソースを効率的に使いながらキャッシュ管理をすることになるのではないでしょうか。

特にキャッシュはリソースの使用量とパフォーマンスがトレードオフになることが多いため、求められる非機能要件と利用可能なリソース(お金や時間)と相談の上アーキテクチャを決定する必要があり、案外難しい課題ではないでしょうか。

アプリケーション

アプリケーションまたはシステムが起動してから終了するまで維持されるライフタイムです。
これは単一のユーザーが利用するクライアントアプリケーションの場合、実質的にセッションと同等となるケースも多いでしょう。
Webアプリケーションの場合、ユーザー間を跨って、システムで共通に維持されるオブジェクトです。
たとえば、DBへの接続情報のような設定値が該当するでしょう。

設定値クラスを作って(.NETの場合、大抵はプラットフォームで提供されていますが)、それを利用しても良いのですが、直接利用するのは避けて、インターフェースベースでDIできるようにした方が良いケースも多いでしょう。
主にテスタビリティを確保するためです。
設定値の変更に基づいてアプリケーションが適切に振舞うかどうか?を自動テストしようとした場合、設定ファイルを直接読み書きしているレイヤーのクラスを使うとテストが困難になることが多いでしょう。

とりあえず今回は以上です。
ご意見・ご感想をお待ちしています。