불변성과 객체 지향 프로그래밍 – 즉 (!

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

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

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

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

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

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

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



답변

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

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

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

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

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

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

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


답변

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

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

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

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


답변

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

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

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

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


답변

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

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

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

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

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

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

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


답변

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

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

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

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

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


답변

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

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

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

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

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


답변

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

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

var brandNewVariable = pureFunction(foo);

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

sameOldObject.changeMe(foo);

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

var brandNewVariable = nonMutatingObject.askMe(foo);

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