Подход, который вы описали, прекрасно работает, если «клиент» (потребитель интерфейса) и «сервер» (поставщик класса) имеют взаимное соглашение о том, что:
- клиентбудет вежливым и не будет пытаться воспользоваться подробностями реализации сервера
- сервер будет вежливым и не будет мутировать объект после того, как клиент получит на него ссылку.
Если у вас нет хороших рабочих отношений между людьми, пишущими клиент, и людьми, пишущими сервер, то все быстро становится грушевидным.Грубый клиент может, конечно, «отбросить» неизменность, приведя к общедоступному типу конфигурации.Грубый сервер может выдать неизменное представление и затем изменить объект, когда клиент меньше всего этого ожидает.
Хороший подход состоит в том, чтобы клиент никогда не видел изменяемый тип:
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 - через конструктора.