7

プロセスを実行する前に、かなりの量の構成データを構築する必要があるプロジェクトがあります。構成段階では、データを変更可能にすると非常に便利です。ただし、構成が完了したら、そのデータの不変ビューを機能プロセスに渡したいと思います。そのプロセスは、その計算の多くで構成の不変性に依存するためです (たとえば、インターフェイスを使用して読み取り専用ビューを公開する可能な解決策を思いつきましたが、このタイプのアプローチで問題が発生したことがあるかどうか、または方法に関する他の推奨事項があるかどうかを知りたいです。この問題を解決します。

私が現在使用しているパターンの一例:

public interface IConfiguration
{
    string Version { get; }

    string VersionTag { get; }

    IEnumerable<IDeviceDescriptor> Devices { get; }

    IEnumerable<ICommandDescriptor> Commands { get; }
}

[DataContract]
public sealed class Configuration : IConfiguration
{
    [DataMember]
    public string Version { get; set; }

    [DataMember]
    public string VersionTag { get; set; }

    [DataMember]
    public List<DeviceDescriptor> Devices { get; private set; }

    [DataMember]
    public List<CommandDescriptor> Commands { get; private set; }

    IEnumerable<IDeviceDescriptor> IConfiguration.Devices
    {
        get { return Devices.Cast<IDeviceDescriptor>(); }
    }

    IEnumerable<ICommandDescriptor> IConfiguration.Commands
    {
        get { return Commands.Cast<ICommandDescriptor>(); }
    }

    public Configuration()
    {
        Devices = new List<DeviceDescriptor>();
        Commands = new List<CommandDescriptor>();
    }
}

編集

Lippert 氏と cdhowie からの入力に基づいて、次のようにまとめました (簡略化するためにいくつかのプロパティを削除しました)。

[DataContract]
public sealed class Configuration
{
    private const string InstanceFrozen = "Instance is frozen";

    private Data _data = new Data();
    private bool _frozen;

    [DataMember]
    public string Version
    {
        get { return _data.Version; }
        set
        {
            if (_frozen) throw new InvalidOperationException(InstanceFrozen);
            _data.Version = value;
        }
    }

    [DataMember]
    public IList<DeviceDescriptor> Devices
    {
        get { return _data.Devices; }
        private set { _data.Devices.AddRange(value); }
    }

    public IConfiguration Freeze()
    {
        if (!_frozen)
        {
            _frozen = true;
            _data.Devices.Freeze();
            foreach (var device in _data.Devices)
                device.Freeze();
        }
        return _data;
    }

    [OnDeserializing]
    private void OnDeserializing(StreamingContext context)
    {
        _data = new Data();
    }

    private sealed class Data : IConfiguration
    {
        private readonly FreezableList<DeviceDescriptor> _devices = new FreezableList<DeviceDescriptor>();

        public string Version { get; set; }

        public FreezableList<DeviceDescriptor> Devices
        {
            get { return _devices; }
        }

        IEnumerable<IDeviceDescriptor> IConfiguration.Devices
        {
            get { return _devices.Select(d => d.Freeze()); }
        }
    }
}

FreezableList<T>ご想像のとおり、 は のフリーズ可能な実装ですIList<T>。これにより、複雑さが増しますが、断熱効果が得られます。

4

5 に答える 5

13

「クライアント」(インターフェースの消費者)と「サーバー」(クラスのプロバイダー)が次の相互合意を持っている場合、あなたが説明するアプローチはうまく機能します。

  • クライアントは礼儀正しく、サーバーの実装の詳細を利用しようとしません。
  • クライアントがオブジェクトへの参照を取得した後、サーバーは礼儀正しく、オブジェクトを変更しません。

クライアントを書いている人々とサーバーを書いている人々の間に良好な協力関係がなければ、物事はすぐにナシの形になります。もちろん、失礼なクライアントは、パブリック構成タ​​イプにキャストすることにより、不変性を「キャストアウェイ」できます。失礼なサーバーは、不変のビューを渡して、クライアントが最も期待していないときにオブジェクトを変更できます。

良いアプローチは、クライアントが変更可能な型を決して見ないようにすることです:

public interface IReadOnly { ... }
public abstract class Frobber : IReadOnly
{
    private Frobber() {}
    public class sealed FrobBuilder
    {
        private bool valid = true;
        private RealFrobber real = new RealFrobber();
        public void Mutate(...) { if (!valid) throw ... }
        public IReadOnly Complete { valid = false; return real; }
    }
    private sealed class RealFrobber : Frobber { ... }
}

Frobber を作成して変異させたい場合は、Frobber.FrobBuilder を作成できます。ミューテーションが完了したら、Complete を呼び出して、読み取り専用インターフェイスを取得します。(そして、ビルダーは無効になります。)可変性の実装の詳細はすべて、ネストされたプライベート クラスに隠されているため、IReadOnly インターフェイスを RealFrobber に「キャスト アウェイ」することはできません。パブリック メソッドを持たない Frobber にのみキャストする必要があります。

Frobber は抽象的でプライベート コンストラクターを持っているため、敵対的なクライアントは独自の Frobber を作成することもできません。Frobber を作成する唯一の方法は、ビルダーを使用することです。

于 2010-11-12T19:43:45.370 に答える
3

これは機能しますが、「悪意のある」メソッドが を にキャストしようとする可能性がIConfigurationありConfiguration、それによってインターフェイスに課せられた制限を回避する可能性があります。それについて心配していなければ、あなたのアプローチはうまくいくでしょう。

私は通常、次のようなことをします:

public class Foo {
    private bool frozen = false;

    private string something;

    public string Something {
        get { return something; }
        set {
            if (frozen)
                throw new InvalidOperationException("Object is frozen.");

            // validate value

            something = value;
        }
    }

    public void Freeze() {
        frozen = true;
    }
}

または、可変クラスを不変クラスにディープ クローンすることもできます。

于 2010-11-12T19:40:51.630 に答える
2

オブジェクトの個別の不変ビューを提供できないのはなぜですか?

public class ImmutableConfiguration {
    private Configuration _config;
    public ImmutableConfiguration(Configuration config) { _config = config; }
    public string Version { get { return _config.Version; } }
}

または、余分な入力が気に入らない場合は、セットメンバーをパブリックではなく内部にします-アセンブリ内でアクセスできますが、アセンブリのクライアントはアクセスできませんか?

于 2010-11-12T19:56:39.770 に答える
1

状況によっては、変更を非常によく似た方法で処理する大規模なCOMベースのフレームワーク(ESRIのArcGIS Engine)を定期的に使用しています。IFoo読み取り専用アクセス用の「デフォルト」インターフェイスとIFooEdit、変更用のインターフェイス(該当する場合)があります。

そのフレームワークはかなりよく知られており、その背後にあるこの特定の設計上の決定についての広範な苦情を私は知りません。

最後に、読み取り専用のパースペクティブとフルアクセスのパースペクティブのどちらの「パースペクティブ」をデフォルトにするかを決定する際には、さらに検討する価値があると思います。私は個人的に読み取り専用ビューをデフォルトにします。

于 2010-11-12T19:54:15.157 に答える
0

どうですか:

struct Readonly<T>
{
    private T _value;
    private bool _hasValue;

    public T Value
    {
        get
        {
            if (!_hasValue)
                throw new InvalidOperationException();
            return _value;
        }
        set
        {
            if (_hasValue)
                throw new InvalidOperationException();
            _value = value;
        }
    }
}


[DataContract]
public sealed class Configuration
{
    private Readonly<string> _version;

    [DataMember]
    public string Version
    {
        get { return _version.Value; }
        set { _version.Value = value; }
    }
}

私はそれを Readonly と呼びましたが、それが最適な名前かどうかはわかりません。

于 2010-11-12T19:50:51.993 に答える