後方互換性を保つために知っておきたい、やってしまいがちなアンチパターンのメモ。
公開されている定数の値を変更する
定数(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
のメンバーでなくなってしまうため実装側がエラーになります。
IHoge
に new int Foo { get; set; }
といったふうにメンバーを残したところで結局 Hoge
が ISuperHoge.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");
相当のコードに展開されるので、フィールドをプロパティに変換しようとしてしまうと、呼び出し側はフィールドにアクセスしようとするのに定義側にはメソッドしかなくてアクセス、ということになります。
他にもパターン思いついたら増やします。