Неизменные виды изменяемых типов - PullRequest
7 голосов
/ 12 ноября 2010

У меня есть проект, в котором мне нужно собрать достаточное количество данных конфигурации, прежде чем я смогу выполнить процесс. На этапе настройки очень удобно иметь изменяемые данные. Однако, как только конфигурация будет завершена, я хотел бы передать неизменное представление этих данных функциональному процессу, поскольку этот процесс будет полагаться на неизменность конфигурации для многих своих вычислений (например, возможность предварительного вычисления на основе при первоначальной настройке.) Я предложил возможное решение с использованием интерфейсов для представления представления только для чтения, но я хотел бы знать, сталкивался ли кто-нибудь с проблемами с этим типом подхода или есть ли другие рекомендации о том, как решить эту проблему.

Один пример шаблона, который я сейчас использую:

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>();
    }
}

EDIT

Основываясь на мнениях г-на Липперта и 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>. Это дает преимущества изоляции, за счет некоторой дополнительной сложности.

Ответы [ 5 ]

13 голосов
/ 12 ноября 2010

Подход, который вы описали, прекрасно работает, если «клиент» (потребитель интерфейса) и «сервер» (поставщик класса) имеют взаимное соглашение о том, что:

  • клиентбудет вежливым и не будет пытаться воспользоваться подробностями реализации сервера
  • сервер будет вежливым и не будет мутировать объект после того, как клиент получит на него ссылку.

Если у вас нет хороших рабочих отношений между людьми, пишущими клиент, и людьми, пишущими сервер, то все быстро становится грушевидным.Грубый клиент может, конечно, «отбросить» неизменность, приведя к общедоступному типу конфигурации.Грубый сервер может выдать неизменное представление и затем изменить объект, когда клиент меньше всего этого ожидает.

Хороший подход состоит в том, чтобы клиент никогда не видел изменяемый тип:

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 - через конструктора.

3 голосов
/ 12 ноября 2010

Это будет работать, но «злонамеренные» методы могут попытаться привести 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;
    }
}

В качестве альтернативы, вы можете глубоко клонировать свои изменяемые классы в неизменяемые классы..

2 голосов
/ 12 ноября 2010

Почему вы не можете предоставить отдельный неизменяемый вид объекта?

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

или, если вам не нравится дополнительная типизация, сделайте элементы набора внутренними, а не открытыми - доступными в сборкено не клиентами этого?

1 голос
/ 12 ноября 2010

Я регулярно работаю с большой платформой на основе COM (ESRI ArcGIS Engine), которая в некоторых ситуациях обрабатывает изменения очень схожим образом: существуют интерфейсы IFoo по умолчанию для доступа только для чтения и IFooEditинтерфейсы (где это применимо) для модификаций.

Эта структура довольно известна, и я не знаю о каких-либо распространенных жалобах на это конкретное проектное решение, лежащее в ее основе.определенно стоит подумать над тем, чтобы решить, какая «перспектива» станет стандартной: перспектива только для чтения или перспектива с полным доступом.Я бы лично сделал по умолчанию доступ только для чтения.

0 голосов
/ 12 ноября 2010

Как насчет:

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; }
    }
}

Я назвал это только для чтения, но я не уверен, что это лучшее название для него.

...