-
09-10-2019 - |
質問
プロセスを実行する前に、かなりの量の構成データを構築する必要があるプロジェクトがあります。構成段階では、データを可変として持つと非常に便利です。ただし、構成が完了したら、そのデータの不変ビューを機能プロセスに渡したいと思います。そのプロセスは、その計算の多くで構成の不変性に依存するためです (たとえば、事前に計算する機能など)。インターフェイスを使用して読み取り専用ビューを公開するという可能な解決策を思いつきましたが、このタイプのアプローチで問題が発生した人がいるかどうか、またはその方法に関する他の推奨事項があるかどうかを知りたいです。この問題を解決します。
私が現在使用しているパターンの一例:
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>
. 。これにより、多少の複雑さが追加されますが、断熱効果が得られます。
解決
あなたが説明したアプローチは、「クライアント」(インターフェースのコンシューマー)と「サーバー」(クラスのプロバイダー)が次のような相互合意を持っている場合にうまく機能します。
- クライアントは礼儀正しく、サーバーの実装の詳細を利用しようとはしません。
- サーバーは礼儀正しく、クライアントがオブジェクトへの参照を取得した後にオブジェクトを変更しません。
クライアントを作成する人々とサーバーを作成する人々の間で良好な作業関係が構築されていない場合、物事はすぐに梨のような状態になります。もちろん、失礼なクライアントは、パブリック構成タイプにキャストすることで、不変性を「放棄」することができます。失礼なサーバーは不変のビューを渡し、クライアントがまったく予期しないときにオブジェクトを変更する可能性があります。
優れたアプローチは、クライアントが可変型を決して認識しないようにすることです。
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 を作成する唯一の方法は、ビルダーを使用することです。
他のヒント
これは機能しますが、「悪意のある」方法はキャストしようとするかもしれません 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;
}
}
または、可変クラスを不変のクラスに深く塗ることができます。
オブジェクトの別の不変のビューを提供できないのはなぜですか?
public class ImmutableConfiguration {
private Configuration _config;
public ImmutableConfiguration(Configuration config) { _config = config; }
public string Version { get { return _config.Version; } }
}
または、追加のタイピングが気に入らない場合は、セットメンバーをパブリックではなく内部にします - アセンブリ内ではアクセスできますが、クライアントがアクセスできませんか?
私は定期的に、いくつかの状況で非常に同様の変更を処理する大規模なCOMベースのフレームワーク(ESRIのArcGISエンジン)を使用しています。「デフォルト」があります。 IFoo
読み取り専用アクセス用のインターフェイス、および IFooEdit
変更のためのインターフェイス(該当する場合)。
そのフレームワークはかなりよく知られており、その背後にあるこの特定の設計上の決定についての広範な苦情は気づいていません。
最後に、どの「視点」がデフォルトのものになるかを決定する際に、いくつかの追加の考えの価値があると思います:読み取り専用の視点またはフルアクセスの視点。私は個人的に読み取り専用ビューをデフォルトにします。
どうですか:
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; }
}
}
私はそれを読み物と呼びましたが、それが最高の名前であるかどうかはわかりません。