2014-09-29 3 views
10

인스턴스를 변경하지 못하게하는 쉬운 방법이 있습니까?불변의 클래스를 만드는 쉬운 방법이 있습니까?

class MyObject 
{ 
    // lots of fields painful to initialize all at once 
    // so we make fields mutable : 

    public String Title { get; set; } 
    public String Author { get; set; } 

    // ... 
} 

창조의 예 :

MyObject CreationExample(String someParameters) 
{ 
    var obj = new MyObject 
    { 
     Title = "foo" 
     // lots of fields initialization 
    }; 

    // even more fields initialization 
    obj.Author = "bar"; 

    return obj; 
} 

하지만 지금 내가 가지고있는 것을

의이 예를하자, 나는 데이터 필드의 많은 (데이터 만, 어떤 동작을) 들고 클래스가 개체를 완전히 만들었으므로 더 이상 개체를 변경할 수 없기 때문에 (데이터 소비자가 상태를 변경할 필요가 없기 때문에) List.AsReadOnly :

var immutableObj = obj.AsReadOnly(); 

하지만이 동작을 원한다면 정확히 동일한 필드가 있지만 설정자가없는 다른 클래스를 만들어야합니다.

그래서이 불변 클래스를 자동 생성하는 방법이 있습니까? 또는 생성 중에 변경 가능하지만 일단 초기화되면 변경할 수있는 또 다른 방법은 무엇입니까?

필드는 "읽기 전용"으로 표시 될 수 있지만 클래스 외부에서 개체가 초기화되고 모든 필드를 생성자 매개 변수로 전달하는 것은 나쁜 아이디어 (너무 많은 매개 변수)처럼 보입니다.

너무 많은 매개 변수가 있고 .... 매개 변수를 생성자를하고 싶은 해달라고하면
+0

성 동적 프록시에서 원래의 예를 기반으로 당신 돈 생성자 호출에서 초기화하려고하지 않습니다 (어떤 이유로). 내 머리 꼭대기에서, 당신이 정말로 이것을하고 싶다면 변경 불가능한 클래스를 구성하기 위해 모든 데이터 항목을 담을 수있는 변경 가능한 도우미 클래스를 사용할 수 있다고 말하고 싶다. 헬퍼 클래스를 조금씩 초기화 한 다음, 헬퍼 클래스의 인스턴스를 전달하여 변경 불가능한 클래스를 생성하십시오. 이것이 아주 좋은 패턴인지는 모르겠습니다. –

+2

에릭 리 퍼트 (Eric Lippert)의 "popsicle immutability"에 대한 검색 –

+0

@AlexeiLevenkov : Lippert가 "Popsicle immutability"(아이디어의 위대한 이름 임)를 언급하는 블로그 글을 볼 수 있지만, 그가 말하는 곳은 없습니다. 그것을 구현하기위한 패턴에 대해. –

답변

7

아니, 당신은 (어떤 변경 가능한 객체가 불변 객체를 통해 도달 할 수없는 즉,) "깊은"불변성을 원하는 특히하지 않을 경우 어떤 유형을 변경할 수 있도록하는 쉬운 방법이 없습니다. 타입을 변경할 수 없도록 명시 적으로 디자인해야합니다. 유형을 변경할 수 없게 만드는 일반적인 메커니즘은 다음과 같습니다.

  • readonly을 선언하십시오. (또는 C# 6/Visual Studio 2015로 시작하여 read-only auto-implemented properties을 사용하십시오.)
  • 속성 설정자, getter 만 노출시키지 마십시오.

  • 필드를 초기화 (속성 백업)하려면 생성자에서 필드를 초기화해야합니다. 따라서 생성자에 (속성) 값을 전달하십시오.

  • 변경 가능한 기본값 형식 (예 : T[], List<T>, Dictionary<TKey,TValue> 등)을 기반으로하는 모음과 같은 변경 가능한 개체를 노출하지 마십시오.

    컬렉션을 노출해야하는 경우 수정하지 못하게하는 래퍼 (예 : .AsReadOnly())로 반환하거나 적어도 내부 컬렉션의 새로운 복사본을 반환하십시오.

  • 작성자 패턴을 사용하십시오. 다음 예는 패턴 정의를 수행하기에는 너무 쉽습니다. 보통 사소한 오브젝트 그래프를 작성해야하는 경우 권장됩니다.

    class FooBuilder // mutable version used to prepare immutable objects 
    { 
        public int X { get; set; } 
        public List<string> Ys { get; set; } 
        public Foo Build() 
        { 
         return new Foo(x, ys); 
        } 
    } 
    
    class Foo // immutable version 
    { 
        public Foo(int x, List<string> ys) 
        { 
         this.x = x; 
         this.ys = new List<string>(ys); // create a copy, don't use the original 
        }         // since that is beyond our control 
        private readonly int x; 
        private readonly List<string> ys; 
        … 
    } 
    
+1

작성자 패턴의 경우 – Basilevs

0

음, 여기에 옵션

class MyObject 
     { 
      private string _title; 
      private string _author; 
      public MyObject() 
      { 

      } 

      public String Title 
      { 
       get 
       { 
        return _title; 
       } 

       set 
       { 
        if (String.IsNullOrWhiteSpace(_title)) 
        { 
         _title = value; 
        } 
       } 
      } 
      public String Author 
      { 
       get 
       { 
        return _author; 
       } 

       set 
       { 
        if (String.IsNullOrWhiteSpace(_author)) 
        { 
         _author = value; 
        } 
       } 
      } 

      // ... 
     } 
+0

많은 필드 (10 개 이상)가 매개 변수로 모두 전달되는 것이 좋습니다. (대부분의 필드는 받아 들일 수있는 기본값을 가지고 있습니다) – anopse

+0

@anopse 글쎄, 내 대답을 업데이트했습니다 ... 좋은 해결책은 아니지만 도움이 될 수도 있습니다 –

+0

어떻게 그게 변경되지 않습니까? – Paparazzi

1
당신은 가지 질문의 방법 암시,하지만 난 '

이것은 당신을위한 옵션이 아닌 경우 잘 모르겠어요 : 인해 생성자가 당신의 저자와 제목을 조작하는 유일한 방법 인에

class MyObject 
{ 
    // lots of fields painful to initialize all at once 
    // so we make fields mutable : 

    public String Title { get; protected set; } 
    public String Author { get; protected set; } 

    // ... 

    public MyObject(string title, string author) 
    { 
     this.Title = title; 
     this.Author = author; 
    } 
} 

클래스는 사실상 건설 후 변경할 수 없습니다.

편집 :

언급 stakx, 나는 너무 빌더를 사용하는 큰 팬이다 - 그것은 단위 테스트를 쉽게 특히 때문이다. 위 클래스의 경우는 같은 빌더 수 : 당신이 기본 값으로 객체를 생성하거나, 제발 당신이 그들을 대체 할 수

public class MyObjectBuilder 
{ 
    private string _author = "Default Author"; 
    private string _title = "Default title"; 

    public MyObjectBuilder WithAuthor(string author) 
    { 
     this._author = author; 
     return this; 
    } 

    public MyObjectBuilder WithTitle(string title) 
    { 
     this._title = title; 
     return this; 
    } 

    public MyObject Build() 
    { 
     return new MyObject(_title, _author); 
    } 
} 

이 방법을하지만 MyObject를의 특성은 건설 후에는 변경할 수 없습니다.

// Returns a MyObject with "Default Author", "Default Title" 
MyObject obj1 = new MyObjectBuilder.Build(); 

// Returns a MyObject with "George R. R. Martin", "Default Title" 
MyObject obj2 = new MyObjectBuilder 
    .WithAuthor("George R. R. Martin") 
    .Build(); 

당신이 이제까지 당신의 클래스에 새 속성을 추가해야하는 경우

, 그것은 내가 무엇을 전화를 모른다 (빌더에서보다는 하드 코딩 된 객체 인스턴스화에서 소비 단위 테스트로 돌아갈 훨씬 쉽게 그것, 그래서 용서해). 나는이 내 첫번째 생각을 열거합니다

+0

많은 필드 (10 개 이상)가 매개 변수로 모두 전달되는 것이 좋습니다. (대부분의 필드는 수용 가능한 기본값을 가짐) – anopse

+0

이 솔루션은이 특별한 경우에는 작동하지만 일반적으로는 작동하지 않습니다. 우리가'class C'라는 속성을 가지고 있다고 가정 해 봅시다. List Foos {get; 보호 된 집합; }'. getter만이 public 인 경우에도 'Foos' 컬렉션을 수정할 수 있기 때문에'C' 타입의 객체를 수정할 수 있습니다 : var c = new C (...); c.Foos.Add (new Foo (...)); 즉, 그러한 객체는 "깊이"불변하지 않습니다. – stakx

+0

@ lulian 좋은 생각처럼 보이지 않습니까? 그것은 작동합니다. 기본값은 null로 지정할 수 있습니다. – Paparazzi

2

흠 ... 유일한 걱정 조작 어셈블리의 외부에있는 경우

1. 사용 internal 세터. internal은 동일한 어셈블리의 클래스에서만 속성을 사용할 수있게합니다. 예 :

public class X 
{ 
    // ... 
    public int Field { get; internal set; } 

    // ... 
} 

2. 나는 생성자에 많은 매개 변수를 가지는 것이 좋지 않다는 것에 동의하지 않는다.

3. 런타임시 다른 유형을 생성 할 수 있습니다. 이는 해당 유형의 읽기 전용 버전입니다. 나는 이것에 대해 자세히 설명 할 수 있지만, 개인적으로 나는 이것이 과잉이라고 생각한다.

최저

, 울리

+0

또한 가능하면 작은 클래스로 리팩토링을 시도 할 수 있습니다. 전의. "Address1, address2, city, state, zip"속성 대신에 "Address"를 나타내는 개체를 만듭니다. 이것은 객체 생성자를 잠재적으로 더 어렵게 만들 수도 있지만, 이것이 빌더를 사용할 수있는 이유입니다. (그리고 나는 다른 패턴을 확신합니다). – Kritner

2

내가 파생 된 형식 MutableMyObjectImmutableMyObject와 함께 추상 기본 유형 ReadableMyObject을 가진 제안 : 그럼에도 불구하고, 기본적인 아이디어는이 같은 것입니다. 모든 유형의 생성자가 ReadableMyObject을 허용하고 ReadableMyObject에 대한 모든 속성 설정 자의지지 필드를 업데이트하기 전에 초록 ThrowIfNotMutable 메서드를 호출해야합니다. 또한 ReadableMyObject은 공개 초록 AsImmutable() 방법을 지원하십시오.

이 방법을 사용하면 개체의 각 속성에 대한 상용구를 작성해야하지만 이는 필요한 코드 복제의 범위가됩니다. MutableMyObjectImmutableMyObject의 생성자는 수신 된 객체를 기본 클래스 생성자로 전달합니다. MutableMyObject 클래스는 아무것도 수행하지 않으려면 ThrowIfNotMutable을 구현하고 new ImmutableMyObject(this);을 반환하려면 AsImmutable()을 구현해야합니다. 클래스 ImmutableByObjectThrowIfNotMutable을 구현하여 예외를 발생시키고 AsImmutable()return this;으로 구현해야합니다.

ReadableMyObject을 수신하고 그 내용을 유지하려는 코드는 AsImmutable() 메서드를 호출하고 그 결과로 ImmutableMyObject을 저장해야합니다. ReadableMyObject을 받고 약간 수정 된 버전을 원한다면 new MutableMyObject(theObject)을 호출하고 필요에 따라 수정해야합니다.

0

다른 옵션이 있습니다. protected 멤버를 가진 기본 클래스와 구성원을 public으로 재정의하는 파생 클래스를 선언하십시오.

public abstract class MyClass 
{ 
    public string Title { get; protected set; } 
    public string Author { get; protected set; } 

    public class Mutable : MyClass 
    { 
     public new string Title { get { return base.Title; } set { base.Title = value; } } 
     public new string Author { get { return base.Author; } set { base.Author = value; } } 
    } 
} 

코드를 생성하면 파생 클래스가 사용됩니다. 당신이 동적 프록시를 사용할 수있는 또 다른 방법은

void DoSomething(MyClass immutableInstance) { ... } 
3

:

MyClass immutableInstance = new MyClass.Mutable { Title = "Foo", "Author" = "Your Mom" }; 

그러나 불변성이 예상되는 모든 경우에 대한

는, 기본 클래스를 사용합니다. Entity Framework http://blogs.msdn.com/b/adonet/archive/2009/12/22/poco-proxies-part-1.aspx에 대해 비슷한 방식이 사용되었습니다. 다음은 Castle.DynamicProxy 프레임 워크를 사용하여 수행하는 방법의 예입니다. 이 코드는 당신이 필드를 많이 가지고 어디 문제에 대한 해결책에 관심이 분명히 있음을 확인하기 위해 질문을 수정해야한다 ( http://kozmic.net/2008/12/16/castle-dynamicproxy-tutorial-part-i-introduction/)

namespace ConsoleApplication8 
{ 
using System; 
using Castle.DynamicProxy; 

internal interface IFreezable 
{ 
    bool IsFrozen { get; } 
    void Freeze(); 
} 

public class Pet : IFreezable 
{ 
    public virtual string Name { get; set; } 
    public virtual int Age { get; set; } 
    public virtual bool Deceased { get; set; } 

    bool _isForzen; 

    public bool IsFrozen => this._isForzen; 

    public void Freeze() 
    { 
     this._isForzen = true; 
    } 

    public override string ToString() 
    { 
     return string.Format("Name: {0}, Age: {1}, Deceased: {2}", Name, Age, Deceased); 
    } 
} 

[Serializable] 
public class FreezableObjectInterceptor : IInterceptor 
{ 
    public void Intercept(IInvocation invocation) 
    { 
     IFreezable obj = (IFreezable)invocation.InvocationTarget; 
     if (obj.IsFrozen && invocation.Method.Name.StartsWith("set_", StringComparison.OrdinalIgnoreCase)) 
     { 
      throw new NotSupportedException("Target is frozen"); 
     } 

     invocation.Proceed(); 
    } 
} 

public static class FreezableObjectFactory 
{ 
    private static readonly ProxyGenerator _generator = new ProxyGenerator(new PersistentProxyBuilder()); 

    public static TFreezable CreateInstance<TFreezable>() where TFreezable : class, new() 
    { 
     var freezableInterceptor = new FreezableObjectInterceptor(); 
     var proxy = _generator.CreateClassProxy<TFreezable>(freezableInterceptor); 
     return proxy; 
    } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     var rex = FreezableObjectFactory.CreateInstance<Pet>(); 
     rex.Name = "Rex"; 

     Console.WriteLine(rex.ToString()); 
     Console.WriteLine("Add 50 years"); 
     rex.Age += 50; 
     Console.WriteLine("Age: {0}", rex.Age); 
     rex.Deceased = true; 
     Console.WriteLine("Deceased: {0}", rex.Deceased); 
     rex.Freeze(); 

     try 
     { 
      rex.Age++; 
     } 
     catch (Exception ex) 
     { 
      Console.WriteLine("Oups. Can't change that anymore"); 
     } 

     Console.WriteLine("--- press enter to close"); 
     Console.ReadLine(); 
    } 
} 
} 
+1

ProxyGenerator.CreateClassProxyWithTarget (existingObject, interceptor)을 사용할 수도 있습니다. 그렇다면 IFreezable 인터페이스를 구현하고 IsFrozen에 대한 추가 논리를 추가 할 필요가 없습니다. 이 경우 반드시해야 할 일은 필요한 모든 필드를 미리 설정하고 프록시를 반환하기 위해 생성기를 호출하는 것입니다. – Celestis

관련 문제