web-dev-qa-db-ja.com

不変性を確保できる場合、カプセル化について考える必要がありますか?

カプセル化

オブジェクト指向プログラミング(OOP)では、カプセル化とは、データをそのデータを操作するメソッドにバンドルすること、またはオブジェクトの一部のコンポーネントへの直接アクセスを制限することを指します。 1 カプセル化は、構造化データオブジェクトの値または状態をクラス内に隠し、不正な当事者がそれらに直接アクセスできないようにします。 Wikipedia-カプセル化(コンピュータプログラミング)

不変性

オブジェクト指向および関数型プログラミングでは、不変オブジェクト(変更不可能なオブジェクト)は、作成後に状態を変更できないオブジェクトです。 Wikipedia-不変オブジェクト

不変性を保証できる場合、カプセル化について考える必要がありますか?

これらの概念がオブジェクト指向プログラミング(OOP)および関数型プログラミング(FP)のアイデアの説明に使用されているのを見てきました。

カプセル化、不変性、およびそれらの相互関係について調査しました。不変性がある場合にカプセル化が保証されるかどうかを明示的に尋ねる投稿は見つかりませんでした。

カプセル化または不変性のトピックについて誤解している場合は修正してください。これらの概念をよりよく理解したいと思います。また、上記の質問に答えるトピックに関して行われた他の投稿に私を誘導してください。

34

質問

質問を実際の生活に当てはめる:

(あなた以外の)誰もそれを変更することができない場合、医師が個人の医療記録をFacebookに一般公開で投稿しても大丈夫ですか?

見知らぬ人が盗んだり、何かを傷つけたりすることができないという条件で、あなたの家に見知らぬ人を入れても大丈夫ですか?

同じことを求めています。あなたの質問の核となる仮定は、データを公開することに関する唯一の懸念はそれが変更されることができるということです。参考資料に従って:

カプセル化は、クラス内の構造化データオブジェクトの値または状態を非表示にするために使用され、不正なパーティからの直接アクセスを防止します。

値や状態を変更する機能は間違いなく最大の関心事ですが、それだけが問題ではありません。 「直接アクセス」には、書き込みアクセスだけではありません。読み取りアクセスも弱点の原因となる可能性があります。

ここでの簡単な例は、一般的にエンドユーザーにスタックトレースを表示しないように勧められていることです。エラーが発生してはならないだけでなく、スタックトレースが特定の実装やライブラリを明らかにすることがあるので、攻撃者がシステムの内部構造を知ることになります。

例外スタックトレースは読み取り専用ですが、システムを攻撃したい場合に役立ちます。

編集
コメントで言及された混乱のため、ここに示されている例は、カプセル化がデータ保護(医療記録など)に使用されることを示唆するものではありません。
これまでのところ、回答のこの部分では、質問の根拠となる中心的な主張のみを取り上げました。つまり、書き込みアクセスなしの読み取りアクセスは害がないということです。これは正しくないと思われるため、単純化した反例。


安全ガードとしてのカプセル化

さらに、書き込みアクセスを防止するために、一番下まで不変である必要があります。この例を見てみましょう:

public class Level1
{
    public string MyValue { get; set; }
}

public class Level2 // immutable
{
    public readonly Level1 _level1;

    public Level2(Level1 level1) { _level1 = level1; }
}

public class Level3 // immutable
{
    public readonly Level2 _level2;

    public Level3(Level2 level2) { _level2 = level2; }
}

Level2Level3に、読み取り専用フィールドを公開させました。これは、質問が安全であると主張していることを実行しています。読み取りアクセス、書き込みアクセスなしです。

それでも、Level3オブジェクトのコンシューマーとして、これを行うことができます。

// fetch the object - this is allowed behavior
var myLevel3 = ...; 

// but this wasn't the intention!
myLevel3.Level2.Level1.MyValue = "SECRET HACK ATTACK!";

このコードはコンパイルされ、完全に正常に実行されます。フィールド(たとえば、myLevel3.Level2)への読み取りアクセスは、オブジェクト(Level2)へのアクセスを提供します。これは、別のオブジェクト(Level1)への読み取りアクセスを公開し、読み取りと書き込みを公開します。 MyValueプロパティへのアクセス。

そして、これはすべてを不変に公に公然と公開する危険です。間違いはすべて表示され、望ましくない動作の可能性があります。簡単に隠されてしまう可能性のあるものを不必要に公開することで、脆弱性が存在する場合、それらを精査して、弱点を悪用することになります。

編集
Calethは、クラス自体は不変ではないものを公開する場合、クラスは不変ではないと述べました。これは意味論的な議論だと思います。 Level2のプロパティは読み取り専用で、表面上は不変です。

公平に言うと、私の例でデメテルの法則が守られていた場合、Level2Level1への直接アクセスを公開しないため、問題はそれほど明白ではありませんでした(ただし、この問題は排除されます)私は強調しようとしていました);しかし、問題の要点は、コードベース全体の不変性を確保しようとするのはばか者の用事であるということです。誰かが単一のクラスで1つの調整を行った場合(他の多くのクラスが何らかの方法で依存している場合)、だれにも気付かれずに、クラス全体のアセンブリ全体が変更可能になる可能性があります。

この問題は、カプセル化の欠如またはデメテルの法則の違反の原因であると主張できます。どちらも問題の原因です。しかし、あなたがそれを事実に起因するとは関係なく、これは間違いなくコードベースの問題であることに変わりはありません。


クリーンなコードのカプセル化

しかし、カプセル化を使用するのはそれだけではありません。

私のアプリケーションが時刻を知りたいと考えているので、日付を知らせるCalendarを作成します。現在、私はこの日付をファイルから文字列として読み取ります(これには正当な理由があると仮定しましょう)。

public class Calendar
{
    public readonly string fileContent; // e.g. "2020-01-28"

    public DateTime Date => return DateTime.Parse(fileContent);

    public Calendar()
    {
        fileContent = File.ReadAllText("C:\\Temp\\calendar.txt");
    }
}

fileContentはカプセル化されたフィールドである必要がありますが、あなたの提案のためにオープンしました。それがどこへ行くのか見てみましょう。

開発者はこのカレンダーを使用しています。ボブのライブラリとジョンのライブラリを見てみましょう。

public class BobsLibrary
{
    // ...

    public void WriteToFile(string content)
    {
        var filename = _calendar.fileContent + ".txt"; // timestamp in filename
        var filePath = $"C:\\Temp\\{filename}";

        File.WriteAllLines(filePath , content);
    }
}

ボブはCalendar.fileContentを使用しました。このフィールドはカプセル化されるべきでしたが、カプセル化されていませんでした。しかし、彼のコードは機能し、結局フィールドは公開されたので、現在問題はありません。

public class JohnsLibrary
{
    // ...

    public void WriteToFile(string content)
    {
        var filename = _calendar.Date.ToString("yyyy-MM-dd") + ".txt"; // timestamp in filename
        var filePath = $"C:\\Temp\\{filename}";

        File.WriteAllLines(filePath , content);
    }
}

JohnはCalendar.Dateを使用しました。これは常に公開されるプロパティです。一見すると、JohnはstringDateTimeに変換し、stringに戻すことによって、不要な作業を行っていると思います。しかし、彼のコードは機能するため、問題は発生しません。

今日、私たちは多くのお金を節約する何かを学びました:あなたはインターネットから現在の日付を得ることができます!毎朝深夜にカレンダーファイルを更新するために、インターンを雇う必要がなくなりました。 Calendarクラスをそれに応じて変更してみましょう。

public class Calendar
{
    public DateTime Date { get; }

    public Calendar()
    {
        Date = GetDateFromTheInternet("http://www.whatistodaysdate.com");
    }
}

ボブのコードが壊れました!文字列から日付を解析する必要がなくなったため、彼はfileContentにアクセスできなくなりました。

ただし、Johnのコードは機能し続けており、更新する必要はありません。ジョンはカレンダーの予定された公的な契約であるDateを使用しました。 ジョンは、実装の詳細に依存するコードを作成しませんでした(つまり、過去の日付を解析したfileContent)、、したがって彼のコードは実装の変更を難なく処理できます。

これがカプセル化が重要な理由です。中間インターフェース(DateTime Date)を使用することで、コンシューマー(Bob、John)を実装(カレンダーファイル)から切断できます。 中間インターフェースに手を触れない限り、コンシューマに影響を与えずに実装を変更できます.

私の例は少し単純化されています。ここでinterfaceを使用し、同じインターフェースを実装する別のクラスのインターフェースを実装する具象クラスを交換する可能性が高くなります。しかし、私が指摘した問題は同じままです。

51
Flater

不正なアクセスを防ぐために、カプセル化が常にフレームに入れられるのが嫌いです。これがそれを考える最良の方法である場合、不変性は実際にカプセル化の必要性のほとんどを排除します。実際、不変性過剰なカプセル化の多くのケースを排除します。カプセル化の唯一の目的は、不快な呼び出し元を遠ざけることでした。

カプセル化は、優れたカスタマーサービスを提供するものとして考えられます。より使いやすく、より適切な抽象化レベルのインターフェースを追加します。 tilidors のようなもので、ディズニーパークの下のトンネルシステムです。彼らの目的は、ゲストが許可されていない特権スペースを提供することではありません。ゲストの体験と没入感を損なうのを避けるためです。不変性は、「ビジターエクスペリエンス」の種類のカプセル化と同じです。

56
Karl Bielefeldt

カプセル化は、不変データの実際のストレージを隠すことを意味する場合があります。

例えば。:

class Color
{
  private readonly uint argb;
  public byte Blue => (byte)(argb & 0xFF);

  public Color(byte red, byte green, byte blue, byte alpha)
  {
    argb = alpha << 24 | red << 16 | green << 8 | blue;
  }
}

インターフェイス(コンストラクターとバイトブルー)は、実際のブルーデータがuintの最後のバイトに格納されるという事実を隠します。このuintは、後でインターフェースを壊すことなく、rgbaまたはバイトの配列に変更できます。

class Color
{
  private readonly byte[] argb;
  public byte Blue => (byte)argb[3];

  public Color(byte red, byte green, byte blue, byte alpha)
  {
    argb = new []{alpha, red, green, blue};
  }
}

この場合、(不変の)データを適切にカプセル化することで、パブリックインターフェイスを壊すことなく実装を変更できます。

したがって、データが不変である場合でも、カプセル化について考えることは価値があるかもしれません。

32
Erno

不変性を保証できる場合、カプセル化について考える必要がありますか?

おそらく、しかしおそらくほとんどの人が考える方法ではありません。


まず、カプセル化(データの内部構造を非表示にする方法)は目標ではないことを認識してください。これはデカップリングへの手段です(これは抽象化への手段です)。

次に、型でプライベートメンバーを宣言することを含まない方法で分離を実現できます。関数型プログラミングにおけるこの最も一般的な例は、クロージャーです。 C#で次のコードを検討します。

var lowest = int.Parse(someString);
var filtered = collection.Where<Elem>(x => x >= lowest);

関数Whereは、Whereが何も知らない値に依存する述語に基づいて結果をフィルタリングします。クロージャタイプElemのオブジェクトを受け入れ、boolを返すインターフェースを公開する限り、すべてが機能します。

タイプElemのメンバーはすべて完全に公開されていますが、Whereはそのタイプをフィルターの一部として完全に利用でき、完全に分離されたままである可​​能性がありますタイプ。分離は非常に完全であるため、WhereElemの両方は、お互いについて何も知らない完全に別個のアセンブリで定義でき、上記のスニペットは両方を参照する3番目のアセンブリにあり、変更はありません。それが機能するかどうか、またはどのように機能するかについて。

したがって、データの構造を非表示にすることはnot分離を実現するために必要な前提条件です。

簡単に言えば、2つのプロパティは無関係であり、一方を処理しても他方が処理されるとは限りません。カプセル化は記述したコードのプロパティであるため、コード化フェーズで実装されます。不変性は、ランタイムオブジェクトのプロパティです。これは、インスタンス化された後、メモリ内のオブジェクトの状態が変更されないようにすることを意味します。

つまり、要約すると、カプセル化はコードのさまざまな部分を切り離すことを意味します。これにより、開発者が気が変わって一部の変数の名前を変更しても、残りのコードは壊れません。最も明らかなユースケースはAPI開発です。API内で変更されたすべてのものが依存関係を壊してはなりません。

代わりに、不変性は、オブジェクトインスタンスの作成時に実行時に設定されるプロパティの値のみに関するものです。デカップリングはその逆です。インスタンスを作成するライブラリは、そのうちの1つがインスタンスの状態を変更するのではないかと心配することなく、複数のコンシューマに渡すことができるはずです。

3
FluidCode

私は主にカールビーレフェルトの答えに同意しますが、この類似性は少しずれていると思います。

カプセル化はセキュリティではなく、情報の過負荷を回避することを目的としています。リフレクションのあるシステムでは、オートコンプリート/オートサジェスト/インテリセンスのさまざまなフレーバーを検討してください。プライベートメソッドを使用すると、リフレクションが簡単になり、エラーが発生しにくくなります。 IDEでクラスの外にいる人を提案しますか?

一度に心に留めておくことができることには限界があります。昔はグローバル変数があり、数百、場合によっては数千もあり、そのほとんどが比較的少ない場所で使用されていました。それらを使用するときは、どちらが使用されているか、すでに存在しているかどうか、変更されている場所、読み取られている場所、アプリケーションのフローのどこで読み取り/書き込みが行われているかを判断する必要がありました。このようなコードを処理するのは困難でした。間違った変数を使用したり、別の場所で使用されている重複を宣言したりすることも珍しくありません。また、使用ごとに不適切に値を変更することも珍しくありませんでした。何百もの変数を一度に考えることは不可能です。

カプセル化とは、状態を変更する必要がある場所に公開し、アクションを使用できる場所にアクションを公開することを意味します。クラスFooのユーザーとして、メソッドBarで使用されるプライベートメソッドBarXで使用される中間値を計算するプライベートメソッドCalcBarXSetYを持つクラスFooがある場合、CalcBarXSetYが存在するという知識に負担をかけるべきではありません。 、私はそれが何をするのかわからず、確実に使用することができないためです。 FooがCalcBarSetXという名前のメソッドを持っていることを知ることは、クラスのユーザーと同じくらい便利です。クラスの作成に使用される行数、またはBarがiまたはxという名前の変数を使用するかどうか。

2
jmoreno