데이터 객체 유형을 불변으로 설계해야하는지 어떻게 결정합니까?


23

필자는 불변의 "패턴"이 장점이기 때문에 과거에는 불변의 데이터 유형 (일부, 대부분 또는 모두)을 가진 시스템을 설계하는 것이 유리하다는 것을 알게되었습니다. 종종 그렇게 할 때 버그를 적게 쓰고 디버깅이 훨씬 쉽습니다.

그러나 동료들은 일반적으로 불변에서 멀어졌습니다. 그들은 전혀 경험이 없지만 (그와는 거리가 멀지 만) 모든 객체에 대해 게터와 세터가있는 개인 멤버 인 고전적인 방식으로 데이터 객체를 씁니다. 그런 다음 일반적으로 생성자는 인수를 사용하지 않거나 편의를 위해 일부 인수를 사용합니다. 종종 객체 생성은 다음과 같습니다.

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

어쩌면 그들은 어디서나 할 수 있습니다. 아무리 중요하더라도 두 문자열을 취하는 생성자를 정의하지 않을 수도 있습니다. 그리고 나중에 해당 문자열의 값을 변경하지 않아도 될 필요가 없습니다. 분명히 그러한 것들이 사실이라면, 객체는 불변으로 더 잘 설계 될 것입니다. (생성자는 두 가지 속성을 취하지 만 세터는 전혀 사용하지 않습니다).

객체 유형을 불변으로 디자인해야하는지 어떻게 결정합니까? 그것을 판단 할 수있는 좋은 기준이 있습니까?

현재 내 프로젝트의 일부 데이터 유형을 변경 불가능으로 전환할지 여부에 대해 토론하고 있지만 동료에게이를 정당화해야하며 유형의 데이터가 (거의 거의) 변경 될 수 있습니다. 변경할 수없는 방법입니다 (변경하려는 것을 제외하고 이전 객체의 속성을 복사하여 새 객체를 만듭니다). 그러나 이것이 이것이 불변의 사람들에 대한 나의 사랑인지, 또는 그들로부터 실제로 필요 / 혜택이 있는지 확실하지 않습니다.


1
Erlang의 프로그램과 전체 문제가 해결되었습니다. 모든 것은 불변입니다
Zachary K

1
@ZacharyK 실제로 함수형 프로그래밍에 대해 언급 할 생각이 있었지만 함수형 프로그래밍 언어에 대한 제한된 경험으로 인해 자제했습니다.
Ricket

답변:


27

거꾸로 다가오는 것 같습니다. 하나는 기본적으로 불변입니다. 절대 변경 불가능한 객체로 작동 할 수없는 경우에만 객체를 변경 가능하게 만드십시오.


11

불변 개체의 주요 이점은 스레드 안전을 보장하는 것입니다. 여러 코어와 스레드가 표준 인 세계에서이 이점은 매우 중요해졌습니다.

그러나 가변 객체를 사용하는 것이 매우 편리합니다. 그것들은 잘 수행되며, 별도의 스레드에서 수정하지 않는 한 , 수행중인 작업을 잘 이해하고 있으면 신뢰할 수 있습니다.


15
불변 개체의 장점은 추론하기 쉽다는 것입니다. 멀티 스레드 환경에서는 이것이 훨씬 쉽습니다. 일반 단일 스레드 알고리즘에서는 여전히 더 쉽습니다. 예를 들어, 상태가 일관성이 있는지, 객체가 특정 컨텍스트에서 사용되는 데 필요한 모든 돌연변이를 통과했는지 여부를 신경 쓸 필요는 없습니다. 최상의 가변 객체를 사용하면 정확한 상태 (예 : 캐시)도 거의 신경 쓰지 않아도됩니다.
9000

-1, @ 9000에 동의합니다. 스레드 안전성은 2 차적입니다 (메모 화와 같이 불변으로 보이지만 내부 변경 가능 상태 인 오브젝트를 고려하십시오). 또한 변경 가능한 성능을 확보하는 것은 조기 최적화이며, 사용자가 "무엇을하고 있는지 알고 있어야"하는 경우에는 무엇이든 방어 할 수 있습니다. 내가 항상하고있는 일을 알았다면 결코 버그가있는 프로그램을 쓰지 않을 것입니다.
Doval

4
@Doval : 동의 할 것이 없습니다. 9000은 절대적으로 정확합니다. 불변 개체 추론하기 더 쉽습니다. 이것이 부분적으로 동시 프로그래밍에 유용한 이유입니다. 다른 부분은 상태 변경에 대해 걱정할 필요가 없다는 것입니다. 조기 최적화는 여기서 관련이 없습니다. 변경 불가능한 객체의 사용은 성능이 아니라 디자인에 관한 것이며 데이터 구조의 초기 선택은 초기 비관 화입니다.
Robert Harvey

@RobertHarvey "불량한 데이터 구조 선택"이 무슨 뜻인지 잘 모르겠습니다. 비슷한 성능을 제공하는 가장 가변적 인 데이터 구조의 불변 버전이 있습니다. 목록이 필요하고 변경 불가능한 목록과 변경 가능 목록을 사용할 수있는 선택 사항이있는 경우, 응용 프로그램에 병목 현상이 있는지 확인하고 변경 가능 버전이 더 잘 수행 될 때까지 변경 불가능한 목록을 사용하십시오.
Doval

2
@Doval : 오카 사키를 읽었습니다. 이러한 데이터 구조는 Haskell 또는 Lisp와 같은 기능적 패러다임을 완전히 지원하는 언어를 사용하지 않는 한 일반적으로 사용되지 않습니다. 그리고 나는 불변의 구조가 기본 선택이라는 생각에 이의를 제기한다. 대부분의 비즈니스 컴퓨팅 시스템은 여전히 ​​가변 구조 (예 : 관계형 데이터베이스)를 중심으로 설계되었습니다. 불변의 데이터 구조로 시작하는 것은 좋은 생각이지만 여전히 상아탑입니다.
Robert Harvey

6

객체를 변경할 수 없는지 결정하는 두 가지 주요 방법이 있습니다.

a) 객체의 특성에 기초

이러한 객체는 생성 된 후에도 변경되지 않음을 알기 때문에 이러한 상황을 쉽게 잡을 수 있습니다. 예를 들어 RequestHistory엔터티가 있고 기본적으로 기록 기록 엔터티가 생성되면 변경되지 않습니다. 이러한 객체는 변경 불가능한 클래스로 직접 설계 될 수 있습니다. 요청 오브젝트는 상태 및 시간이 지남에 따라 할당 된 사람 등을 변경할 수 있지만 요청 히스토리는 변경되지 않으므로 변경할 수 있습니다. 예를 들어, 제출 된 상태에서 할당 된 상태로 이동했을 때 지난 주에 작성된 기록 요소가 있었으며이 기록 질내 사정은 절대 변경할 수 없습니다. 따라서 이것은 고전적인 불변의 경우입니다.

b) 설계 선택에 따라 외부 요인

이것은 java.lang.String 예제와 유사합니다. 문자열은 실제로 시간이 지남에 따라 변경 될 수 있지만 의도적으로 캐싱 / 문자열 풀 / 동시성 요인으로 인해 문자열을 변경할 수 없게 만들었습니다. 마찬가지로 캐싱 / 동시성 및 관련 성능이 응용 프로그램에서 중요한 경우 캐싱 / 동시성 등이 개체를 불안정하게 만드는 데 좋은 역할을 할 수 있습니다. 그러나이 결정은 모든 영향을 해결 한 후에 매우 신중하게 결정해야합니다.

불변 개체의 주요 장점은 회전 초 패턴이 적용되지 않는다는 것입니다. 즉, 개체가 수명 시간 동안 어떠한 변화도 일으키지 않으며 코딩 및 유지 관리가 매우 쉬워집니다.


4

현재 내 프로젝트의 일부 데이터 유형을 변경 불가능으로 전환할지 여부에 대해 토론하고 있지만 동료에게이를 정당화해야하며 유형의 데이터가 (거의 거의) 변경 될 수 있습니다. 변경할 수없는 방법입니다 (변경하려는 것을 제외하고 이전 객체의 속성을 복사하여 새 객체를 만듭니다).

프로그램 상태를 최소화하는 것이 매우 유리합니다.

클라이언트 클래스에서 임시 저장을 위해 클래스 중 하나에서 변경 가능한 값 유형을 사용할 것인지 물어보십시오.

그들이 예라고 대답하면 이유를 물어보십시오. 가변 상태는 이와 같은 시나리오에 속하지 않습니다. 그들이 실제로 속한 상태를 만들도록하고 데이터 유형의 상태를 가능한 한 명시 적으로 만드는 것이 훌륭한 이유입니다.


4

대답은 언어에 따라 다소 다릅니다. 코드는 Java처럼 보이며이 문제는 가능한 한 어렵습니다. Java에서 객체는 참조로만 전달 될 수 있으며 복제는 완전히 중단됩니다.

간단한 대답은 없지만, 작은 값의 객체를 변경할 수 없게 만들고 싶습니다. Java는 문자열을 변경 불가능하게 만들었지 만 날짜 및 달력을 변경 가능하게 만들었습니다.

따라서 작은 값의 객체를 변경 불가능하게 만들고 복사 생성자를 구현하십시오. Cloneable에 대한 모든 것을 잊어 버려, 너무 나쁘게 설계되어 쓸모가 없습니다.

값이 큰 객체의 경우 불변으로 만드는 것이 불편한 경우 쉽게 복사 할 수 있습니다.


C 또는 C ++를 작성할 때 스택과 힙 중에서 선택하는 방법과 매우 비슷합니다. :)
Ricket

@Ricket : 그렇게 많은 IMO가 아닙니다. 스택 / 힙은 개체 수명에 따라 다릅니다. C ++에서는 스택에 가변 객체를 갖는 것이 매우 일반적입니다.
kevin cline

1

그리고 나중에 해당 문자열의 값을 변경하지 않아도 될 필요가 없습니다. 분명히 그러한 것들이 사실이라면, 객체는 불변으로 더 잘 설계 될 것입니다.

다소 직관적으로, 나중에 문자열을 변경할 필요가 없다는 것은 객체가 불변인지 아닌지 중요하지 않다는 아주 좋은 주장입니다. 프로그래머는 컴파일러가 강제로 적용하는지 여부에 관계없이 이미이를 불변의 것으로 취급하고 있습니다.

불변성은 일반적으로 아프지 않지만 항상 도움이되는 것은 아닙니다. 객체가 불변성으로 인해 이익을 얻을 수 있는지 쉽게 알 수있는 방법 은 객체를 변경하기 전에 객체를 복사하거나 뮤텍스를 확보해야하는 경우 입니다. 그것이 변하지 않으면 불변성은 실제로 아무것도 사지 않으며 때로는 일을 더 복잡하게 만듭니다.

당신은 유효하지 않은 상태에서 개체를 구성하는 위험에 대한 좋은 점을 가지고 있지만, 그건 정말 불변성과는 별개의 문제입니다. 객체는 구성 후 변경 가능 하고 항상 유효한 상태 일 수 있습니다 .

이 규칙의 예외는 Java가 명명 된 매개 변수 나 기본 매개 변수를 지원하지 않기 때문에 오버로드 된 생성자를 사용하여 유효한 객체를 보장하는 클래스를 설계하기가 때로는 까다로울 수 있다는 것입니다. 두 속성의 경우에는 그다지 중요하지 않지만 코드의 다른 부분에서 유사하지만 더 큰 클래스로 패턴이 자주 나타나는 경우 일관성에 대해서도 언급해야합니다.


나는 변이성에 대한 논의가 불변성과 물체가 안전하게 노출되거나 공유 될 수있는 방법 사이의 관계를 인식하지 못한다는 것이 궁금하다. 불변의 클래스 객체에 대한 참조는 신뢰할 수없는 코드와 안전하게 공유 할 수 있습니다. 참조를 보유한 모든 사람이 오브젝트를 수정하거나이를 수행 할 수있는 코드에 노출시키지 않도록 신뢰할 수있는 경우 변경 가능한 클래스의 인스턴스에 대한 참조를 공유 할 수 있습니다. 변경 될 수있는 클래스 인스턴스에 대한 참조는 일반적으로 전혀 공유되지 않아야합니다. 유니버스의 어느 곳에도 개체에 대한 참조가 하나만있는 경우 ...
supercat

... "identity hashcode"를 쿼리하거나 잠금을 위해 사용하거나 "숨겨진 Object기능"에 액세스 한 경우 , 객체를 직접 변경하는 것은 의미가 새로운 객체에 대한 참조로 참조를 덮어 쓰는 것과 의미가 다릅니다. 동일하지만 지시 된 변화 자바에서 가장 큰 의미 론적 약점 중 하나 인 IMHO는 변수가 무언가에 대한 유일한 비 일시적 참조만을 나타내도록 코드를 표시 할 수단이 없다는 것입니다. 참조는 반환 후 복사본을 유지할 수없는 메서드에만 전달할 수 있어야합니다.
supercat

0

나는 이것에 대해 지나치게 낮은 수준의 견해를 가질 수 있으며 아마도 C와 C ++을 사용하고 있기 때문에 모든 것을 불변으로 만드는 것이 그렇게 간단하지는 않지만 더 많은 것을 작성하기 위해 불변의 데이터 유형을 최적화 세부 사항 으로 본다 부작용이없는 효율적인 기능으로 실행 취소 시스템 및 비파괴 편집과 같은 기능을 매우 쉽게 제공 할 수 있습니다.

예를 들어, 이것은 매우 비쌀 수 있습니다.

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

... Mesh영구적 인 데이터 구조로 설계되지 않고 대신 전체 시나리오를 복사 해야하는 데이터 유형 (일부 시나리오에서는 기가 바이트에이를 수 있음)이 변경되는 경우에도 정점 위치 만 수정하는 위의 시나리오와 같이 그 일부입니다.

그래서 불변성에 도달하고 수정되지 않은 부분을 얕게 복사하고 참조 횟수를 계산할 수 있도록 데이터 구조를 설계하면 위의 기능을 전체 메쉬를 깊게 복사하지 않고도 합리적으로 효율적으로 작성할 수 있습니다. 스레드 안전성, 예외 안전성, 작업 실행 취소, 비파괴 적 적용 등을 극적으로 단순화하는 부작용이없는 기능

제 경우에는 모든 것을 불변으로 만들기에는 너무 비싸고 (적어도 생산성 측면에서는) 비용이 많이 들기 때문에 딥 카피에 너무 비싸서 클래스를 위해 저장합니다. 이러한 클래스는 일반적으로 메쉬 및 이미지와 같은 무거운 데이터 구조이며 일반적으로 변경 가능한 인터페이스를 사용하여 "빌더"객체를 통해 변경 사항을 표현하여 새로운 불변 ​​복사본을 가져옵니다. 그리고 나는 클래스의 중앙 수준에서 불변의 보장을 달성하기 위해 그렇게 많은 노력을 기울이지 않고 부작용없이 클래스를 사용할 수 있도록 도와줍니다. Mesh위에서 불변성 을 만들고자하는 나의 욕망 은 메시를 불변으로 만드는 것이 아니라 메시를 입력하고 큰 메모리와 비용을 대가로 지불하지 않고 메시를 입력하고 새로운 메시를 출력하는 부작용없이 함수를 쉽게 작성할 수 있도록하는 것입니다.

결과적으로 전체 코드베이스에는 4 개의 불변 데이터 유형 만 있으며 모두 무거운 데이터 구조이지만 부작용이없는 함수를 작성하는 데 도움이됩니다. 이 대답은 나처럼 당신이 모든 것을 불변으로 만들기가 쉽지 않은 언어로 일하고 있다면 적용 할 수 있습니다. 이 경우 대량의 함수가 부작용을 피하도록 집중할 수 있습니다.이 시점에서 고가의 전체 사본을 피하기 위해 선택한 데이터 구조를 변경 불가능한 PDS 유형으로 최적화 세부 사항으로 만들 수 있습니다. 한편 다음과 같은 기능이있을 때 :

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

... 그러면 벡터가 완전히 복사되기에 충분히 저렴하기 때문에 벡터를 불변으로 만들려는 유혹이 없습니다. 벡터를 수정되지 않은 부분을 얕게 복사 할 수있는 불변 구조로 변환하여 얻을 수있는 성능 이점은 없습니다. 이러한 비용은 전체 벡터를 복사하는 비용보다 중요합니다.


-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Foo에 두 개의 속성 만있는 경우 두 개의 인수를 사용하는 생성자를 쉽게 작성할 수 있습니다. 그러나 다른 속성을 추가한다고 가정하십시오. 그런 다음 다른 생성자를 추가해야합니다.

public Foo(String a, String b, SomethingElse c)

이 시점에서 여전히 관리가 가능합니다. 그러나 10 개의 속성이 있다면 어떨까요? 인수가 10 개인 생성자를 원하지 않습니다. 빌더 패턴을 사용하여 인스턴스를 구성 할 수 있지만 복잡성이 추가됩니다. 이번에는 "일반 사람들처럼 모든 속성에 세터를 추가하지 않은 이유는 무엇입니까?"라는 생각이들 것입니다.


4
property7이 설정되지 않았을 때 발생하는 NullPointerException보다 10 개의 인수를 가진 생성자가 더 나쁜가요? 각 메소드에서 초기화되지 않은 특성을 확인하기 위해 빌더 패턴이 코드보다 복잡합니까? 객체가 생성되면 한 번 확인하는 것이 좋습니다.
케빈 클라인


1
왜 10 개의 인수를 가진 생성자를 원하지 않습니까? 어느 시점에서 선을 그리나요? 나는 인수가 10 인 생성자가 불변의 이점을 지불하는 작은 가격이라고 생각합니다. 또한 쉼표로 구분 된 10 개의 항목이있는 하나의 라인이 있거나 (선택적으로 더 나은 경우 10 개 라인으로 표시됨) 각 속성을 개별적으로 설정할 때 10 개의 라인이 있습니다.
Ricket

1
@Ricket : 인수를 잘못된 순서로 놓을 위험이 높아지기 때문입니다 . 세터 또는 빌더 패턴을 사용하는 경우에는 거의 없습니다.
Mike Baranczak

4
인수가 10 개인 생성자가 있다면 해당 인수를 자체 클래스 (또는 클래스)로 캡슐화하거나 클래스 디자인을 비판적으로 살펴볼 때가 될 것입니다.
Adam Lear
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.