13

構築時にデータベースから情報をロードするクラスがあります。情報はすべて変更可能であり、開発者はその情報に対してSave()を呼び出して、その情報をデータベースに保存し直すことができます。

また、データベースからロードするクラスを作成していますが、データベースの更新は許可していません。(読み取り専用バージョン。)私の質問は、別のクラスを作成して継承するか、コンストラクターで読み取り専用パラメーターを取得するように既存のオブジェクトを更新するか、それとも完全に別のクラスを作成するかです。

既存のクラスは、コード内の多くの場所ですでに使用されています。

ありがとう。

アップデート:

まず、ここにはたくさんの素晴らしい答えがあります。1つだけを受け入れるのは難しいでしょう。みんな、ありがとう。

主な問題は次のとおりです。

  • クラス名と継承構造に基づいて期待に応えます。
  • 不要な重複コードの防止

ReadableとReadOnlyには大きな違いがあるようです。読み取り専用クラスはおそらく継承されるべきではありません。しかし、Readableクラスは、ある時点で書き込み可能性も得られる可能性があることを示唆しています。

よく考えた後、私が考えていることは次のとおりです。

public class PersonTestClass
{
    public static void Test()
    {

        ModifiablePerson mp = new ModifiablePerson();
        mp.SetName("value");
        ReadOnlyPerson rop = new ReadOnlyPerson();
        rop.GetName();
        //ReadOnlyPerson ropFmp = (ReadOnlyPerson)mp;  // not allowed.
        ReadOnlyPerson ropFmp = (ReadOnlyPerson)(ReadablePerson)mp; 
          // above is allowed at compile time (bad), not at runtime (good).
        ReadablePerson rp = mp;
    }
}

public class ReadablePerson
{
    protected string name;
    public string GetName()
    {
        return name;
    }        
}
public sealed class ReadOnlyPerson : ReadablePerson
{
}
public class ModifiablePerson : ReadablePerson
{
    public void SetName(string value)
    {
        name = value;
    }
}

残念ながら、これをプロパティで行う方法はまだわかりません(これについては、プロパティで行われるStriplingWarriorの回答を参照してください)が、保護されたキーワードと非対称プロパティアクセス修飾子が含まれると思います。

また、幸いなことに、データベースからロードされるデータは、参照オブジェクトに変換する必要はなく、単純なタイプです。これは、オブジェクトのメンバーを変更する人を本当に心配する必要がないことを意味しReadOnlyPersonます。

アップデート2:

StriplingWarriorが示唆しているように、ダウンキャストは問題を引き起こす可能性がありますが、サルを犬にキャス​​トし、動物を犬にキャス​​トするのは悪い場合があるため、これは一般的に当てはまります。ただし、キャストはコンパイル時に許可されていても、実際には実行時に許可されていないようです。

ラッパークラスでもうまくいくかもしれませんが、渡されたオブジェクトをディープコピーする必要がない/渡されたオブジェクトを変更できるようにしてラッパークラスを変更する必要がないため、これがより好きです。

4

5 に答える 5

13

リスコフの置換原則では、読み取り専用クラスを読み取り/書き込みクラスから継承しないようにする必要があります。これは、消費クラスは、例外を取得せずにSaveメソッドを呼び出せないことを認識している必要があるためです。

書き込み可能なクラスに読み取り可能なクラスを拡張させることは、そのオブジェクトが永続化されないことを示す読み取り可能なクラスに何もない限り、私にはより意味があります。たとえば、基本クラスをaとは呼びません。引数としてReadOnly[Whatever]aをとるメソッドがある場合ReadOnlyPerson、そのオブジェクトに対して行うことは何も影響を及ぼさないと仮定して、そのメソッドは正当化されるからです。データベース。これは、実際のインスタンスがである場合は必ずしも当てはまりませんWriteablePerson

アップデート

私はもともと、読み取り専用クラスでは、人々がメソッドを呼び出さないようにしたいだけだと思っていましたSave。あなたの質問に対するあなたの回答-回答(ちなみに、実際にはあなたの質問の更新であるはずです)に私が見ているものに基づいて、あなたが従うことを望むかもしれないパターンはここにあります:

public abstract class ReadablePerson
{

    public ReadablePerson(string name)
    {
        Name = name;
    }

    public string Name { get; protected set; }

}

public sealed class ReadOnlyPerson : ReadablePerson
{
    public ReadOnlyPerson(string name) : base(name)
    {
    }
}

public sealed class ModifiablePerson : ReadablePerson
{
    public ModifiablePerson(string name) : base(name)
    {
    }
    public new string Name { 
        get {return base.Name;}
        set {base.Name = value; }
    }
}

これにより、真にReadOnlyPersonModizablePersonとしてキャストして変更することはできません。ただし、開発者がこのように議論を軽視しようとしないことを信頼するのであれば、私はSteveandOlivierの回答のインターフェイスベースのアプローチを好みます。

もう1つのオプションは、オブジェクトReadOnlyPersonのラッパークラスにするPersonことです。これにはより多くの定型コードが必要になりますが、基本クラスを変更できない場合に便利です。

最後にもう1つ、リスコフの置換原則について学ぶことを楽しんだので、Personクラスにデータベースからの読み込みを担当させることで、単一責任原則を破ることになります。理想的には、Personクラスには、「Person」を構成するデータを表すプロパティがありPersonRepository、データベースからPersonを生成したり、Personをデータベースに保存したりする別のクラス(おそらく)があります。

アップデート2

コメントへの返信:

  • あなたは技術的にあなた自身の質問に答えることができますが、StackOverflowは主に他の人から答えを得ることについてです。そのため、一定の猶予期間が経過するまで、自分の回答を受け入れることはできません。誰かがあなたの最初の質問に対する適切な解決策を思い付くまで、あなたはあなたの質問を洗練し、コメントと回答に答えることをお勧めします。
  • 読み取り専用または書き込み可能な人だけを作りたいと思ったので、ReadablePersonクラスを作りました。abstract両方の子クラスをReadablePersonと見なすことがnew ReadablePerson()できますが、同じように簡単に作成できる場合、を作成するポイントは何でしょうnew ReadOnlyPerson()か。クラスを抽象化するには、ユーザーがインスタンス化するときに2つの子クラスのいずれかを選択する必要があります。
  • PersonRepositoryは工場のようなものですが、「リポジトリ」という言葉は、人を空から作成するのではなく、実際にデータソースから人の情報を取得していることを示します。
  • 私の考えでは、Personクラスは単なるPOCOであり、ロジックは含まれていません。プロパティのみです。リポジトリは、Personオブジェクトの構築を担当します。言うのではなく:

    // This is what I think you had in mind originally
    var p = new Person(personId);
    

    ...そしてPersonオブジェクトがデータベースに移動して、さまざまなプロパティを設定できるようにすると、次のようになります。

    // This is a better separation of concerns
    var p = _personRepository.GetById(personId);
    

    次に、PersonRepositoryはデータベースから適切な情報を取得し、そのデータを使用してPersonを構築します。

    個人を変更する理由のないメソッドを呼び出したい場合は、読み取り専用ラッパーに変換することで、その個人を変更から保護できます(.NETライブラリがReadonlyCollection<T>クラスで従うパターンに従います)。一方、書き込み可能なオブジェクトを必要とするメソッドは、Person直接次のように指定できます。

    var person = _personRepository.GetById(personId);
    // Prevent GetVoteCount from changing any of the person's information
    int currentVoteCount = GetVoteCount(person.AsReadOnly()); 
    // This is allowed to modify the person. If it does, save the changes.
    if(UpdatePersonDataFromLdap(person))
    {
         _personRepository.Save(person);
    }
    
  • インターフェイスを使用する利点は、特定のクラス階層を強制しないことです。これにより、将来的に柔軟性が向上します。たとえば、今のところ、次のようにメソッドを記述しているとしましょう。

    GetVoteCount(ReadablePerson p);
    UpdatePersonDataFromLdap(ReadWritePerson p);
    

    ...しかし、2年後には、ラッパーの実装に変更することにしました。基本クラスの拡張ではなくラッパークラスであるため、突然ReadOnlyPersonではなくなりました。すべてのメソッドシグネチャをにReadablePerson変更ReadablePersonしますか?ReadOnlyPerson

    または、物事を単純化して、すべてのクラスを1つのPersonクラスに統合することにしたとします。次に、すべてのメソッドを変更して、Personオブジェクトのみを取得する必要があります。一方、インターフェースにプログラムした場合:

    GetVoteCount(IReadablePerson p);
    UpdatePersonDataFromLdap(IReadWritePerson p);
    

    ...次に、これらのメソッドは、指定したオブジェクトが要求するインターフェイスを実装している限り、オブジェクト階層がどのように見えるかを気にしません。これらのメソッドをまったく変更せずに、いつでも実装階層を変更できます。

于 2012-04-04T16:08:32.407 に答える
6

読み取り専用クラスに書き込み可能クラスを継承させないでください。派生クラスは、基本クラスの機能を拡張および変更する必要があります。決して機能を奪ってはなりません。

書き込み可能なクラスに読み取り専用クラスを継承させることもできますが、慎重に行う必要があります。重要な質問は、読み取り専用クラスの消費者は、それが読み取り専用であるという事実に依存するかということです。コンシューマーが値が変更されないことを期待しているが、書き込み可能な派生型が渡され、その後値が変更された場合、そのコンシューマーが壊れる可能性があります。

2 つの型の構造 (つまり、それらに含まれるデータ) が類似または同一であるため、一方を他方から継承する必要があると考えたくなるのは承知しています。しかし、多くの場合、そうではありません。大幅に異なるユースケース向けに設計されている場合は、おそらく別のクラスにする必要があります。

于 2012-04-04T16:15:53.463 に答える
5

簡単なオプションはIReadablePerson、get プロパティのみを含み、Save() を含まない (etc) インターフェイスを作成することです。次に、既存のクラスにそのインターフェイスを実装させ、読み取り専用アクセスが必要な場合は、消費するコードがそのインターフェイスを介してクラスを参照するようにします。

このパターンに合わせてIReadWritePerson、setter と を含むインターフェイスも必要になるでしょうSave()

編集さらに考えてみると、書き込み専用クラスを持つことはあまり意味がないため、IWriteablePersonおそらく である必要があります。IReadWritePerson

例:

public interface IReadablePerson
{
    string Name { get; }
}

public interface IReadWritePerson : IReadablePerson
{
    new string Name { get; set; }
    void Save();
}

public class Person : IReadWritePerson
{
    public string Name { get; set; }
    public void Save() {}
}
于 2012-04-04T16:17:34.040 に答える
4

問題は、「変更可能なクラスを継承して読み取り専用クラスにするにはどうすればよいか」ということです。継承を使用すると、クラスを拡張できますが、制限することはできません。例外をスローしてこれを行うと、 Liskov Substitution Principle (LSP)に違反します。

逆に、つまり、読み取り専用クラスから変更可能なクラスを派生させることは、この観点からは問題ありません。ただし、読み取り専用プロパティを読み書き可能プロパティに変更するにはどうすればよいでしょうか。さらに、読み取り専用オブジェクトが期待される場所で、変更可能なオブジェクトを代用できることが望ましいでしょうか?

ただし、インターフェイスを使用してこれを行うことができます

interface IReadOnly
{
    int MyProperty { get; }
}

interface IModifiable : IReadOnly
{
    new int MyProperty { set; }
    void Save();
}

IReadOnlyこのクラスは、インターフェイスにも代入互換性があります。読み取り専用のコンテキストでは、IReadOnlyインターフェイスを介してアクセスできます。

class ModifiableClass : IModifiable
{
    public int MyProperty { get; set; }
    public void Save()
    {
        ...
    }
}

アップデート

この件についてさらに調査を行いました。

ただし、これには注意点があります。newキーワードを追加するIModifiable必要がありました。ゲッターには、直接ModifiableClassまたはインターフェイスを介してのみアクセスできますが、IReadOnlyインターフェイスを介してはアクセスできませんIModifiable

また、2 つのインターフェイスIReadOnlyを使用しIWriteOnlyて、それぞれゲッターまたはセッターのみを使用しようとしました。次に、両方から継承するインターフェイスを宣言できます。newプロパティの前にキーワードは必要ありません ( のようにIModifiable)。ただし、そのようなオブジェクトのプロパティにアクセスしようとすると、コンパイラ エラーが発生しますAmbiguity between 'IReadOnly.MyProperty' and 'IWriteOnly.MyProperty'

明らかに、私が予想したように、個別のゲッターとセッターからプロパティを合成することはできません。

于 2012-04-04T16:38:22.033 に答える
0

ユーザーのセキュリティ権限のオブジェクトを作成するときに解決する同じ問題がありました。特定の場合、高レベルのユーザーがセキュリティ設定を変更できるように変更可能である必要がありますが、通常は読み取り専用で、現在ログインしているユーザーの権限情報を保存します。コードがそれらの権限をその場で変更することを許可せずに。

私が思いついたパターンは、読み取り専用のプロパティゲッターを持つ、可変オブジェクトが実装するインターフェイスを定義することでした。そのインターフェースの可変実装はプライベートにすることができ、オブジェクトのインスタンス化とハイドレイティングを直接処理するコードがそうすることを可能にしますが、オブジェクトがそのコードから(インターフェースのインスタンスとして)返されると、セッターはアクセスできなくなります。

例:

//this is what "ordinary" code uses for read-only access to user info.
public interface IUser
{
   string UserName {get;}
   IEnumerable<string> PermissionStrongNames {get;}

   ...
}

//This class is used for editing user information.
//It does not implement the interface, and so while editable it cannot be 
//easily used to "fake" an IUser for authorization
public sealed class EditableUser 
{
   public string UserName{get;set;}
   List<SecurityGroup> Groups {get;set;}

   ...
}

...

//this class is nested within the class responsible for login authentication,
//which returns instances as IUsers once successfully authenticated
private sealed class AuthUser:IUser
{
   private readonly EditableUser user;

   public AuthUser(EditableUser mutableUser) { user = mutableUser; }

   public string UserName {get{return user.UserName;}}

   public IEnumerable<string> PermissionNames 
   {
       //GetPermissions is an extension method that traverses the list of nestable Groups.
       get {return user.Groups.GetPermissions().Select(p=>p.StrongName);
   }

   ...
}

このようなパターンを使用すると、既に作成したコードを読み取り/書き込み方式で使用できますが、JoeProgrammerが読み取り専用インスタンスを可変インスタンスに変換することはできません。私の実際の実装には、主に編集可能なオブジェクトの永続性を扱ういくつかのトリックがあります(ユーザーレコードの編集はセキュリティで保護されたアクションであるため、EditableUserはリポジトリの「通常の」永続化メソッドで保存できません。代わりにオーバーロードを呼び出す必要があります。また、十分な権限を持っている必要があるIUserを取ります)。

単に理解しなければならないことが1つあります。プログラムが任意のスコープのレコードを編集できる場合は、意図的かどうかにかかわらず、その機能が悪用される可能性があります。他のコーダーが「賢い」ことをしていないことを確認するために、オブジェクトの可変形式または不変形式の使用法を定期的にコードレビューする必要があります。このパターンは、一般の人々が使用するアプリケーションの安全性を確保するのにも十分ではありません。IUser実装を記述できる場合、攻撃者も同様であるため、攻撃者ではなくコードが特定のIUserインスタンスを生成し、その間にインスタンスが改ざんされていないことを確認するための追加の方法が必要になります。

于 2012-04-04T17:23:30.373 に答える