Pergunta

Eu tenho ao longo de alguns projectos desenvolveu um padrão para a criação imutável (somente leitura) objetos e gráficos de objetos imutáveis. objetos imutáveis ??levar a vantagem de ser 100% seguro para threads e pode, portanto, ser reutilizado em threads. No meu trabalho eu muito frequentemente usam este padrão em aplicativos da Web para as definições de configuração e outros objetos que carregar e cache na memória. objetos em cache deve ser sempre imutável como você quiser garantir que eles não são alterados inesperadamente.

Agora, você pode, naturalmente, facilmente criar objetos imutáveis ??como no exemplo a seguir:

public class SampleElement
{
  private Guid id;
  private string name;

  public SampleElement(Guid id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public Guid Id
  {
    get { return id; }
  }

  public string Name
  {
    get { return name; }
  }
}

Isso é bom para classes simples - mas para as classes mais complexas que não gosta do conceito de passar todos os valores através de um construtor. Tendo setters das propriedades é mais desejável e seu código de construção de um novo objeto fica mais fácil de ler.

Assim como você criar objetos imutáveis ??com setters?

Bem, em meus objetos padrão começam como sendo totalmente mutável até que você congelá-los com uma única chamada de método. Depois que um objeto é congelado permanecerá imutável para sempre - não pode ser transformado em um objeto mutável novamente. Se você precisar de uma versão mutável do objeto, você simplesmente cloná-lo.

Ok, agora para alguns códigos. Eu tenho nos seguintes trechos de código tentaram ferver o baixo padrão a sua forma mais simples. O IElement é a interface base que todos os objetos imutáveis ??deve finalmente implementar.

public interface IElement : ICloneable
{
  bool IsReadOnly { get; }
  void MakeReadOnly();
}

O elemento de classe é a implementação padrão da interface IElement:

public abstract class Element : IElement
{
  private bool immutable;

  public bool IsReadOnly
  {
    get { return immutable; }
  }

  public virtual void MakeReadOnly()
  {
    immutable = true;
  }

  protected virtual void FailIfImmutable()
  {
    if (immutable) throw new ImmutableElementException(this);
  }

  ...
}

Vamos refactor a classe SampleElement acima para implementar o padrão de objeto imutável:

public class SampleElement : Element
{
  private Guid id;
  private string name;

  public SampleElement() {}

  public Guid Id
  {
    get 
    { 
      return id; 
    }
    set
    {
      FailIfImmutable();
      id = value;
    }
  }

  public string Name
  {
    get 
    { 
      return name; 
    }
    set
    {
      FailIfImmutable();
      name = value;
    }
  }
}

Você agora pode alterar a propriedade ID ea propriedade Nome, desde que o objeto não foi marcado como imutável, chamando o método MakeReadOnly (). Uma vez que é imutável, chamando um setter irá produzir um ImmutableElementException.

Nota final: O padrão completo é mais complexo do que os trechos de código mostradas aqui. Ele também contém suporte para coleções de objetos imutáveis ??e gráficos de objetos completos de gráficos de objetos imutáveis. O padrão completo permite-lhe transformar toda uma imutável gráfico do objeto chamando o método MakeReadOnly () no objeto mais externo. Uma vez que você começar a criar modelos de objetos maiores que utilizam este padrão o risco de vazamento objetos aumenta. Um objecto gotejante é um objecto que não se apresente o método FailIfImmutable () antes de fazer uma mudança para o objecto. Para testar se há vazamentos I também têm desenvolvido uma classe detector de vazamento genérico para uso em testes de unidade. Ele usa reflexão para testar se todas as propriedades e métodos de jogar a ImmutableElementException no estado imutável. Em outras palavras TDD é usado aqui.

Eu cresci a gostar desse padrão muito e encontrar grandes benefícios na mesma. Então o que eu gostaria de saber é se algum de vocês estiver usando padrões semelhantes? Se sim, você sabe de qualquer bons recursos que documentá-lo? Eu sou essencialmente à procura de potenciais melhorias e para quaisquer padrões que já possam existir sobre este tema.

Foi útil?

Solução

Para informações, a segunda abordagem é chamada de "imutabilidade picolé".

Eric Lippert tem uma série de entradas de blog sobre a imutabilidade começando aqui . Eu ainda estou chegar ao confronto com o CTP (C # 4.0), mas parece interessante o opcional / chamados parâmetros (para o .ctor) pode fazer aqui (quando mapeados para campos somente leitura) ... [Update: Eu tenho um blog sobre este aqui ]

Para sua informação, eu provavelmente não fazer aqueles métodos virtual - nós provavelmente não quer subclasses ser capaz de torná-lo não freezable. Se você quer que eles sejam capazes de adicionar código extra, eu sugiro algo como:

[public|protected] void Freeze()
{
    if(!frozen)
    {
        frozen = true;
        OnFrozen();
    }
}
protected virtual void OnFrozen() {} // subclass can add code here.

Além disso -. AOP (como PostSharp) pode ser uma opção viável para a adição de todos esses cheques ThrowIfFrozen ()

(desculpas se eu mudei de terminologia / método nomes - SO não manter o post original visível ao compor respostas)

Outras dicas

Outra opção seria a criação de algum tipo de classe Builder.

Por exemplo, em Java (e C # e muitas outras línguas) String é imutável. Se você quer fazer várias operações para criar uma String você usar um StringBuilder. Esta é mutável, e, em seguida, uma vez que você está feito, você tem que voltar para você o objeto String final. A partir de então, por si imutável.

Você poderia fazer algo semelhante para suas outras classes. Você tem o seu imutável Element, e em seguida, um ElementBuilder. Tudo o construtor gostaria de fazer é armazenar as opções definidas, então quando você finalizá-lo constrói e retorna o imutável elemento.

É um pouco mais de código, mas eu acho que é mais limpo do que ter setters em uma classe que é suposto ser imutável.

Depois do meu desconforto inicial sobre o fato de que eu tinha que criar uma nova System.Drawing.Point em cada modificação, eu totalmente abraçaram o conceito há alguns anos. Na verdade, eu agora criar todos os campos como readonly por padrão e apenas alterá-lo para ser mutável, se há uma razão convincente -. Que não é surpreendentemente raramente

Eu não me importo muito sobre questões inter-threading, embora (eu raramente usar o código se for pertinente). Acabei de encontrá-lo muito, muito melhor por causa da expressividade semântica. Imutabilidade é o epítome de uma interface que é difícil de usar incorretamente.

Você ainda está lidando com o estado e, portanto, ainda pode ser mordido se os objetos são parallelized antes de ser feitas imutável.

A maneira mais funcional poderia ser a de retornar uma nova instância do objeto com cada setter. Ou criar um objeto mutável e passar que para o construtor.

A (relativamente) novo Software Design paradigma chamado Domain Driven Design, faz a distinção entre objetos de entidade e objetos de valor.

objetos de entidade são definidos como qualquer coisa que tem para mapear para um objeto orientado a chave em um armazenamento de dados persistente, como um empregado, ou um cliente, ou uma factura, etc ... onde alterando as propriedades do objeto implica que você precisa para salvar a mudança para um lugar de armazenamento de dados, ea existência de várias instâncias de uma classe com o mesmo imnplies "chave" a necessidade de sincronizar-los, ou coordenar a sua persistência para o armazenamento de dados para que as alterações uma instância' não substituir os outros. Alterando as propriedades de um objeto de entidade implica que você está mudando algo sobre o objeto - não mudar qual objeto você está fazendo referência ...

Valor objetos otoh, são objetos que podem ser consideradas imutáveis, cuja utilidade é definido estritamente por seus valores de propriedade, e para o qual várias instâncias, não precisam ser coordenadas de qualquer maneira ... como endereços ou números de telefone, ou as rodas de um carro, ou as letras em um documento ... estas coisas são totalmente definidos por suas propriedades ... uma letra maiúscula objeto 'a' em um editor de texto podem ser trocados de forma transparente com qualquer outro maiúscula 'a' objeto ao longo o documento, você não precisa de uma chave para distingui-lo de todos os outros 'de um neste sentido, é imutável, porque se você alterá-lo para um 'B'(exatamente como mudar a seqüência de número de telefone em um objeto de número de telefone, você não está mudando os dados associados com alguma entidade mutável, estiver a mudar de um valor para outro ... assim como quando você alterar o valor de uma string ...

System.String é um bom exemplo de uma classe imutável com setters e métodos de mutação, apenas que cada método de mutação retorna uma nova instância.

Expandindo o ponto por @Cory Foy e @Charles Bretana onde há uma diferença entre entidades e valores. Considerando o valor-objetos deve ser sempre imutável, eu realmente não acho que um objeto deve ser capaz de congelar-se, ou se permitem ser congeladas arbitrariamente na base de código. Ele tem um mau cheiro para ele, e eu me preocupo que ele poderia ficar difícil de rastrear exatamente onde um objeto foi congelado, e por que ele foi congelado, e o fato de que entre chamadas para um objeto que poderia alterar o estado de descongelado para congelado .

Isso não quer dizer que às vezes você quer dar uma entidade (mutável) para algo e garantir que ele não vai ser alterado.

Assim, em vez de congelar o objeto em si, outra possibilidade é copiar a semântica de ReadOnlyCollection

List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();

O objeto pode ter uma parte tão mutável, quando ele precisa, em seguida, ser imutável quando você deseja que ele seja.

Note que ReadOnlyCollection também implementa ICollection que tem um método Add( T item) na interface. No entanto, também é bool IsReadOnly { get; } definido na interface para que os consumidores podem verificar antes de chamar um método que irá lançar uma exceção.

A diferença é que você não pode apenas definir IsReadOnly para falso. Uma coleção é ou não é somente leitura, e que nunca muda para o tempo de vida da coleção.

Seria bom na hora de ter o const-correção que C ++ lhe dá em tempo de compilação, mas que começa a ter seu próprio conjunto de problemas e estou feliz C # não ir lá.


ICloneable - Eu pensei que eu tinha acabado de remeter para o seguinte:

Não implementar ICloneable

Não use ICloneable em APIs públicas

Brad Abrams - Guia de Design, código gerenciado e o. NET Framework

Este é um problema importante, e eu tenho amor para ver o apoio framework / linguagem mais direta para resolvê-lo. A solução que você tem requer muito clichê. Pode ser simples para automatizar algumas das clichê usando a geração de código.

Você iria gerar uma classe parcial que contém todas as propriedades freezable. Seria bastante simples de fazer um modelo T4 reutilizável para isso.

O modelo levaria isso para entrada:

  • namespace
  • nome da classe
  • lista de nome de propriedade / Tipo de tuplas

E saída seria um arquivo C #, contendo:

  • declaração de namespace
  • classe parcial
  • cada uma das propriedades, com os correspondentes tipos, um campo de suporte, um getter, e de fixar o qual invoca o método FailIfFrozen

tag AOP em propriedades freezable poderiam também trabalhar, mas isso exigiria mais dependências, enquanto que T4 é construído em versões mais recentes do Visual Studio.

Outro cenário que é muito parecido com isso é a interface INotifyPropertyChanged. Soluções para esse problema são susceptíveis de ser aplicável a este problema.

O meu problema com este padrão é que você não está impor quaisquer restrições de tempo de compilação sobre a imutabilidade. O codificador é responsável por certificar-se um objeto é definido como imutável antes, por exemplo, adicionando-o a um cache ou outra estrutura não-thread-safe.

É por isso que gostaria de estender esse padrão de codificação com um apoio em tempo de compilação na forma de uma classe genérica, como este:

public class Immutable<T> where T : IElement
{
    private T value;

    public Immutable(T mutable) 
    {
        this.value = (T) mutable.Clone();
        this.value.MakeReadOnly();
    }

    public T Value 
    {
        get 
        {
            return this.value;
        }
    }

    public static implicit operator Immutable<T>(T mutable) 
    {
        return new Immutable<T>(mutable);
    }

    public static implicit operator T(Immutable<T> immutable)
    {
        return immutable.value;
    }
}

Aqui está um exemplo como você usaria o seguinte:

// All elements of this list are guaranteed to be immutable
List<Immutable<SampleElement>> elements = 
    new List<Immutable<SampleElement>>();

for (int i = 1; i < 10; i++) 
{
    SampleElement newElement = new SampleElement();
    newElement.Id = Guid.NewGuid();
    newElement.Name = "Sample" + i.ToString();

    // The compiler will automatically convert to Immutable<SampleElement> for you
    // because of the implicit conversion operator
    elements.Add(newElement);
}

foreach (SampleElement element in elements)
    Console.Out.WriteLine(element.Name);

elements[3].Value.Id = Guid.NewGuid();      // This will throw an ImmutableElementException

Apenas uma dica para simplificar as propriedades do elemento: Use propriedades automáticas com private set e evitar declarar explicitamente o campo de dados. por exemplo.

public class SampleElement {
  public SampleElement(Guid id, string name) {
    Id = id;
    Name = name;
  }

  public Guid Id {
    get; private set;
  }

  public string Name {
    get; private set;
  }
}

Aqui está um novo vídeo no Channel 9, onde Anders Hejlsberg de 36:30 na entrevista começa a falar sobre a imutabilidade em C #. Ele dá um bom caso de uso para imutabilidade picolé e explica como isso é algo que você está obrigado a implementar-se. Foi música para os meus ouvidos ouvi-lo dizer que vale a pena pensar sobre o melhor suporte para a criação de gráficos de objetos imutáveis ??em futuras versões do C #

especialista para especialista: Anders Hejlsberg - The Future of C #

Duas outras opções para o seu problema particular que não foram discutidos:

  1. Construa seu próprio desserializador, que pode chamar um setter propriedade privada. Embora o esforço na construção da desserializador no início vai ser muito mais, isso torna as coisas mais limpo. O compilador irá mantê-lo de mesmo tentando chamar os setters e o código em suas classes será mais fácil de ler.

  2. Coloque um construtor em cada classe que leva um XElement (ou algum outro sabor do modelo de objeto XML) e preenche-se a partir dele. Obviamente, como o número de classes aumenta, este rapidamente se torna menos desejável como uma solução.

Que tal ter uma classe abstrata ThingBase, com subclasses MutableThing e ImmutableThing? ThingBase iria conter todos os dados em uma estrutura protegida, proporcionando propriedades públicas somente leitura para os campos e protegidos somente leitura propriedade para sua estrutura. Também forneceria um método AsImmutable substituível que retornaria um ImmutableThing.

MutableThing iria sombra das propriedades com propriedades de leitura / gravação, e fornecer um construtor padrão e um construtor que aceita um ThingBase.

coisa imutável seria uma classe selada que substitui AsImmutable simplesmente em si retornar. Seria também fornecer um construtor que aceita um ThingBase.

Eu não gosto da idéia de ser capaz de mudar um objeto de um mutável para um estado imutável, esse tipo de parece derrotar o ponto de projeto para mim. Quando você precisar fazer isso? Apenas os objetos que representam os valores devem ser imutável

Você pode usar opcionais argumentos nomeados juntamente com nullables para fazer um setter imutável com muito pouco clichê. Se você realmente quer para definir uma propriedade como nulo, então você pode ter mais alguns problemas.

class Foo{ 
    ...
    public Foo 
        Set
        ( double? majorBar=null
        , double? minorBar=null
        , int?        cats=null
        , double?     dogs=null)
    {
        return new Foo
            ( majorBar ?? MajorBar
            , minorBar ?? MinorBar
            , cats     ?? Cats
            , dogs     ?? Dogs);
    }

    public Foo
        ( double R
        , double r
        , int l
        , double e
        ) 
    {
        ....
    }
}

Você poderia usá-lo como assim

var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top