Immutable object pattern w C# - co o tym sądzisz? [zamknięte]

W trakcie kilku projektów opracowałem wzorzec do tworzenia obiektów niezmiennych (readonly) i wykresów obiektów niezmiennych. Niezmienne obiekty mają tę zaletę, że są w 100% bezpieczne dla wątku i dlatego mogą być ponownie użyte w różnych wątkach. W swojej pracy Bardzo często używam tego wzorca w aplikacjach webowych do konfiguracji ustawień i innych obiektów, które Ładuję i buforuję w pamięci. Obiekty w pamięci podręcznej powinny być zawsze niezmienne, ponieważ chcesz zagwarantować, że nie zostaną nieoczekiwanie zmieniony.

Teraz można oczywiście łatwo zaprojektować obiekty niezmienne, jak w poniższym przykładzie:

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

Jest to w porządku dla prostych klas - ale dla bardziej złożonych klas nie podoba mi się koncepcja przekazywania wszystkich wartości przez konstruktor. Posiadanie ustawiaczy na właściwościach jest bardziej pożądane, a kod konstruowania nowego obiektu staje się łatwiejszy do odczytania.

Jak więc tworzyć obiekty niezmienne za pomocą seterów?

Cóż, w moim wzorcu obiekty zaczynają się jako w pełni mutable dopóki nie zamrozisz ich jednym wywołaniem metody. Gdy obiekt zostanie zamrożony, pozostanie niezmienny na zawsze - nie można go ponownie zmienić w zmienny obiekt. Jeśli potrzebujesz mutowalnej wersji obiektu, po prostu go Sklonuj.

Ok, przejdźmy do kodu. W poniższych fragmentach kodu próbowałem sprowadzić wzór do najprostszej postaci. IElement jest interfejsem bazowym, który wszystkie niezmienne obiekty muszą ostatecznie zaimplementować.
public interface IElement : ICloneable
{
  bool IsReadOnly { get; }
  void MakeReadOnly();
}

Klasa elementu jest domyślna implementacja interfejsu 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);
  }

  ...
}

Przeformułujmy klasę SampleElement powyżej, aby zaimplementować niezmienny wzorzec obiektu:

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

Można teraz zmienić właściwość Id oraz właściwość Name, o ile obiekt nie został oznaczony jako niezmienny przez wywołanie metody MakeReadOnly (). Gdy jest niezmienny, wywołanie settera spowoduje ImmutableElementException.

Uwaga końcowa: Pełny wzór jest bardziej złożony niż urywki kodu pokazane tutaj. To także zawiera wsparcie dla kolekcji obiektów niezmiennych i kompletnych Wykresów obiektów niezmiennych. Pełny wzorzec umożliwia zmianę niezmiennego wykresu całego obiektu poprzez wywołanie metody MakeReadOnly () na najbardziej oddalonym obiekcie. Po rozpoczęciu tworzenia większych modeli obiektów za pomocą tego wzorca zwiększa się ryzyko nieszczelności obiektów. Nieszczelny obiekt jest obiektem, który nie wywoła metody FailIfImmutable () przed dokonaniem zmiany w obiekcie. Aby przetestować wycieki opracowałem również klasy ogólnego detektora nieszczelności do stosowania w testach jednostkowych. Używa odbicia, aby sprawdzić, czy wszystkie właściwości i metody rzucają ImmutableElementException w stanie niezmiennym. Innymi słowy TDD jest tutaj używany.

Bardzo polubiłem ten wzór i odnajduję w nim wielkie korzyści. Więc chciałbym wiedzieć, czy ktoś z was używa podobnych wzorów? Jeśli tak, czy znasz jakieś dobre zasoby, które je dokumentują? Zasadniczo Szukam potencjalnych ulepszeń i wszelkich standardów, które może już istnieć w tym temacie.

Author: Lars Fastrup, 2008-11-05

15 answers

Dla informacji, drugie podejście nazywa się "niezmienność popsicle".

Eric Lippert ma serię wpisów na blogu o niezmienności począwszy tutaj . Wciąż mam do czynienia z CTP( C# 4.0), ale ciekawie wygląda jakie opcjonalne / nazwane parametry (do .ctor) może to zrobić tutaj (gdy mapowane są do pól readonly)... [update: blogowałem o tym tutaj ]

Dla informacji, prawdopodobnie nie robiłbym tych metod virtual - prawdopodobnie nie chcemy, aby podklasy były w stanie aby nie był zamrażalny. Jeśli chcesz, aby mogli dodać dodatkowy kod, proponuję coś w stylu:

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

Również-AOP (taki jak PostSharp) może być realną opcją do dodania wszystkich sprawdzeń ThrowIfFrozen ().

(przepraszam, jeśli zmieniłem terminologię / nazwy metod - więc oryginalny post nie jest widoczny podczas komponowania odpowiedzi)

 31
Author: Marc Gravell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-08 12:37:12

Inną opcją byłoby stworzenie jakiejś klasy konstruktora.

Dla przykładu, w Javie (oraz C# i wielu innych językach) ciąg znaków jest niezmienny. Jeśli chcesz wykonać wiele operacji, aby utworzyć ciąg znaków, używasz StringBuilder. Jest to zmienna, a po zakończeniu masz go zwrócić do ciebie ostatni obiekt String. Od tego czasu jest niezmienny.

Mógłbyś zrobić coś podobnego na innych zajęciach. Masz swój niezmienny Element, a następnie ElementBuilder. Wszystko, co zrobi konstruktor, to przechowuje ustawione opcje, a następnie po sfinalizowaniu konstruuje i zwraca niezmienny Element.

To trochę więcej kodu, ale myślę, że jest czystsze niż posiadanie seterów na klasie, która ma być niezmienna.

 17
Author: Herms,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-04 21:59:32

Po moim początkowym dyskomforcie z powodu tego, że musiałem tworzyć nowe System.Drawing.Point przy każdej modyfikacji, w pełni zaakceptowałem tę koncepcję kilka lat temu. W rzeczywistości teraz tworzę każde pole jako readonly domyślnie i zmieniam je na mutowalne tylko wtedy, gdy istnieje przekonujący powód – który jest zaskakująco rzadko.

Nie dbam zbytnio o kwestie cross-threadingu (rzadko używam kodu tam, gdzie jest to istotne). Po prostu uważam to za dużo, dużo lepsze ze względu na semantyczną ekspresję. Niezmienność jest bardzo uosobieniem interfejsu, który jest trudny do nieprawidłowego użycia.

 9
Author: Konrad Rudolph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-04 21:58:42

Nadal masz do czynienia ze stanem, a zatem nadal możesz zostać ugryziony, jeśli twoje obiekty są zrównoleglone przed uczynieniem ich niezmiennymi.

Bardziej funkcjonalnym sposobem może być zwrócenie nowej instancji obiektu z każdym ustawiaczem. Lub utworzyć zmienny obiekt i przekazać go konstruktorowi.

 8
Author: Cory Foy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-04 21:53:14

(stosunkowo) nowy paradygmat projektowania oprogramowania o nazwie Domain Driven design, rozróżnia obiekty encji i Obiekty wartości.

Obiekty encji są definiowane jako wszystko, co musi być mapowane do obiektu opartego na kluczach w trwałym magazynie danych, takiego jak pracownik, klient, faktura itp... gdzie zmiana właściwości obiektu oznacza konieczność zapisania zmiany w jakimś magazynie danych oraz istnienie wielu instancji klasy o tym samym "klucz" imnplies potrzebę ich synchronizacji, lub koordynować ich trwałość do magazynu danych tak, że jedna instancja' zmiany nie nadpisać inne. Zmiana właściwości obiektu encji oznacza, że zmieniasz coś w obiekcie - nie zmieniasz, do którego obiektu się odwołujesz...

Obiekty wartości otoh, to obiekty, które można uznać za niezmienne, których użyteczność jest ściśle określona przez ich wartości właściwości i dla których wiele instancji nie musi być skoordynowane w jakikolwiek sposób... jak adresy, numery telefonów, koła w samochodzie, czy litery w dokumencie... te rzeczy są całkowicie zdefiniowane przez ich właściwości... obiekt " A "Z wielkiej litery w edytorze tekstu może być przeźroczysty z dowolnym innym obiektem" A "z wielkiej litery w całym dokumencie, nie potrzebujesz klucza, aby odróżnić go od wszystkich innych" A "w tym sensie, że jest niezmienny, ponieważ jeśli zmienisz go na" B " (podobnie jak zmiana ciągu numeru telefonu w dokumencie). obiekt numer telefonu, nie zmieniasz danych związanych z jakimś mutowalnym elementem, przełączasz się z jednej wartości na drugą... tak jak w przypadku zmiany wartości ciągu znaków...

 6
Author: Charles Bretana,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-05 01:41:26

System.String jest dobrym przykładem niezmiennej klasy z seterami i metodami mutującymi, tyle że każda metoda mutująca zwraca nową instancję.

 4
Author: dalle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-04 21:56:12

Rozszerzenie punktu przez @ Cory Foy i @ Charles Bretana, gdzie istnieje różnica między bytami a wartościami. Podczas gdy obiekty wartości zawsze powinny być niezmienne, naprawdę nie sądzę, że obiekt powinien być w stanie zamrozić się lub pozwolić sobie na arbitralne zamrożenie w bazie kodowej. Ma naprawdę nieprzyjemny zapach i obawiam się, że może być trudno wyśledzić, gdzie dokładnie obiekt został zamrożony, i dlaczego został zamrożony, oraz fakt, że między wywołaniami do obiektu może zmienić stan z rozmrożonego na zamrożony.

Nie oznacza to, że czasami chcesz dać (mutowalną) byt czemuś i upewnić się, że nie zostanie zmieniona.

Więc, zamiast zamrażać sam obiekt, inną możliwością jest skopiowanie semantyki ReadOnlyCollection

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

Twój obiekt może wziąć udział jako zmienny, gdy tego potrzebuje, a następnie być niezmienny, gdy tego pragniesz.

Zauważ, że ReadOnlyCollection również implementuje ICollection, który posiada metodę Add( T item) w interfejsie. Jednakże istnieje również bool IsReadOnly { get; } zdefiniowany w interfejsie, aby konsumenci mogli sprawdzić przed wywołaniem metody, która rzuci wyjątek.

Różnica polega na tym, że nie można po prostu ustawić IsReadOnly na false. Zbiór jest lub nie jest tylko do odczytu i nigdy się to nie zmienia przez cały okres istnienia kolekcji.

Byłoby miło mieć const-poprawność, którą daje Ci C++ podczas kompilacji, ale to zaczyna mieć to własny zestaw problemów i cieszę się, że C# tam nie idzie.


ICloneable - pomyślałem, że odnoszę się tylko do następującego:

Nie implementuj ICloneable

Nie używaj ICloneable w publicznych API

Brad Abrams-Design Guidelines, Managed code and the. Net Framework

 4
Author: Robert Paulson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-04 23:48:19

Jest to ważny problem i uwielbiam widzieć bardziej bezpośrednie wsparcie frameworku/języka, aby go rozwiązać. Rozwiązanie, które posiadasz, wymaga dużej ilości kotła. Może to być proste, aby zautomatyzować część kotła za pomocą generowania kodu.

Można wygenerować klasę częściową, która zawiera wszystkie właściwości freezable. Byłoby dość proste stworzenie do tego szablonu T4 wielokrotnego użytku.

Szablon pobierze to do wprowadzenia:

  • przestrzeń nazw
  • klasa nazwa
  • lista właściwości nazwa / typ krotki

I wyświetli plik C# zawierający:

  • deklaracja przestrzeni nazw
  • klasa częściowa
  • każda z właściwości, wraz z odpowiadającymi im typami, pole pomocnicze, getter i setter wywołujący metodę FailIfFrozen

Znaczniki AOP na właściwościach freezable również mogłyby działać, ale wymagałyby więcej zależności, podczas gdy T4 jest wbudowany w nowsze wersje Visual Studio.

Inny scenariusz, który jest bardzo podobny do tego, to interfejs INotifyPropertyChanged. Rozwiązania tego problemu mogą mieć zastosowanie do tego problemu.

 4
Author: Merlyn Morgan-Graham,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-04 16:11:12

Mój problem z tym wzorcem polega na tym, że nie nakładasz żadnych ograniczeń czasu kompilacji na niezmienność. Koder jest odpowiedzialny za upewnienie się, że obiekt jest ustawiony na immutable przed na przykład dodaniem go do pamięci podręcznej lub innej struktury nie zabezpieczającej wątków.

Dlatego rozszerzyłbym ten wzór kodowania o ograniczenie czasu kompilacji w postaci klasy generycznej, jak to:

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

Oto przykład jak byś tego użył:

// 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
 3
Author: Jos Bosmans,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-06 13:36:49

Wystarczy wskazówka, aby uprościć właściwości elementu: Użyj właściwości automatycznych z private set i unikaj jawnego deklarowania pola danych. np.

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;
  }
}
 2
Author: spoulson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-05 12:37:53

Oto nowy film na kanale 9, gdzie Anders Hejlsberg z 36: 30 w wywiadzie zaczyna mówić o niezmienności w C#. Daje bardzo dobry przypadek użycia dla niezmienności lodów i wyjaśnia, w jaki sposób jest to coś, co jest obecnie wymagane do wdrożenia siebie. To była muzyka dla moich uszu słysząc jak mówi, że warto pomyśleć o lepszym wsparciu dla tworzenia niezmiennych Wykresów obiektowych w przyszłych wersjach C #

Ekspert do eksperta: Anders Hejlsberg-przyszłość C #

 2
Author: Lars Fastrup,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-08 09:37:26

Dwie inne opcje dla konkretnego problemu, które nie zostały omówione:

  1. Zbuduj swój własny deserializer, który może nazwać prywatnym seterem nieruchomości. Podczas gdy wysiłek w budowie deserializera na początku będzie znacznie większy, sprawia, że rzeczy są czystsze. Kompilator powstrzyma cię nawet od próby wywołania setterów, a kod w klasach będzie łatwiejszy do odczytania.

  2. Umieść konstruktor w każdej klasie, która przyjmuje XElement (lub jakiś inny model obiektowy XML) i zapełnia się z niego. Oczywiście wraz ze wzrostem liczby klas, szybko staje się to mniej pożądane jako rozwiązanie.

 2
Author: Neil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-23 20:23:35

A co z klasą abstrakcyjną ThingBase, z podklasami MutableThing i ImmutableThing? ThingBase zawiera wszystkie dane w chronionej strukturze, zapewniając publiczne właściwości tylko do odczytu dla pól i chronioną właściwość tylko do odczytu dla jego struktury. Zapewni to również nadpisywalną metodę AsImmutable, która zwróci niezmienność.

MutableThing przyciemni właściwości właściwościami do odczytu/zapisu i zapewni zarówno domyślny konstruktor, jak i konstruktor, który akceptuje ThingBase.

Immutable thing będzie klasą zamkniętą, która nadpisuje AsImmutable, aby po prostu zwrócić samą siebie. Dostarczy również konstruktora, który akceptuje ThingBase.

 2
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-30 20:51:28

Nie podoba mi się pomysł, aby móc zmienić obiekt ze stanu zmiennego na niezmienny, który wydaje mi się pokonywać punkt projektowania. Kiedy musisz to zrobić? Tylko obiekty reprezentujące wartości powinny być niezmienne

 2
Author: Andrew Bullock,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-06 21:51:30

Możesz użyć opcjonalnych nazwanych argumentów razem z nullables, aby utworzyć niezmienny setter z bardzo małą płytą kotła. Jeśli naprawdę chcesz ustawić właściwość NA null, możesz mieć więcej problemów.

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
        ) 
    {
        ....
    }
}

Użyłbyś go tak

var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);
 2
Author: bradgonesurfing,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-10 14:56:15