문제

나는 몇 가지 프로젝트를 통해 불변(읽기 전용) 객체와 불변 객체 그래프를 생성하기 위한 패턴을 개발했습니다.불변 객체는 100% 스레드로부터 안전하다는 이점을 제공하므로 스레드 간에 재사용할 수 있습니다.내 작업에서는 메모리에 로드하고 캐시하는 구성 설정 및 기타 개체에 대해 웹 애플리케이션에서 이 패턴을 자주 사용합니다.캐시된 객체는 예기치 않게 변경되지 않도록 보장하기 위해 항상 불변이어야 합니다.

이제 다음 예제와 같이 불변 객체를 쉽게 디자인할 수 있습니다.

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

간단한 클래스에는 괜찮습니다. 그러나 더 복잡한 클래스의 경우 생성자를 통해 모든 값을 전달하는 개념이 마음에 들지 않습니다.속성에 설정자를 갖는 것이 더 바람직하며 새 개체를 구성하는 코드를 읽기가 더 쉬워집니다.

그렇다면 setter를 사용하여 불변 객체를 어떻게 생성합니까?

글쎄, 내 패턴 개체는 단일 메서드 호출로 개체를 고정할 때까지 완전히 변경 가능한 것으로 시작합니다.개체가 고정되면 영원히 변경 불가능한 상태로 유지됩니다. 즉, 다시 변경 가능한 개체로 바뀔 수 없습니다.객체의 변경 가능한 버전이 필요한 경우 간단히 복제하면 됩니다.

좋아, 이제 몇 가지 코드를 살펴보자.다음 코드 조각에서는 패턴을 가장 간단한 형식으로 요약하려고 했습니다.IElement는 모든 불변 객체가 궁극적으로 구현해야 하는 기본 인터페이스입니다.

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

Element 클래스는 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);
  }

  ...
}

불변 객체 패턴을 구현하기 위해 위의 SampleElement 클래스를 리팩토링해 보겠습니다.

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

이제 MakeReadOnly() 메서드를 호출하여 객체가 불변으로 표시되지 않은 한 Id 속성과 Name 속성을 변경할 수 있습니다.불변인 경우 setter를 호출하면 ImmutableElementException이 발생합니다.

최종 참고사항:전체 패턴은 여기에 표시된 코드 조각보다 더 복잡합니다.또한 불변 객체 컬렉션과 불변 객체 그래프의 완전한 객체 그래프에 대한 지원도 포함되어 있습니다.전체 패턴을 사용하면 가장 바깥쪽 개체에서 MakeReadOnly() 메서드를 호출하여 전체 개체 그래프를 불변으로 전환할 수 있습니다.이 패턴을 사용하여 더 큰 객체 모델을 만들기 시작하면 객체 누출 위험이 증가합니다.누출 개체는 개체를 변경하기 전에 FailIfImmutable() 메서드를 호출하지 못하는 개체입니다.누출을 테스트하기 위해 단위 테스트에 사용할 일반 누출 감지기 클래스도 개발했습니다.리플렉션을 사용하여 모든 속성과 메서드가 불변 상태에서 ImmutableElementException을 발생시키는지 테스트합니다.즉, 여기서는 TDD가 사용됩니다.

나는 이 패턴을 많이 좋아하게 되었고 그 안에서 큰 이점을 발견하게 되었습니다.그렇다면 제가 알고 싶은 것은 여러분 중에 비슷한 패턴을 사용하고 있는 분이 계시다는 것입니다.그렇다면 이를 문서화하는 좋은 리소스를 알고 있습니까?저는 잠재적인 개선 사항과 이 주제에 대해 이미 존재할 수 있는 표준을 찾고 있습니다.

도움이 되었습니까?

해결책

정보를 위해 두 번째 접근 방식을 "Popsicle Emutibility"라고합니다.

Eric Lippert에는 불변성 시작에 대한 일련의 블로그 항목이 있습니다. 여기. 나는 여전히 CTP (c# 4.0)를 잡고 있지만, 옵션 / 명명 된 매개 변수 (.ctor에)가 여기에서 (readonly 필드에 매핑 될 때) 할 수있는 것은 흥미로워 보입니다 ... [업데이트 : 블로그 이에 여기]

정보를 위해 아마도 그 방법을 만들지 않을 것입니다 virtual - 우리는 아마도 서브 클래스가 냉동 할 수없는 것을 만들 수 있기를 원하지 않을 것입니다. 추가 코드를 추가 할 수 있기를 원한다면 다음과 같은 것을 제안합니다.

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

또한 -AOP (예 : PostSharp)는 모든 Throwiffrozen () 점검을 추가하기위한 실용적인 옵션 일 수 있습니다.

(용어 / 메소드 이름을 변경 한 경우 사과 - 답장을 작성할 때 원본 게시물을 보지 않습니다)

다른 팁

또 다른 옵션은 일종의 Builder 클래스를 만드는 것입니다.

예를 들어 Java(및 C# 및 기타 여러 언어)에서 문자열은 변경할 수 없습니다.문자열을 생성하기 위해 여러 작업을 수행하려면 StringBuilder를 사용합니다.이는 변경 가능하며 작업이 완료되면 최종 String 개체로 반환됩니다.그 이후로는 불변입니다.

다른 수업에서도 비슷한 작업을 수행할 수 있습니다.불변 요소와 ElementBuilder가 있습니다.빌더가 수행하는 작업은 사용자가 설정한 옵션을 저장한 다음, 이를 마무리할 때 불변 요소를 구성하고 반환하는 것뿐입니다.

코드가 조금 더 많지만 불변으로 간주되는 클래스에 setter를 두는 것보다 더 깔끔하다고 생각합니다.

새로운 것을 만들어야 한다는 사실에 대한 처음의 불편함 이후 System.Drawing.Point 각 수정마다 나는 몇 년 전에 그 개념을 완전히 받아들였습니다.사실 저는 이제 모든 필드를 다음과 같이 만듭니다. readonly 기본적으로 설득력 있는 이유가 있는 경우에만 변경 가능하도록 변경합니다. 놀랍게도 그런 경우는 거의 없습니다.

하지만 저는 크로스스레딩 문제에 별로 관심이 없습니다(저는 이와 관련된 코드를 거의 사용하지 않습니다).나는 의미론적 표현력 때문에 훨씬 더 좋다고 생각합니다.불변성은 잘못 사용하기 어려운 인터페이스의 전형입니다.

여전히 상태를 다루고 있으므로 객체를 불변으로 만들기 전에 병렬화하면 문제가 발생할 수 있습니다.

보다 기능적인 방법은 각 setter를 사용하여 개체의 새 인스턴스를 반환하는 것입니다.또는 변경 가능한 객체를 생성하고 이를 생성자에 전달합니다.

도메인 구동 설계라고 불리는 (비교적) 새로운 소프트웨어 설계 패러다임은 엔티티 객체와 가치 객체를 구분합니다.

엔티티 객체는 직원, 클라이언트 또는 송장 등과 같은 영구 데이터 저장소에서 키 구동 객체에 매핑되어야하는 모든 것으로 정의됩니다. 변경 사항을 어딘가에 데이터 저장소로 저장하고 동일한 "키"를 갖는 여러 클래스 인스턴스의 존재를 동기화하거나 데이터 저장에 지속성을 조정하여 한 인스턴스의 변경 사항이 다른 인스턴스 변경을 덮어 쓰지 않도록해야합니다. . 엔티티 객체의 속성을 변경하면 객체에 대해 무언가를 변경하고 있음을 의미합니다.

가치 대상 OTOH는 불변으로 간주 될 수있는 개체이며, 유틸리티는 재산 값에 의해 엄격하게 정의되고 여러 인스턴스가 주소, 전화 번호 또는 바퀴와 같은 방식으로 어떤 식 으로든 조정할 필요가없는 개체입니다. 자동차 또는 문서의 문자에 ...이 것들은 속성에 의해 완전히 정의되어 있습니다 ... 텍스트 편집기의 객체는 문서 전체의 다른 대문자와 투명하게 상호 교환 할 수 있습니다. 다른 모든 'A'와 구별 할 키가 필요하지 않습니다.이 의미에서는 'B'로 변경하면 전화 번호 개체의 전화 번호 문자열을 변경하는 것과 같이, 당신은 아닙니다. 약간의 변이 가능한 엔티티와 관련된 데이터를 변경하면 한 값에서 다른 값으로 전환하는 것입니다 ... 문자열 값을 변경할 때와 마찬가지로 ...

System.String은 세터와 돌연변이 방법을 가진 불변의 클래스의 좋은 예이며, 각 돌연변이 방법만이 새로운 인스턴스를 반환한다는 것입니다.

엔티티와 값 사이에 차이가있는 @cory foy와 @charles bretana에 의해 지점을 확장합니다. 가치 객체는 항상 불변이어야하지만, 나는 물체가 스스로 얼어 붙을 수 있거나 코드베이스에서 임의로 얼어 붙을 수 있다고 생각하지 않습니다. 그것은 정말로 냄새가 좋지 않으며, 정확히 물체가 얼어 붙은 곳을 추적하기가 어려워 질 수 있고 왜 얼어 붙은 이유와 물체에 대한 호출 사이에 상태가 해동에서 얼어 붙은 상태로 변경 될 수 있다는 사실이 걱정됩니다. .

그것은 때때로 당신이 (돌연변이 가능한) 엔티티를 무언가에주고 변경되지 않도록하기를 원한다고 말하는 것은 아닙니다.

따라서 물체 자체를 동결하는 대신 또 다른 가능성은 readonlycollection의 의미를 복사하는 것입니다 <t>

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

당신의 물체는 필요할 때 돌연변이 가능한 부분을 차지할 수 있으며, 원할 때 불변 할 수 있습니다.

readonlycollection <t>는 또한 icollection <t>를 구현합니다. Add( T item) 인터페이스의 메소드. 그러나 또한 있습니다 bool IsReadOnly { get; } 소비자가 예외를 던질 방법을 호출하기 전에 확인할 수 있도록 인터페이스에 정의됩니다.

차이점은 Isreadonly를 False로 설정할 수 없다는 것입니다. 컬렉션은 읽지 않거나 읽지 않으며 컬렉션의 수명 동안 변경되지 않습니다.

C ++가 컴파일 타임에 제공하는 const-correctness를 갖는 것이 좋을 것입니다. 그러나 그것은 자체 문제 세트를 갖기 시작하고 C#이 거기에 가지 않는 것이 기쁩니다.


ICLONEBLE - 나는 단지 다음을 다시 언급 할 것이라고 생각했다.

ICLoneable을 구현하지 마십시오

공개 API에서 iclonable을 사용하지 마십시오

Brad Abrams- 디자인 가이드 라인, 관리 코드 및 .NET 프레임 워크

이것은 중요한 문제이며,이를 해결하기 위해보다 직접적인 프레임 워크/언어 지원을보고 싶습니다. 가지고있는 솔루션에는 많은 보일러 플레이트가 필요합니다. 코드 생성을 사용하여 일부 보일러 플레이트를 자동화하는 것이 간단 할 수 있습니다.

모든 동결성 속성이 포함 된 부분 클래스를 생성합니다. 이를 위해 재사용 가능한 T4 템플릿을 만드는 것은 상당히 간단합니다.

템플릿은 입력을 위해 이것을 가져옵니다.

  • 네임 스페이스
  • 클래스 이름
  • 속성 이름/유형 튜플 목록

포함 된 C# 파일을 출력합니다.

  • 네임 스페이스 선언
  • 부분 수업
  • 해당 유형, 백킹 필드, getter 및 failiffrozen 메소드를 호출하는 세터와 함께 각 속성

동결성 속성에 대한 AOP 태그도 작동 할 수 있지만 T4는 새로운 버전의 Visual Studio에 내장되어있는 반면 더 많은 종속성이 필요합니다.

이것과 매우 유사한 또 다른 시나리오는 INotifyPropertyChanged 상호 작용. 이 문제에 대한 솔루션은이 문제에 적용 할 수 있습니다.

이 패턴의 문제는 불변성에 대한 컴파일 타임 제한을 부과하지 않는다는 것입니다. 코더는 예를 들어 캐시 또는 다른 비 스레드-안전 구조에 추가하기 전에 객체를 불변으로 설정하도록해야합니다.

그렇기 때문에이 코딩 패턴을 다음과 같은 일반 클래스 형태의 컴파일 타임 구속으로 확장하는 이유입니다.

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

다음은 이것을 사용하는 방법입니다.

// 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

요소 속성을 단순화하기위한 팁 : 사용 자동 특성 ~와 함께 private set 데이터 필드를 명시 적으로 선언하지 마십시오. 예를 들어

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

다음은 인터뷰에서 36:30의 Anders Hejlsberg가 C#의 불변성에 대해 이야기하기 시작하는 채널 9의 새로운 비디오입니다. 그는 아이스 사이클 불변성에 대한 아주 좋은 사용 사례를 제공하고 이것이 현재 자신이 자신을 구현하는 데 필요한 방법을 설명합니다. 내 귀에 음악이 들었습니다. 그는 미래 버전의 C#에서 불변의 물체 그래프를 만드는 데 더 나은 지원을 생각할 가치가 있다고 말하는 것을 듣고있었습니다.

전문가 대 전문가 : Anders Hejlsberg- C#의 미래

논의되지 않은 특정 문제에 대한 다른 두 가지 옵션 :

  1. 사유 재산 세터를 호출 할 수있는 자신의 deserializer를 구축하십시오. 처음에 디스 세 리아이저를 구축하려는 노력은 훨씬 더 많을 것이지만, 상황을 깨끗하게 만듭니다. 컴파일러는 세터를 호출하려고 시도하지 않으며 클래스의 코드를 읽기가 더 쉽습니다.

  2. xlement (또는 XML 객체 모델의 다른 맛)를 취하고 그 자체로 채워진 각 클래스에 생성자를 넣습니다. 분명히 클래스 수가 증가함에 따라 솔루션으로서 빠르게 바람직 해집니다.

하위 클래스가 상호 부정형과 불변의 비율로 추상적 인 클래스 thingbase를 갖는 것은 어떻습니까? ThingBase는 보호 된 구조의 모든 데이터를 포함하여 필드에 대한 공개 읽기 전용 특성 및 해당 구조에 대한 보호 된 읽기 전용 속성을 제공합니다. 또한 불변의 여성을 반환 할 수있는 우선적 인 무시할 수있는 방법을 제공 할 것입니다.

MutableThing은 속성을 읽기/쓰기 속성으로 쉐도우로 만들고 기본 생성자와 사물을 받아들이는 생성자를 제공합니다.

불변의 것은 단순히 스스로를 돌려주기 위해 무시할 수있는 것을 무시하는 봉인 된 클래스입니다. 또한 사물을 받아들이는 생성자를 제공합니다.

나는 물체를 돌연변이 가능한 상태에서 불변 상태로 바꿀 수 있다는 생각이 마음에 들지 않습니다. 언제해야합니까? 값을 나타내는 객체 만 불변해야합니다

Nullables와 함께 옵션으로 명명 된 인수를 사용하여 보일러 플레이트가 거의없는 불변의 세터를 만들 수 있습니다. 당신이 정말로 당신이 재산을 null로 설정하고 싶다면, 당신은 더 많은 어려움을 겪을 수 있습니다.

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

당신은 그렇게 사용할 것입니다

var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top