Joel Spolsky-漏れやすい抽象化の法則
そして時には非常に単純な抽象化
この記事の著者
現代のほとんどの開発者は、ジョエル・スポルスキーの同名の有名な記事の「穴あき抽象の法則」に精通しています。 この法則は、インタラクションプロトコル、フレームワーク、またはサブジェクトエリアをモデル化するクラスのセットがどれほど優れていても、遅かれ早かれ、この抽象化がどのように機能するかに対処する必要があるという事実に基づいています。 抽象化の内部構造は抽象化自体の問題でなければなりませんが、これは最も一般的な場合にのみ可能であり、すべてがうまくいけば可能です(*)。
昔々、「小さな」小さなソフト会社で、彼らはなぜオブジェクトの場所から「抽象化」しないのかを決定し、オブジェクトがローカルであるかリモートであるかという事実を、単なる「実装の詳細」としています。 そのため、DCOMテクノロジとその後継の.NET Remotingが登場しました。これは、オブジェクトがリモートであるかどうかに関係なく、開発者から隠されていました。 同時に、これらすべての「透明なプロキシ」が登場し、リモートオブジェクトを知らなくても作業できるようになりました。 ただし、時間が経つにつれて、リモートオブジェクトは例外の完全に異なるリストを生成できるため、この情報が開発者にとって重要であることが明らかになり、それを操作するコストはローカルオブジェクトとのやり取りよりも比較にならないほど高くなります。
もちろん、このような「情報の隠蔽」は有用ですが、一般的な場合、開発者の生活の複雑化につながり、単純化にはつながりません。 そのため、WCFと呼ばれる分散アプリケーションを開発するための新しいバージョンのテクノロジは、このプラクティスから遠ざかり、ローカルオブジェクトとリモートオブジェクトの間の線は非常に細いままですが、それでも変わりません。
目に見える動作(抽象化)だけでなく、内部構造(実装)を理解する必要がある場合、同様の例がたくさんあります。 ほとんどのプログラミング言語では、さまざまなタイプのコレクションの操作は非常によく似た方法で行われます。 コレクションは、ベースクラスまたはインターフェイスの背後に(「.NET」のように)「隠す」ことも、(C ++などの)他の一般化方法を使用することもできます。 しかし、異なる方法で同じ方法で作業できるという事実にもかかわらず、特定のタイプのコレクションからクラスを完全に「ほどく」ことはできません。 明らかな類似性にもかかわらず、現時点で最も使用されているものを理解する必要があります。ベクトルまたは二重リンクリスト、ハッシュセットまたはソートセットです。 基本的な操作の複雑さは、コレクションの内部実装に依存します。要素を検索し、コレクションの中央または末尾に要素を挿入し、そのような違いを知ることが必要です。
特定の例を見てみましょう。 List < T> (またはC ++のstd :: vector )などの型が単純な配列に基づいて実装されていることは誰もが知っています。 コレクションが既にいっぱいになっている場合、新しい要素を追加すると、新しい内部配列が作成されますが、1つの要素ではなく「やや強い」(**)「成長」します。 多くの人がこの動作を認識していますが、ほとんどの場合、それに注意を払うことはできません。これはList < T>クラスの「個人的な問題」であり、気にしません。
しかし、WCFを介して列挙型のリストを渡すか、 DataContractSerializerまたはNetDataContractSerializer (***)クラスを使用してそのようなリストをシリアル化する必要があると仮定しましょう。 リストは次のように宣言されます。
public enum Color
{
Green = 1,
Red,
Blue
}
* This source code was highlighted with Source Code Highlighter .
この列挙が属性でマークされていないという事実に注意を払ってはいけません;これはNeDataContractSerializerの障害ではありません。 この列挙の主な機能は、null値がないことです。 列挙値は1で始まります。
WCFで列挙をシリアル化することの特性は、この列挙に属さない値をシリアル化できないことです。
public static string Serialize<T>(T obj)
{
// NetDataContractSerializer,
// DataContractSerializer
var serializer = new NetDataContractSerializer();
var sb = new StringBuilder ();
using ( var writer = XmlWriter.Create(sb))
{
serializer.WriteObject(writer, obj);
writer.Flush();
return sb.ToString();
}
}
Color color = (Color) 55;
Serialize(color);
* This source code was highlighted with Source Code Highlighter .
このコードを実行しようとすると、次のエラーメッセージが表示されます。 列挙型の値 '55'は型Colorに対して無効であり、シリアル化できません。 。 このように、異なるアプリケーション間で未知の値を渡すことから身を守るため、この動作は非常に論理的です。
次に、1つのアイテムのコレクションを渡してみましょう。
var colors = new List <Color> {Color.Green};
Serialize(colors);
* This source code was highlighted with Source Code Highlighter .
ただし、この明らかに無害なコードは、同じ内容の実行時エラーにもつながります。唯一の違いは、シリアライザーが列挙値0に対応できないことです。 何のために... 0はどこから来るのでしょうか? この要素の値は完全に正しいですが、1つの要素を持つ単純なコレクションを伝えようとしています。 ただし、 DataContractSerializer / NetDataContractSerializerは 、古き良きバイナリシリアル化と同様に、リフレクションを使用してすべてのフィールドにアクセスします。 その結果、開いたフィールドと閉じたフィールドの両方に含まれるオブジェクトの内部表現全体が出力ストリームにシリアル化されます。
List < T>クラスは配列に基づいて構築されるため、リストに含まれる要素の数に関係なく、配列全体をシリアル化するときにシリアル化されます。 したがって、たとえば、2つの要素のコレクションをシリアル化する場合:
var list = new List < int > {1, 2};
string s = Serialize(list);
* This source code was highlighted with Source Code Highlighter .
出力ストリームでは、予想どおり2つの要素ではなく、4(つまり、 CountではなくCapacityプロパティに対応する要素の数 )を取得します。
< ArrayOfint >
< _items z:Id ="2" z:Size ="4" >
< int > 1 </ int >
< int > 2 </ int >
< int > 0 </ int >
< int > 0 </ int >
</ _items >
< _size > 2 </ _size >
< _version > 2 </ _version >
</ ArrayOfint >
* This source code was highlighted with Source Code Highlighter .
この場合、列挙のリストをシリアル化するときに発生するエラーメッセージの理由が明らかになります。 Colorの列挙には0に等しい値が含まれていません。つまり、リストの内部配列の要素にはこの値が設定されています。
これは、 List < T>のような単純なクラスの内部実装でさえ、通常のシリアル化を妨げることがある場合の、抽象化の「コース」の別の例です。
問題解決
この問題にはいくつかの解決策がありますが、それぞれの解決策には欠点があります。
1. デフォルト値の追加
この問題の最も簡単な解決策は、 0の値を列挙に追加するか、既存の要素の1つの値を変更することです。
public enum Color
{
None = 0,
Green = 1, // Green = 0
Red,
Blue
}
* This source code was highlighted with Source Code Highlighter .
このオプションは最も簡単ですが、常に可能というわけではありません。 列挙値はデータベースの一部の値に対応する場合があり、ダミー値を追加するとアプリケーションのビジネスロジックと矛盾する場合があります。
2. 「空の」要素なしでコレクションを転送する
列挙で何かをする代わりに、コレクションにそのような空の要素が含まれないようにすることができます。 これは、たとえば次のようにして実行できます。
var li1 = new List <Color> { Color.Green };
var li2 = new List <Color>(li1);
* This source code was highlighted with Source Code Highlighter .
この場合、変数li1には3つの追加の空の要素が含まれ(この場合、 Countは1 、 Capacityは 4 )、変数li2は含まれません(2番目のリストの内部配列には1つの要素のみが含まれます)。
このオプションは非常に機能しますが、非常に「脆弱」です。動作中のコードを壊すことは難しくありません。 不要な中間コレクションを削除するという形での同僚側の無害な変更、それがすべてでした。
3. サービスインターフェイスで他のタイプのコレクションを使用する
配列などの他のデータ構造を使用するか、DataContractSerializerの代わりにパブリックメンバーのみを使用するXMLシリアル化を使用すると、この問題を解決できます。 しかし、それはあなた次第です。
抽象化の流れ、期間。 そのため、さまざまなライブラリの内部実装を調べるのが非常に便利です。 このライブラリがすべての詳細を完全に隠していても、遅かれ早かれ、その内部構造の知識がなければ問題を解決できない状況に遭遇します。 借方記入し、内部デバイスに対処し、将来変更されることを恐れないでください。 あなたがそれを必要とするという事実ではありませんが、少なくともそれは面白いです!
Z.Y. ところで、型List < T>の WCFを介して重要な型を渡すには、よく考えてください。 524個の要素のコレクションがある場合、さらに重要なタイプの500個の追加オブジェクトが転送されます!
-
(*)ジョエルは、これらの目的のために優れた比phorを提供する最初の著者ではなく、最後の著者ではありません。 したがって、たとえば、リー・キャンベルはかつて同じことを完全に言っていましたが、わずかに異なる言葉で言いました。「コーディングするレベルよりも少なくとも1つの抽象化レベルを理解する必要があります。」 短いメモの詳細: 抽象化の望ましいレベルの理解について 。
(**)通常、このようなデータ構造は内部配列を2倍にします。 したがって、たとえば、リスト<T>に要素を追加すると、「容量」は次のように変わります。0、4、8、16、32、64、128、256、512、1024 ...
(***)WCFシリアライザーの2つの主要なタイプの違いは非常に重要です。 NetDataContractSerializerは、 DataContractSerializerとは異なり、SOAの原則に違反し、CLRタイプに関する情報を出力ストリームに追加します。これは、「クロスプラットフォーム」サービス指向のパラダイムに違反します。 これについての詳細は、ノート: NetDataContractSerializerの WCFまたは宣言的使用 とは何ですか 。