LoginSignup
59
58

More than 5 years have passed since last update.

後方互換性を保つために知っておきたいアンチパターン

Last updated at Posted at 2014-02-14

後方互換性を保つために知っておきたい、やってしまいがちなアンチパターンのメモ。

公開されている定数の値を変更する

定数(const キーワードによって定義されたもの)はビルド時にインラインで埋め込まれるためバイナリ互換性が崩れます。

ビルド前
public const string Foo = "ふー";

public void DoSomething()
{
    Console.WriteLine(Foo);
}
ビルド後
public const string Foo = "ふー";

public void DoSomething()
{
    // ビルド後は呼び出し元にビルド時の値が埋め込まれる
    // アセンブリを跨ぐ場合、リビルドするまで呼び出し元は古い値のままになる
    Console.WriteLine("ふー");
}

そもそも文字通り定数でないものは const で公開するべきではないです。

static readonly で公開するか読み取り専用プロパティにします。
また、private および internal メンバーであればアセンブリを跨がないため問題ないです。

インターフェースにメンバーを追加する

言わずと知れたアンチパターン。
言うまでもなく既にそのインターフェースを実装している型があれば死にます。

変更前
public interface IFoo
{
    void Hoge();
}

public class Foo : IFoo
{
    void Hoge() { ... }
}
変更後
public interface IFoo
{
    void Hoge();
    void Fuga(); // ← メンバーを追加
}

public class Foo : IFoo
{
    void Hoge() { ... }
    // こちらにも Fuga を追加しないとビルドエラーになる
}

省略可能引数を追加する

これはある意味有名ですが知らないとやってしまいがち。
省略可能引数の仕組みを理解していても「うっかり」でやりかねないパターンです。

下記の変更をすると「バイナリレベルでの」互換性が崩れます。
「ソースコードレベルでの」互換性は保たれるので気づきづらいです。
また、裏を返せば呼び出し側を必ずリビルドする運用であれば問題ありません。
(定義側の DLL だけ入れ替えた場合に問題になる。)

呼び出し側
SomeClass.DoSomething("Hoge", 123);
定義側(変更前)
public static void DoSomething(string s, int i)
{
    // 何かする
}

  ↓

定義側(変更後)
public static void DoSomething(string s, int i, bool b = false)
{
    // 何かする
}

ダメな理由

省略可能引数はオーバーロードのシンタックスシュガーではないから。

省略可能引数を持つメソッドを呼び出した場合、呼び出し側はビルド時に既定値が補われます。

呼び出し側(ビルド前)
SomeClass.DoSomething("Hoge", 123);
呼び出し側(ビルド後)
SomeClass.DoSomething("Hoge", 123, false);

逆に、定義側はどうなっているかというと、属性に置き換わります。

定義側(ビルド前)
public static void DoSomething(string s, int i, bool b = false)
{
    // 何かする
}
定義側(ビルド後)
public static void DoSomething(string s, int i, [Optional, DefaultParameterValue(false)] bool b)
{
    // 何かする
}

あくまでメソッドは1つで、ビルド時に呼び出し側に値が埋め込まれるだけなので
定義側の DLL だけを入れ替えると「メソッドが見つかりません」の実行時例外となる。
(呼び出し側は DoSomething(string, int) を呼ぼうとするが
 定義側に DoSomething(string, int, bool) しかなくなるため)

インターフェースの一部を基底インターフェースに抜き出す

これも気づきづらいパターンです。
.NET4.5 で IDictionary<TKey, TValue>IReadOnlyDictionary<TKey, TValue> を継承しなかった理由ですね。

元のインターフェースのメンバーが明示的に実装されていた場合に後方互換性が崩れます。

変更前
public class Hoge : IHoge
{
    string IHoge.Foo { get; set; }
    int IHoge.Bar { get; set; }
}

public interface IHoge
{
    string Foo { get; set; }
    int Bar { get; set; }
}
変更後
public class Hoge : IHoge
{
    string IHoge.Foo { get; set; } // ビルドエラー!!
    int IHoge.Bar { get; set; }
}

public interface IHoge : ISuperHoge
{
    int Bar { get; set; }
}

public interface ISuperHoge
{
    int Foo { get; set; }
}

ダメな理由

明示的な実装を理解していれば、もはやそのままです。
int Foo { get; set; }IHoge のメンバーでなくなってしまうため実装側がエラーになります。
IHogenew int Foo { get; set; } といったふうにメンバーを残したところで結局 HogeISuperHoge.Foo を実装しないためエラーになってしまいます。
つまり、既存のインターフェースはメンバーを増やすのはもちろん(たとえ同じシグネチャのメンバーしか持ってなかったとしても)他のインターフェースを継承するのもダメということです。

public フィールドをプロパティに変える (2016/05/16追記)

省略可能引数の話と似ていますが、「ソースコードレベルでの」互換性は保たれるものの「バイナリレベルでの」互換性が崩れますので呼び出し元のリビルドが必要になります。

// 定義側
public string Hoge { get; set; }

// 呼び出し側
foo.Hoge = foo.Hoge + "bar";

というコードは

// 定義側
private string _hoge;
public string get_Hoge()
{
    return _hoge;
}
public void set_Hoge(string value)
{
    _hoge = value;
}

// 呼び出し側
foo.set_Hoge(foo.get_Hoge() + "bar");

相当のコードに展開されるので、フィールドをプロパティに変換しようとしてしまうと、呼び出し側はフィールドにアクセスしようとするのに定義側にはメソッドしかなくてアクセス、ということになります。


他にもパターン思いついたら増やします。

59
58
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
58