[oop] 가변 대 불변 개체

변경 가능한 객체와 불변의 객체 주위에 머리를 갖으려고합니다. 변경 가능한 객체를 사용하면 프레스에서 많은 나쁜 프레스 (예 : 메소드에서 문자열 배열 반환)가 발생하지만 부정적인 영향이 무엇인지 이해하는 데 어려움을 겪고 있습니다. 가변 객체 사용에 대한 모범 사례는 무엇입니까? 가능할 때마다 피해야합니까?



답변

글쎄, 이것에는 몇 가지 측면이 있습니다.

  1. 참조 ID가없는 가변 객체는 이상한 시간에 버그를 일으킬 수 있습니다. 예를 들어, Person값 기반 equals메소드가 있는 Bean을 고려하십시오 .

    Map<Person, String> map = ...
    Person p = new Person();
    map.put(p, "Hey, there!");
    
    p.setName("Daniel");
    map.get(p);       // => null
    

    Person그 때문에 키로서 사용하는 경우 인스턴스는 맵에 “손실”됩니다 hashCode와 평등이 변경 가능한 값에 근거했다. 이러한 값은 맵 외부에서 변경되었으며 모든 해싱은 더 이상 사용되지 않습니다. 이론가들은이 시점에서 하프를 좋아하지만 실제로는 그다지 큰 문제가 아니라고 생각했습니다.

  2. 또 다른 측면은 코드의 논리적 “합리성”입니다. 가독성부터 흐름까지 모든 것을 포괄하는 정의하기 어려운 용어입니다. 일반적으로 코드 조각을보고 코드의 기능을 쉽게 이해할 수 있어야합니다. 그러나 그보다 더 중요한 것은 자신이 올바르게 작동한다는 것을 스스로에게 확신시킬 수 있어야 합니다 . 서로 다른 코드 “도메인”에서 개체가 독립적으로 변경 될 수있는 경우, 위치와 이유 ( ” 원거리에서의 무시 무시한 동작 “)를 추적하기가 어려운 경우가 있습니다. 이것은 예시하기가 더 어려운 개념이지만, 더 크고 복잡한 아키텍처에서 종종 직면하는 것입니다.

  3. 마지막으로, 변경 가능한 객체는 동시 상황에서 킬러 입니다. 별도의 스레드에서 변경 가능한 객체에 액세스 할 때마다 잠금을 처리해야합니다. 이 처리량 감소하고 코드를 만드는 극적으로 더 어려워 유지 할 수 있습니다. 충분히 복잡한 시스템은이 문제를 일정 비율 이상으로 날려 유지하기가 거의 불가능 해집니다 (동시성 전문가 인 경우에도).

불변 개체 (특히 불변 컬렉션)는 이러한 모든 문제를 피합니다. 코드의 작동 방식에 대해 염두에두면 코드는 읽기 쉽고 유지 관리하기 쉽고 이상한 방식으로 예측할 수없는 방식으로 개발됩니다. 불변의 객체는 쉽게 조롱 할 수있을뿐만 아니라 실행하려는 코드 패턴으로 인해 테스트하기도 더 쉽습니다. 요컨대, 그들은 좋은 연습입니다!

그 말로, 나는이 문제에 열심이 아닙니다. 모든 것이 불변 인 경우 일부 문제는 잘 모델링되지 않습니다. 그러나 나는 당신이 이것을 가능한 견해로 만드는 언어를 사용한다고 가정 할 때 가능한 한 많은 방향으로 코드를 푸시하려고 노력해야한다고 생각합니다 (C / C ++은 Java와 마찬가지로 이것을 어렵게 만듭니다) . 요컨대, 장점은 문제에 다소 달려 있지만 불변성을 선호하는 경향이 있습니다.


답변

불변 객체와 불변 콜렉션

불변성 객체와 불변성 객체에 대한 토론에서 더 좋은 점 중 하나는 불변성의 개념을 컬렉션으로 확장 할 수 있다는 것입니다. 불변 객체는 종종 하나의 논리적 데이터 구조 (예 : 불변 문자열)를 나타내는 객체입니다. 불변 객체에 대한 참조가 있으면 객체의 내용이 변경되지 않습니다.

불변 컬렉션은 절대 변경되지 않는 컬렉션입니다.

변경 가능한 컬렉션에서 작업을 수행하면 컬렉션을 적절히 변경하고 컬렉션을 참조하는 모든 엔터티가 변경 사항을 볼 수 있습니다.

변경 불가능한 콜렉션에서 조작을 수행하면 변경 사항을 반영하는 참조가 새 콜렉션으로 리턴됩니다. 이전 버전의 컬렉션에 대한 참조가있는 모든 엔터티에는 변경 내용이 표시되지 않습니다.

영리한 구현은 불변성을 제공하기 위해 전체 컬렉션을 복사 (복제) 할 필요는 없습니다. 가장 간단한 예는 단일 연결 목록 및 푸시 / 팝 작업으로 구현 된 스택입니다. 새 컬렉션의 이전 컬렉션에서 모든 노드를 재사용하여 푸시에 단일 노드 만 추가하고 팝에 대한 노드는 복제하지 않을 수 있습니다. 반면에 단독으로 연결된 목록의 push_tail 작업은 그렇게 간단하지 않거나 효율적이지 않습니다.

불변 변수와 가변 변수 / 참조

일부 기능적 언어는 객체 참조 자체에 대한 불변성의 개념을 취하여 단일 참조 할당 만 허용합니다.

  • Erlang에서는 모든 “변수”에 해당됩니다. 객체를 참조에 한 번만 할당 할 수 있습니다. 컬렉션에서 작업 할 경우 새 컬렉션을 이전 참조 (변수 이름)에 다시 할당 할 수 없습니다.
  • 스칼라는 또한 모든 참조가 var 또는 val 으로 선언되고 , val 은 단일 지정이고 기능적 스타일을 홍보하지만보다 C 형 또는 Java 형 프로그램 구조를 허용하는 vars 로 언어에이를 빌드합니다 .
  • var / val 선언이 필요하지만 많은 전통적인 언어는 final in java 및 const const 와 같은 선택적 수정자를 사용합니다 .

손쉬운 개발 및 성능

거의 항상 불변의 객체를 사용하는 이유는 부작용이없는 프로그래밍과 코드에 대한 간단한 추론을 촉진하기위한 것입니다 (특히 동시 / 병렬 환경에서). 객체가 불변 인 경우 다른 엔터티가 기본 데이터를 변경하는 것에 대해 걱정할 필요가 없습니다.

주요 단점은 성능입니다. 다음은 장난감 문제에서 변경 불가능한 객체와 변경 불가능한 객체를 비교하여 Java 에서 수행 한 간단한 테스트 에 대한 글입니다.

성능 문제는 많은 응용 프로그램에서 문제가 될 수 있지만 전부는 아닙니다. 따라서 Python의 Numpy Array 클래스와 같은 많은 큰 숫자 패키지가 큰 배열의 내부 업데이트를 허용합니다. 이는 대규모 행렬 및 벡터 연산을 사용하는 응용 분야에 중요합니다. 이처럼 큰 데이터 병렬 및 계산 집약적 인 문제는 제자리에서 작동하여 속도를 크게 향상시킵니다.


답변

이 블로그 게시물을 확인하십시오 : http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html . 불변의 객체가 변경 가능한 것보다 나은 이유를 설명합니다. 한마디로 :

  • 불변 개체는 구성, 테스트 및 사용이 더 간단합니다.
  • 진정한 불변의 객체는 항상 스레드 안전
  • 그들은 시간적 결합을 피하는 데 도움이됩니다
  • 그들의 사용법은 부작용이 없습니다 (방어 사본 없음)
  • 신원 변경 가능성 문제를 피할 수 있습니다
  • 그들은 항상 실패 원 자성을 가지고
  • 그들은 캐시하기가 훨씬 쉽다

답변

불변 개체는 매우 강력한 개념입니다. 모든 클라이언트에 대해 객체 / 변수를 일관성있게 유지해야하는 많은 부담을 제거합니다.

값 의미와 함께 주로 사용되는 CPoint 클래스와 같은 저수준의 다형성이 아닌 객체에 사용할 수 있습니다.

또는 객체 시맨틱과 함께 독점적으로 사용되는 수학 함수를 나타내는 IFunction과 같은 높은 수준의 다형성 인터페이스에 사용할 수 있습니다.

가장 큰 장점 : 불변성 + 객체 의미론 + 스마트 포인터는 객체 소유권을 문제가 아닌 것으로 만들며, 객체의 모든 클라이언트는 기본적으로 자체 사본을 가지고 있습니다. 암시 적으로 이것은 동시성이있을 때 결정적 동작을 의미합니다.

단점 : 많은 양의 데이터가 포함 된 객체와 함께 사용하면 메모리 소비가 문제가 될 수 있습니다. 이것에 대한 해결책은 객체에 대한 작업을 상징적으로 유지하고 게으른 평가를 수행하는 것입니다. 그러나 이는 인터페이스가 심볼 연산을 수용하도록 설계되지 않은 경우 성능 계산에 부정적인 영향을 줄 수있는 일련의 심볼 계산으로 이어질 수 있습니다. 이 경우 확실히 피해야 할 것은 메소드에서 엄청난 양의 메모리를 반환하는 것입니다. 체인화 된 심볼 작업과 함께 이로 인해 엄청난 메모리 소비와 성능 저하가 발생할 수 있습니다.

따라서 불변의 객체는 객체 지향 디자인에 대한 나의 주요 사고 방식이지만 분명히 교리가 아닙니다. 객체의 클라이언트에게는 많은 문제를 해결하지만 특히 구현 자에게는 많은 문제가 발생합니다.


답변

당신이 말하는 언어를 지정해야합니다. C 또는 C ++와 같은 저수준 언어의 경우 공간을 절약하고 메모리 변동을 줄이기 위해 가변 객체를 사용하는 것을 선호합니다. 더 높은 수준의 언어에서, 불변의 객체는 “먼 거리에서 멍청한 행동”이 없기 때문에 코드의 동작 (특히 멀티 스레드 코드)에 대해 추론하기가 더 쉽습니다.


답변

변경 가능한 객체는 단순히 만들거나 인스턴스화 한 후에 수정할 수있는 수정 가능한 객체와 수정할 수없는 변경 불가능한 객체입니다 ( 대상 의 Wikipedia 페이지 참조 ). 프로그래밍 언어에서 이에 대한 예는 Pythons 목록과 튜플입니다. 튜플은 할 수 없지만 목록을 수정할 수 있습니다 (예 : 새 항목을 만든 후 추가 할 수 있음).

나는 모든 상황에서 어느 것이 더 나은지에 대한 명확한 대답이 있다고 생각하지 않습니다. 둘 다 자리가 있습니다.


답변

클래스 유형이 변경 가능한 경우 해당 클래스 유형의 변수는 여러 가지 다른 의미를 가질 수 있습니다. 예를 들어, 오브젝트 foo에 field int[] arr가 있고 int[3]숫자 {5, 7, 9} 를 보유한 것에 대한 참조를 보유 한다고 가정하십시오 . 필드 유형이 알려져 있지만 나타낼 수있는 최소한 네 가지가 있습니다.

  • 잠재적으로 공유 참조하는 경우, 그 소유자는 값을 캡슐화 것만 신경 모두 5, 7, 9 foo욕구가 arr다른 값을 캡슐화하기 위해서는, 원하는 값을 포함하는 다른 배열을 교체한다. 의 사본을 만들려면 {1,2,3} 값을 보유한 foo참조 arr또는 새로운 배열 중 더 편리한 사본을 사본에 제공 할 수 있습니다 .

  • 5, 7 및 9 값을 캡슐화하는 배열에 대한 유니버스의 유일한 참조는 현재 값 5, 7, 9를 보유하는 세 개의 저장 위치 세트입니다. 만약 foo이 숫자 5, 8, 9를 캡슐화하고자, 그것도 그 배열의 두 번째 항목을 변경하거나 숫자 5, 8 채 새로운 배열을 생성하고, (9) 및 이전을 포기할 수있다. 의 복사본을 만들려는 경우 유니버스의 모든 위치에서 해당 배열에 대한 유일한 참조로 남아 foo있으려면 복사본 arr에서 새 배열에 대한 참조로 바꿔야 foo.arr합니다.

  • 어떤 이유로 인해 노출 된 다른 객체 가 소유 한 배열에 대한 참조 foo(예 : foo데이터를 저장 하려는 경우) 이 시나리오에서는 arr배열의 내용을 캡슐화하지 않고 ID를 캡슐화합니다 . 교체 때문에 arr완전히 그 의미를 변경하는 새로운 배열에 참조하여이 복사의 foo동일한 어레이에 대한 참조를 보유한다.

  • 배열이 foo단독 소유자이지만 어떤 이유로 든 다른 객체에 의해 참조가 유지되는 배열에 대한 참조 (예 : 다른 개체가 데이터를 저장하기를 원함) (이전 사례의 뒤집 음). 이 시나리오에서는 arr배열의 ID와 해당 내용을 모두 캡슐화합니다. 교체 arr완전히 그 의미를 변경하는 새로운 배열에 대한 참조 만의 클론을 갖는 arr참조가 foo.arr가정 위반 foo단독 소유자입니다. 따라서 복사 할 방법이 없습니다 foo.

이론 상으로는 int[]잘 정의 된 단순하고 잘 정의 된 유형이어야하지만 네 가지 의미가 매우 다릅니다. 대조적으로, 불변 개체 (예를 들어 String)에 대한 언급 은 일반적으로 하나의 의미만을 갖는다. 불변 개체의 “파워”의 대부분은 그 사실에서 비롯됩니다.