불변성과 객체 지향 프로그래밍


43

대부분의 OOP 언어에서 객체는 일반적으로 제한된 예외 세트 (예 : 파이썬의 튜플 및 문자열)로 변경 가능합니다. 대부분의 기능적 언어에서 데이터는 변경할 수 없습니다.

변경 가능한 객체와 변경 불가능한 객체는 모두 장점과 단점의 전체 목록을 제공합니다.

예를 들어 변경 가능하고 변경 불가능한 데이터가있는 (명시 적으로 선언 된) 스칼라와 같은 두 개념을 결혼하려고하는 언어가 있습니다 (잘못되면 스칼라에 대한 나의 지식이 제한적입니다).

내 질문은 : 않습니다 완전한 그것이 OOP의 맥락에서 이해가 created-되면 어떤 객체 - 즉 (! 원문) 불변성은 변이 할 수 있습니까?

그러한 모델의 설계 또는 구현이 있습니까?

기본적으로 (완전한) 불변성과 OOP 반대 또는 직교입니까?

동기 부여 : OOP에서는 일반적 으로 데이터를 조작 하여 기본 정보를 변경 (돌연변이)하여 해당 객체 간의 참조를 유지합니다. 예를 들어 다른 객체를 참조 Person하는 멤버가있는 클래스의 객체. 아버지의 이름을 변경하면 업데이트 할 필요없이 자식 개체에 즉시 표시됩니다. 불변이기 때문에 아버지와 자녀 모두를 위해 새로운 물건을 만들어야합니다. 그러나 공유 객체, 멀티 스레딩, GIL 등으로 커프가 훨씬 적습니다.fatherPerson


2
데이터 액세스를 변경하지 않는 메서드 나 읽기 전용 속성으로 만 개체 액세스 지점을 노출함으로써 OOP 언어로 불변성을 시뮬레이션 할 수 있습니다. 불변성은 일부 기능적 언어 기능이 누락 될 수 있다는 점을 제외하고는 모든 기능적 언어에서와 동일하게 OOP 언어에서 작동합니다.
Robert Harvey

4
변경 가능성은 C # 및 Java와 같은 OOP 언어의 속성이 아니며 변경 불가능하지도 않습니다. 클래스를 작성하는 방식으로 변경 성 또는 불변성을 지정합니다.
Robert Harvey

9
변경 가능성은 객체 지향의 핵심 기능이라고 가정합니다. 그렇지 않습니다. 변경 가능성은 단순히 객체 또는 값의 속성입니다. 객체 지향에는 돌연변이와 관련이 없거나 거의없는 여러 가지 고유 개념 (봉지, 다형성, 상속 등)이 포함되며 이러한 기능의 이점을 여전히 얻을 수 있습니다. 모든 것을 불변으로 만들었더라도.
Robert Harvey

2
@MichaelT 문제는 특정 것을 변경 가능하게 만드는 것이 아니라 모든 것을 변경 불가능하게 만드는 것에 관한 것입니다.

답변:


43

OOP와 불변성은 서로 거의 직교합니다. 그러나 명령형 프로그래밍과 불변성은 아닙니다.

OOP는 두 가지 핵심 기능으로 요약 할 수 있습니다.

  • 캡슐화 : 객체의 내용에 직접 액세스하지 않고이 객체와 특정 인터페이스 ( "메소드")를 통해 통신합니다. 이 인터페이스는 내부 데이터를 숨길 수 있습니다. 기술적으로 이것은 OOP가 아닌 모듈 식 프로그래밍에만 해당됩니다. 정의 된 인터페이스를 통해 데이터에 액세스하는 것은 추상 데이터 유형과 거의 같습니다.

  • 동적 디스패치 : 객체에서 메소드를 호출하면 실행 된 메소드가 런타임에 해결됩니다. 예를 들어 클래스 기반 OOP에서는 인스턴스에서 size메서드를 IList호출 할 수 있지만 LinkedList클래스 의 구현으로 호출이 해결 될 수 있습니다 . 다이나믹 디스패치는 다형성 동작을 허용하는 한 가지 방법입니다.

캡슐화는 변경 없이는 의미가 떨어지지 만 ( 외부 간섭으로 인해 손상 될 수있는 내부 상태 는 없음 ) 모든 것이 불변 인 경우에도 추상화가 더 쉬워지는 경향이 있습니다.

명령형 프로그램은 순차적으로 실행되는 명령문으로 구성됩니다. 명령문은 프로그램 상태 변경과 같은 부작용이 있습니다. 불변성을 사용하면 상태를 변경할 수 없습니다 (물론 상태를 만들 수 있음). 따라서 명령형 프로그래밍은 기본적으로 불변성과 호환되지 않습니다.

이제 OOP는 역사적으로 항상 명령형 프로그래밍 (Simula는 Algol을 기반으로 함)과 연결되어 있으며 모든 주류 OOP 언어는 명령형 뿌리를 가지고 있습니다 (C ++, Java, C #, ...는 모두 C에 뿌리를두고 있습니다). 이것은 OOP 자체가 필수적이거나 변경 가능하다는 것을 의미하지는 않으며, 이러한 언어로 OOP를 구현하면 변경이 가능하다는 것을 의미합니다.


2
특히 두 가지 핵심 기능을 정의 해 주셔서 감사합니다.
Hyperboreus

동적 디스패치는 OOP의 핵심 기능이 아닙니다. 캡슐화도 마찬가지입니다 (이미 인정한대로).
Monica Harming 중지

5
@OrangeDog 예, 보편적으로 허용되는 OOP 정의는 없지만 작업하려면 정의가 필요했습니다. 그래서 나는 그것에 대한 전체 논문을 쓰지 않고 얻을 수있는 진실에 가까운 것을 골랐다. 그러나 동적 디스패치는 다른 패러다임과 OOP의 주요 특징 중 하나라고 생각합니다. OOP처럼 보이지만 실제로 모든 호출을 정적으로 해결하는 것은 실제로 임시 다형성을 사용한 모듈 식 프로그래밍입니다. 객체는 메서드와 데이터 쌍이며 클로저와 동일합니다.
amon

2
"객체는 메소드와 데이터의 쌍"입니다. 불변성과 관련이없는 것만 있으면됩니다.
해밍 모니카

3
@CodeYogi 데이터 숨기기는 가장 일반적인 종류의 캡슐화입니다. 그러나 객체에 의해 데이터가 내부적으로 저장되는 방식 만이 숨겨져 야하는 유일한 구현 세부 사항은 아닙니다. 공용 인터페이스 구현 방법 (예 : 도우미 메서드 사용 여부)을 숨기는 것도 마찬가지로 중요합니다. 이러한 도우미 메서드는 일반적으로 말하면 비공개입니다. 요약하자면, 캡슐화는 원칙이지만 데이터 숨기기는 캡슐화 기술입니다.
amon

25

OOP를 수행하는 경우 사람들이 대부분의 객체를 변경할 수 있다고 가정하는 객체 지향 프로그래머 에게는 문화 가 있지만 OOP에 변경 필요한지 여부와는 별개의 문제입니다 . 또한 그 문화는 사람들이 함수형 프로그래밍에 노출되어 더 많은 불변성을 향해 천천히 변하는 것 같습니다.

스칼라는 객체 지향에 변경이 필요하지 않다는 사실을 잘 보여줍니다. 스칼라 가변성을 지원 하지만 사용을 권장하지 않습니다. 관용 스칼라는 매우 객체 지향적이며 거의 전적으로 불변입니다. 그것은 대부분 Java와의 호환성을 위해 변경 성을 허용하며, 어떤 상황에서는 불변의 객체가 비효율적이거나 복잡하게 작동하기 때문입니다.

예를 들어 스칼라 목록Java 목록을 비교하십시오 . 스칼라의 불변 목록에는 Java의 불변 목록과 동일한 객체 메소드가 모두 포함됩니다. 실제로 Java는 sort같은 연산에 정적 함수를 사용 하고 Scala는와 같은 기능 스타일 메소드를 추가하기 때문에 map. 캡슐화, 상속 및 다형성과 같은 OOP의 모든 특징은 객체 지향 프로그래머에게 친숙한 형태로 제공되며 적절하게 사용됩니다.

당신이 볼 수있는 유일한 차이점은 목록을 변경하면 결과적으로 새로운 객체를 얻는 것입니다. 따라서 종종 가변 객체와는 다른 디자인 패턴을 사용해야하지만 OOP를 완전히 포기할 필요는 없습니다.


17

데이터 액세스를 변경하지 않는 메서드 나 읽기 전용 속성으로 만 개체 액세스 지점을 노출함으로써 OOP 언어로 불변성을 시뮬레이션 할 수 있습니다. 불변성은 일부 기능적 언어 기능이 누락 될 수 있다는 점을 제외하고는 모든 기능적 언어에서와 동일하게 OOP 언어에서 작동합니다.

변경 가능성은 객체 지향의 핵심 기능이라고 생각합니다. 그러나 가변성은 단순히 객체 또는 값의 속성입니다. 객체 지향에는 돌연변이와 관련이 없거나 거의없는 여러 가지 본질적인 개념 (봉지, 다형성, 상속 등)이 포함되며, 모든 것을 불변으로 만들었더라도 이러한 기능의 이점을 얻을 수 있습니다.

모든 기능 언어가 불변성을 요구하는 것은 아닙니다. Clojure에는 유형을 변경할 수있는 특정 주석이 있으며 대부분의 "실용적인"기능 언어에는 변경 가능한 유형을 지정할 수있는 방법이 있습니다.

더 좋은 질문은 "완전한 불변성이 명령형 프로그래밍 에서 의미가 있습니까?"입니다. 그 질문에 대한 명백한 대답은 '아니오'라고 말하고 싶습니다. 명령형 프로그래밍에서 완전한 불변성을 달성하려면 for재귀에 찬성하여 루프 와 같은 것을 반복해야합니다 (루프 변수를 변경해야하기 때문에) 이제 본질적으로 기능 방식으로 프로그래밍하고 있습니다.


감사합니다. 마지막 단락을 좀 더 정교하게 설명해 주시겠습니까 ( "분명한"내용은 약간 주관적 일 수 있습니다).
Hyperboreus

이미 ..
Robert Harvey

1
@Hyperboreus 다형성을 달성하는 방법 에는 여러 가지 가 있습니다 . 동적 디스패치, 정적 임시 다형성 (일명 함수 오버로딩) 및 파라 메트릭 다형성 (일명 일반)을 사용하여 서브 타이핑하는 것이 가장 일반적인 방법이며 모든 방법에는 강점과 약점이 있습니다. 현대의 OOP 언어는이 세 가지 방식을 모두 결합한 반면 Haskell은 주로 파라 메트릭 다형성 및 임시 다형성에 의존합니다.
amon

3
@RobertHarvey 루프를 반복해야하기 때문에 변경이 필요하다고 말합니다 (그렇지 않으면 재귀를 사용해야합니다). 2 년 전에 Haskell을 사용하기 전에 가변 변수도 필요하다고 생각했습니다. "루프"하는 다른 방법이 있습니다 (지도, 접기, 필터 등). 테이블에서 루프를 해제하면 왜 가변 변수가 필요한가?
cimmanon

1
@RobertHarvey 그러나 이것이 바로 프로그래밍 언어의 요점입니다. 노출되는 것이 아니라 노출되는 것입니다. 후자는 응용 프로그램 개발자가 아닌 컴파일러 또는 인터프리터의 책임입니다. 그렇지 않으면 어셈블러로 돌아갑니다.
Hyperboreus

5

객체가 값 또는 엔터티를 캡슐화하는 것으로 분류하는 것이 유용하며, 무언가가 값이면 참조를 보유한 코드는 코드 자체가 시작하지 않은 방식으로 상태가 변경되지 않아야한다는 점이 다릅니다. 대조적으로, 엔티티에 대한 참조를 보유한 코드는 참조 보유자의 통제 범위를 넘어서서 변경 될 것으로 예상 할 수 있습니다.

변경 가능 또는 변경 불가능한 유형의 객체를 사용하여 캡슐화 값을 사용할 수 있지만 다음 조건 중 하나 이상이 적용되는 경우에만 객체가 값으로 작동 할 수 있습니다.

  1. 객체에 대한 어떤 참조도 그 안에 캡슐화 된 상태를 바꿀 수있는 어떤 것에 노출되지 않을 것입니다.

  2. 객체에 대한 하나 이상의 참조를 보유한 사람은 모든 기존 참조가 사용될 수있는 모든 용도를 알고 있습니다.

불변 유형의 모든 인스턴스는 자동으로 첫 번째 요구 사항을 충족하므로 값으로 쉽게 사용할 수 있습니다. 반대로 가변 유형을 사용할 때 두 가지 요구 사항을 모두 충족시키는 것은 훨씬 어렵습니다. 불변 유형에 대한 참조는 그 안에 캡슐화 된 상태를 캡슐화하는 수단으로서 자유롭게 전달 될 수있는 반면에, 불변 유형에 저장된 상태를 통과하는 것은 불변 래퍼 객체를 구성하거나 개인이 보유한 객체에 의해 캡슐화 된 상태를 다른 객체에 복사하는 것을 필요로한다 데이터 수신자에 의해 제공되거나 데이터 수신자를 위해 구성됩니다.

불변 유형은 값을 전달하는 데 매우 효과적이며 종종 값을 조작하는 데 약간 사용 가능합니다. 그러나 엔티티를 처리하는 데 그렇게 좋지 않습니다. 순수한 불변 유형을 가진 시스템에서 엔티티에 대해 가장 가까운 것은 시스템의 상태에 따라 그 일부의 속성을보고하거나 다음과 같은 새로운 시스템 상태 인스턴스를 생성하는 함수입니다. 일부 선택 가능한 방식이 다른 특정 부분을 제외하고 하나를 공급했다. 또한 엔터티의 목적이 일부 코드를 실제 세계에 존재하는 코드와 인터페이스하는 것이라면 엔터티가 변경 가능한 상태의 노출을 피하는 것이 불가능할 수 있습니다.

예를 들어, TCP 연결을 통해 일부 데이터를 수신하는 경우 이전 "세계 상태"에 대한 참조에 영향을주지 않고 버퍼에 해당 데이터를 포함하는 새로운 "세계 상태"객체를 생성 할 수 있지만 마지막 데이터 배치를 포함하지 않는 월드 상태는 결함이 있으며 더 이상 실제 TCP 소켓의 상태와 일치하지 않으므로 사용해서는 안됩니다.


4

C #에서 일부 유형은 문자열과 같이 변경할 수 없습니다.

이것은 또한 선택이 강력하게 고려되었음을 암시하는 것으로 보인다.

그 유형을 수십만 번 수정 해야하는 경우 불변 유형을 사용하는 것이 실제로 성능이 뛰어납니다. 이것이이 경우 StringBuilder클래스 대신 클래스 를 사용하도록 제안 된 이유 string입니다.

나는 프로파일 러로 실험을 했고 불변 유형을 사용하는 것은 실제로 더 많은 CPU와 RAM을 요구합니다.

4000 자의 문자열에서 하나의 문자 만 수정하려면 RAM의 다른 영역에있는 모든 문자를 복사해야한다고 생각하면 직관적입니다.


6
불변 데이터를 자주 수정하는 것은 반복 된 string연결 과 같이 치명적으로 느릴 필요가 없습니다 . 사실상 모든 종류의 데이터 / 사용 사례의 경우 효율적인 영구 구조가 개발 될 수 있습니다 (종종 이미 개발 된 경우도 있음). 상수 요인이 때때로 더 악화 되더라도 대부분의 성능은 거의 같습니다.

@delnan 나는 또한 대답의 마지막 단락이 (im) 변경 가능성보다는 구현 세부 사항에 관한 것이라고 생각합니다.
Hyperboreus

@ Hyperboreus : 그 부분을 삭제해야한다고 생각합니까? 그러나 불변 인 경우 어떻게 문자열을 변경할 수 있습니까? 내 생각에는 .. 내 생각에, 내가 틀릴 수 있다는 것은 그것이 객체가 불변이 아닌 주된 이유 일 수있다.
Revious

1
@Revious 절대로. 그대로두면 토론과 더 흥미로운 의견과 관점이 생깁니다.
Hyperboreus

1
@Revious 네, string(전통적인 표현) 을 변경하는 것만 큼 느리지는 않지만 읽기는 느릴 것 입니다. 1000을 수정 한 후 "문자열"(내가 말하고있는 표현에서)은 새로 만든 문자열 (모듈로 내용)과 같습니다. 유용하거나 널리 사용되는 영구 데이터 구조는 X 작업 후 품질이 저하되지 않습니다. 메모리 조각화는 심각한 문제가 아닙니다 (많은 할당이있을 것입니다. 그러나 현대 가비지 수집기 에서는 조각화 가 문제가되지 않습니다 )

0

OOP 또는 그 문제에 대한 대부분의 다른 패러다임에서 모든 것에 대한 완전한 불변성은 큰 의미가 없습니다.

모든 유용한 프로그램에는 부작용이 있습니다.

아무것도 변경하지 않는 프로그램은 가치가 없습니다. 효과가 동일하므로 실행하지 않았을 수도 있습니다.

아무 것도 바꾸지 않는다고 생각하고 어떻게 든받은 숫자 목록을 요약하는 경우에도 표준 출력으로 인쇄하든 파일에 쓰든 결과와 관련하여 무언가를 수행해야한다고 생각하십시오. 또는 어디서나. 버퍼를 변경하고 시스템 상태를 변경하는 작업이 포함됩니다.

변경이 필요한 부분으로의 변경 을 제한 하는 것은 많은 의미 가 있습니다. 그러나 절대로 변경할 필요가 없다면할만한 일을하지 않습니다.


4
순수한 기능적 언어를 다루지 않았기 때문에 귀하의 답변이 질문과 어떻게 관련되는지 알 수 없습니다. 얼랭을 예로 들어 보자 : 불변 데이터, 파괴적인 할당, 부작용에 대한 애매함. 또한 상태에서 작동하는 함수와 달리 상태가 함수를 통해 "흐르도록"기능 언어로 상태가 있습니다. 상태는 변경되지만 변경되지는 않지만 향후 상태가 현재 상태를 대체합니다. 불변성은 메모리 버퍼의 변경 여부에 관한 것이 아니라 이러한 돌연변이가 외부에서 보이는지에 관한 것입니다.
Hyperboreus

미래 상태는 현재 상태를 어떻게 대체 합니까? OO 프로그램에서이 상태는 어딘가에있는 객체의 속성입니다. 상태를 바꾸려면 객체를 변경해야합니다 (또는 다른 객체로 바꾸려면 객체를 참조하는 객체를 변경해야합니다 (또는 객체를 다른 것으로 교체해야합니다). 모든 동작이 완전히 새로운 응용 프로그램을 생성하는 일종의 모나 딕 해킹이 발생할 수 있습니다. 그럼에도 불구하고 프로그램의 현재 상태는 어딘가에 기록되어야합니다.
cHao

7
-1. 이것은 올바르지 않습니다. 부작용과 돌연변이를 혼동하고 있으며, 종종 기능적 언어로 동일하게 취급되지만 서로 다릅니다. 모든 유용한 프로그램에는 부작용이 있습니다. 모든 유용한 프로그램에 돌연변이가있는 것은 아닙니다.
Michael Shaw

@Michael : OOP와 관련하여 돌연변이와 부작용이 너무 얽혀 현실적으로 분리 될 수 없습니다. 돌연변이가 없으면 대량의 해커가 없으면 부작용을 가질 수 없습니다.
cHao


-2

OOP의 정의가 메시지 전달 스타일을 사용하는지 여부에 달려 있다고 생각합니다.

순수한 함수는 새로운 변수에 저장할 수있는 값을 반환하므로 아무것도 변경하지 않아도됩니다.

var brandNewVariable = pureFunction(foo);

메시지 전달 스타일을 사용하면 새 변수에 어떤 새 데이터를 저장해야하는지 묻는 대신 새 데이터를 저장하도록 개체에 지시합니다.

sameOldObject.changeMe(foo);

메소드를 외부 대신에 내부에 존재하는 순수한 함수로 만들어서 객체를 가질 수 있고 변형하지 않을 수 있습니다.

var brandNewVariable = nonMutatingObject.askMe(foo);

그러나 메시지 전달 스타일과 변경 불가능한 객체를 혼합 할 수는 없습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.