雑記 - otherwise

最近はDQ10しかやっていないダメ技術者がちまちまと綴る雑記帳

ユニバーサル Windows アプリでのコード共有方法

ユニバーサル Windows アプリでは、 Windows ストアアプリと Windows Phone アプリ で共通なコードを Shared プロジェクトに格納する事でコードの共有が可能ですが、実際の開発では、「クラス全体としては共有したいけど、一部の処理だけプラットフォームに依存する」と云ったケースが多く発生すると思います。
今回は、そうした状況の対応方法を幾つかご紹介します。
# 先日のわんくま同盟東京勉強会 #90 の LT でお話しした内容です。

対応法 (1) : 諦める(共有しない)

# いや、冗談ではなく。
クラスの半分以上が共有不可とか、冷静に見ると実は共有しない方が幸せなのではないか、と云うケースは結構あるものです。
実際のところ、無理に Shared に入れずに各プラットフォームのプロジェクト内にそれぞれ作成した方がいい場合もあると思います。
ちなみに、共有しないでそれぞれにクラスを作成しても、 Shared 内の別のファイルから参照する(インスタンス生成とかメソッド呼び出しとか)する事自体は可能ですし、クラス名やメソッドシグネチャを同じにしておけば、共有コードでインスタンスを生成したりメソッドを呼び出したりする事も出来ます。

\Sample.Windows\Hoge.cs
public class Hoge
{
  public string Foo()
  {
    return "Foo (Windows)";
  }
}
\Sample.WindowsPhone\Hoge.cs
public class Hoge
{
  public string Foo()
  {
    return "Foo (Windows Phone)";
  }
}
\Sample.Shared\AppContext.cs
var hoge = new Hoge();
var result = hoge.Foo();

// [Windows]
//   result : "Foo (Windows)"
// [Windows Phone]
//   result : "Foo (Windows Phone)"

なおこの場合、共有可能なコードもそれぞれに記述する必要がありますが、その辺は基底クラスを作成して共有可能なコードを基底クラス側に記述する等で対応は可能かと思われます。

対処法 (2) : if ディレクティブを利用する

ユニバーサル Windows アプリでのコード共有化では最も一般的でよく紹介されているのがこの方法です。
# ユニバーサルプロジェクトテンプレートで作成されるスケルトンコード内でも、固有ロジックの呼び出しにはこの if ディレクティブが利用されています。
具体的には、 Windows ストアアプリ向けのコードは "WINDOWS_APP" 、 Windows Phone アプリ向けのコードは "WINDOWS_PHONE_APP" の各定数が予め設定されているため、この定数を利用して切り替えを行います。

\Shared\Piyo.cs
public class Piyo
{
  public string Bar()
  {
#if WINDOWS_APP
    return "Bar (Windows)";
#elif WINDOWS_PHONE_APP
    return "Bar (Windows Phone)";
#endif
  }
}
\Sample.Shared\AppContext.cs
var piyo = new Piyo();
var result = piyo.Bar();

// [Windows]
//   result : "Bar (Windows)"
// [Windows Phone]
//   result : "Bar (Windows Phone)"

なお、 if ディレクティブを利用した場合、 Visual Studio や Blend で現在アクティブなプラットフォームを切り替える事で連動してコードのハイライトが切り替わる等のサポートも享受出来るので、特に問題がない限りこの機能を使えばよいのではないかと思われます。

対処法 (3) : partial クラスを利用する

別の方法として、固有ロジックを partial クラスとして切り出すと云う方法も可能です。

\Sample.Shared\Fuga.cs
public partial class Fuga
{
  public string Baz1()
  {
    return "Baz1 (Common)";
  }
}

※共有可能なコードは Shared で記述

\Sample.Windows\Fuga.Windows.cs
public partial class Fuga
{
  public string Baz2()
  {
    return "Baz2 (Windows)";
  }
}
\Sample.WindowsPhone\Fuga.WindowsPhone.cs
public partial class Fuga
{
  public string Baz2()
  {
    return "Baz2 (Windows Phone)";
  }
}
\Sample.Shared\AppContext.cs
var fuga = new Fuga();
var result1 = fuga.Baz1();
var result2 = fuga.Baz2();

// [Windows]
//   result1 : "Baz1 (Common)"
//   result2 : "Baz2 (Windows)"
// [Windows Phone]
//   result1 : "Baz1 (Common)"
//   result2 : "Baz2 (Windows Phone)"

但し、 partial クラスで切り分け可能なのはメソッドまでのため、特定のメソッド内の一部分のみ切り替える場合はその部分を別のメソッドとして切り出す等の工夫が必要です。

\Sample.Shared\Hogera.cs
public partial class Hogera
{
  public string Qux()
  {
    var result = 0;
    // 一部分のみ切り替える場合はメソッド切り出し
    result += Quux();
    // メソッド内に特定のプラットフォームでのみ実行する処理がある場合も partial で書けない事はない、という例 :p
    FooBar(ref result);
    return result.ToString();
  }

  // partial メソッド 宣言
  partial void FooBar(ref int value);
}
\Sample.Windows\Hogera.Windows.cs
public partial class Hogera
{
  private int Quux()
  {
    return 1;
  }

  // FooBar は Windows 側のみ実装
  partial void FooBar(ref int value)
  {
    value += 4;
  }
}
\Sample.WindowsPhone\Hogera.WindowsPhone.cs
public partial class Hogera
{
  private int Quux()
  {
    return 2;
  }
}
\Sample.Shared\AppContext.cs
var hogera = new Hogera();
var result = hogera.Qux();

// [Windows]
//   result : 5
// [Windows Phone]
//   result : 2

もちろん、メソッドレベルでの切り替えは partial 、メソッド内の一部分の切り替えは if ディレクティブ、と云う風に組み合わせて記述する事も可能です。
うまく共有化してコード量を減らして、見通しの良いプログラムを組みたいものですね。