불변 클래스가 필요한 시나리오를 얻을 수 없습니다.
그러한 요구 사항에 직면 한 적이 있습니까? 아니면 우리가이 패턴을 사용해야 할 실제 예를 들어 주시겠습니까?
불변 클래스가 필요한 시나리오를 얻을 수 없습니다.
그러한 요구 사항에 직면 한 적이 있습니까? 아니면 우리가이 패턴을 사용해야 할 실제 예를 들어 주시겠습니까?
답변:
다른 답변은 불변성이 좋은 이유를 설명하는 데 너무 집중된 것 같습니다. 그것은 매우 좋으며 가능할 때마다 사용합니다. 그러나 그것은 귀하의 질문이 아닙니다 . 나는 당신이 필요한 답과 예제를 얻고 있는지 확인하기 위해 당신의 질문을 한 점씩 가져갈 것입니다.
불변 클래스가 필요한 시나리오를 얻을 수 없습니다.
여기서 "Need"는 상대적인 용어입니다. 불변 클래스는 다른 패러다임 / 패턴 / 도구와 마찬가지로 소프트웨어를 더 쉽게 구성 할 수있는 디자인 패턴입니다. 마찬가지로, OO 패러다임이 나오기 전에 많은 코드가 작성되었지만, OO 를 "필요로 하는 " 프로그래머 중에는 저를 포함 시키십시오 . OO와 같은 불변 클래스는 꼭 필요한 것은 아니지만 필요한 것처럼 행동 할 것입니다.
그러한 요구 사항에 직면 한 적이 있습니까?
올바른 관점으로 문제 도메인의 개체를 보지 않으면 불변 개체에 대한 요구 사항이 표시되지 않을 수 있습니다 . 언제 유용하게 사용할지 잘 모르는 경우 문제 도메인에는 불변 클래스가 필요 하지 않다고 생각하기 쉽습니다 .
나는 종종 내 문제 영역에서 주어진 객체를 값 또는 고정 인스턴스 로 생각하는 불변 클래스를 사용 합니다 . 이 개념은 때때로 관점이나 관점에 의존하지만 이상적으로는 좋은 후보 개체를 식별하기 위해 올바른 관점으로 쉽게 전환 할 수 있습니다.
불변 클래스에 대해 생각하는 방법에 대한 좋은 감각을 개발하기 위해 다양한 책 / 온라인 기사를 읽어 보면 불변 객체가 정말로 유용한 곳 (엄격히 필요한 것은 아니지만)을 더 잘 이해할 수 있습니다. 시작하기에 좋은 기사 중 하나는 Java 이론과 실습입니다. 돌연변이를 원하십니까?
나는 관점이 의미하는 바를 명확히하기 위해 어떻게 다른 관점 (변경 가능 vs 불변)에서 객체를 볼 수 있는지에 대한 몇 가지 예를 아래에 제공하려고합니다.
...이 패턴을 사용해야하는 실제 예를 들어 주시겠습니까?
실제 예제를 요청 했으므로 몇 가지를 제공하지만 먼저 몇 가지 고전적인 예제부터 시작하겠습니다.
클래식 값 개체
문자열과 정수는 종종 값으로 간주됩니다. 따라서 String 클래스와 Integer 래퍼 클래스 (다른 래퍼 클래스뿐만 아니라)가 Java에서 변경 불가능하다는 사실은 놀라운 일이 아닙니다. 색상은 일반적으로 값으로 간주되므로 변경할 수없는 Color 클래스입니다.
반례
반대로 자동차는 일반적으로 가치 대상으로 간주되지 않습니다. 자동차를 모델링한다는 것은 일반적으로 상태 (주행 거리계, 속도, 연료 수준 등)가 변경되는 클래스를 만드는 것을 의미합니다. 그러나 자동차가 가치 객체가 될 수있는 일부 도메인이 있습니다. 예를 들어, 자동차 (또는 특히 자동차 모델)는 특정 차량에 적합한 엔진 오일을 찾기위한 앱에서 값 개체로 간주 될 수 있습니다.
카드 놀이
카드 놀이 프로그램을 작성한 적이 있습니까? 내가했다. 나는 트럼프 패를 수트와 계급이 변경 가능한 개체로 표현할 수 있었다. 드로우-포커 핸드는 5 개의 고정 된 경우가 될 수 있습니다. 내 손에있는 5 번째 카드를 교체하는 것은 그 슈트와 랭크 ivars를 변경하여 5 번째 플레잉 카드 인스턴스를 새 카드로 변경하는 것을 의미합니다.
그러나 나는 트럼프 카드를 일단 만들어지면 변하지 않는 고정 된 수트와 랭크를 가진 불변의 개체로 생각하는 경향이 있습니다. 제 드로우 포커 핸드는 5 개의 인스턴스가 될 것이고 제 핸드의 카드를 교체하는 것은 그 인스턴스 중 하나를 버리고 제 손에 새로운 무작위 인스턴스를 추가하는 것을 포함합니다.
지도 투영
마지막 예는지도가 다양한 투영으로 표시 될 수있는지도 코드를 작업했을 때입니다. 입니다. 원래 코드는지도에서 고정되었지만 변경 가능한 투영 인스턴스 (위의 변경 가능한 재생 카드처럼)를 사용했습니다. 지도 투영을 변경하면지도의 투영 인스턴스의 ivar (투영 유형, 중심점, 확대 / 축소 등)가 변경되었습니다.
하지만 투영을 불변의 값이나 고정 된 인스턴스로 생각하면 디자인이 더 간단하다고 느꼈습니다. 지도 투영을 변경한다는 것은지도의 고정 투영 인스턴스를 변경하는 대신지도가 다른 투영 인스턴스를 참조하도록하는 것을 의미했습니다. 이는 또한 MERCATOR_WORLD_VIEW.
변경 불가능한 클래스는 일반적으로 올바르게 설계, 구현 및 사용하기가 훨씬 간단합니다 . 예는 String입니다.의 구현은 대부분 불변성으로 인해 C ++ 의 구현 java.lang.String보다 훨씬 간단 std::string합니다.
불변성이 특히 큰 차이를 만드는 특정 영역 중 하나는 동시성입니다. 불변 객체는 여러 스레드간에 안전하게 공유 할 수있는 반면, 변경 가능한 객체는 신중한 설계 및 구현을 통해 스레드로부터 안전하게 만들어야합니다. 일반적으로 이것은 사소한 작업과 거리가 멀습니다.
업데이트 : Effective Java 2nd Edition 은이 문제를 자세히 다루고 있습니다. 항목 15 : 변경 가능성 최소화를 참조하십시오 .
다음 관련 게시물도 참조하십시오.
Joshua Bloch의 Effective Java는 불변 클래스를 작성하는 몇 가지 이유를 설명합니다.
일반적으로 심각한 성능 문제가 발생하지 않는 한 객체를 변경 불가능하게 만드는 것이 좋습니다. 이러한 상황에서 변경 가능한 빌더 객체는 StringBuilder와 같은 변경 불가능한 객체를 빌드하는 데 사용될 수 있습니다.
해시 맵은 전형적인 예입니다. 지도의 키는 변경 불가능해야합니다. 키가 변경 불가능하지 않은 경우 hashCode ()가 새 값을 생성하도록 키의 값을 변경하면 이제 맵이 손상됩니다 (이제 키가 해시 테이블의 잘못된 위치에 있음).
Java는 사실상 하나의 모든 참조입니다. 때때로 인스턴스는 여러 번 참조됩니다. 이러한 인스턴스를 변경하면 모든 참조에 반영됩니다. 때로는 견고성과 스레드 안전성을 향상시키기 위해 이것을 원하지 않을 수도 있습니다. 그런 다음 변경 불가능한 클래스가 유용하므로 새 인스턴스 를 만들고 현재 참조에 다시 할당해야합니다. 이렇게하면 다른 참조의 원래 인스턴스가 그대로 유지됩니다.
Java String가 변경 가능 하다면 어떻게 보일지 상상해보십시오 .
Date및 Calendar변경할 수 있었다. 오, 잠깐, 그들은, OH SH
String에서는 변경 가능합니다! (힌트 : 일부 이전 JRockit 버전). string.trim ()을 호출하면 원래 문자열이 잘
우리는 그 자체로 불변 클래스 가 필요 하지 않지만 , 특히 여러 스레드가 관련된 경우 일부 프로그래밍 작업을 더 쉽게 만들 수 있습니다. 변경 불가능한 객체에 액세스하기 위해 잠금을 수행 할 필요가 없으며 이러한 객체에 대해 이미 설정 한 사실은 향후에도 계속 적용됩니다.
극단적 인 경우를 보자 : 정수 상수. "x = x + 1"과 같은 문을 작성하면 프로그램의 다른 곳에서 어떤 일이 발생하더라도 숫자 "1"이 어떻게 든 2가되지 않을 것이라고 100 % 확신하고 싶습니다.
이제 정수 상수는 클래스가 아니지만 개념은 동일합니다. 내가 다음과 같이 쓴다고 가정하자.
String customerId=getCustomerId();
String customerName=getCustomerName(customerId);
String customerBalance=getCustomerBalance(customerid);
충분히 간단 해 보입니다. 그러나 Strings가 변경 불가능하지 않은 경우 getCustomerName이 customerId를 변경할 수있는 가능성을 고려해야하므로 getCustomerBalance를 호출 할 때 다른 고객에 대한 잔액을 얻습니다. 이제 "왜 누군가 getCustomerName 함수를 작성하여 ID를 변경하겠습니까? 그것은 말이되지 않을 것입니다."라고 말할 수 있습니다. 그러나 그것이 바로 당신이 곤경에 처할 수있는 곳입니다. 위의 코드를 작성하는 사람은 함수가 매개 변수를 변경하지 않는다는 것을 명백히 받아 들일 수 있습니다. 그런 다음 고객이 동일한 이름으로 여러 계정을 가지고있는 경우를 처리하기 위해 해당 기능의 다른 사용을 수정해야하는 누군가가 나타납니다. 그리고 그는 "이미 이름을 찾고있는이 편리한 getCustomer 이름 함수가 있습니다.
불변성은 단순히 특정 클래스의 객체가 상수임을 의미하며이를 상수로 취급 할 수 있습니다.
(물론 사용자는 변수에 다른 "상수 객체"를 할당 할 수 있습니다. 누군가는 String s = "hello"를 작성하고 나중에 s = "goodbye"를 작성할 수 있습니다. 변수를 최종화하지 않는 한 확신 할 수 없습니다. 내 자신의 코드 블록 내에서 변경되지 않습니다. 정수 상수처럼 "1"은 항상 같은 숫자이지만 "x = 1"은 "x = 2"를 써도 변경되지 않습니다. 변경 불가능한 객체에 대한 핸들이 있으면 전달하는 함수가 나에게 변경할 수 없거나 두 개의 사본을 만들면 하나의 사본을 보유하는 변수를 변경해도 변경되지 않는다고 확신 할 수 있습니다. 기타 등등.
불변성에는 여러 가지 이유가 있습니다.
String수업.따라서 네트워크 서비스를 통해 데이터를 보내고 싶을 때 전송 한 것과 똑같은 결과를 얻을 것이라는 보장 을 원하면 불변으로 설정하십시오.
finalJava에서 플래그가 지정된 모든 클래스 가 불변하는 것은 아니며 모든 불변 클래스가 플래그가 지정되는 것은 아닙니다 final.
나는 이것을 다른 관점에서 공격 할 것입니다. 불변의 객체는 코드를 읽을 때 삶을 더 쉽게 만듭니다.
변경 가능한 객체가 있으면 즉각적인 범위 밖에서 사용 된 적이 있다면 그 값이 무엇인지 확신 할 수 없습니다. MyMutableObject메서드의 지역 변수를 만들고 값으로 채운 다음 5 개의 다른 메서드에 전달 한다고 가정 해 보겠습니다 . 이러한 메서드 중 하나는 내 개체의 상태를 변경할 수 있으므로 다음 두 가지 중 하나가 발생해야합니다.
첫 번째는 내 코드에 대한 추론을 어렵게 만듭니다. 두 번째는 내 코드의 성능을 저하시킵니다. 기본적으로 쓰기시 복사 (copy-on-write) 의미론을 사용하여 변경 불가능한 객체를 모방하고 있지만 호출 된 메서드가 실제로 내 객체의 상태를 수정하는지 여부에 관계없이 항상 수행합니다.
대신를 사용 MyImmutableObject하면 내가 설정 한 값이 내 방법의 수명에 대한 값임을 확신 할 수 있습니다. 내 아래에서 그것을 바꿀 "원거리에서의 으스스한 행동"은 없으며, 다섯 가지 다른 방법을 호출하기 전에 내 개체의 방어 복사본을 만들 필요가 없습니다. 다른 방법은 자신의 목적을 위해 변경 일을하려는 경우 그들은 그러나 그들은 정말 (내 각각의 모든 외부 메서드 호출하기 전에 그 일을 반대) 사본을해야 할 경우 그들은 단지 이렇게 - 복사를해야한다. 나는 현재의 소스 파일에도 없을 수있는 방법을 추적하는 정신적 자원을 아끼고, 만일을 대비하여 끊임없이 불필요한 방어 복사본을 만드는 오버 헤드를 시스템에 아끼지 않습니다.
(Java 세계를 벗어나서 C ++ 세계로 들어가면 더 까다로울 수 있습니다. 객체를 변경 가능한 것처럼 보이게 만들 수 있지만, 뒤에서 투명하게 복제 할 수 있습니다. 어느 누구도 현명하지 않은 상태 변경 (기록 중 복사)입니다.)
향후 방문자를위한 2 센트 :
불변 객체가 좋은 선택이되는 두 가지 시나리오는 다음과 같습니다.
멀티 스레드 환경의 동시성 문제는 동기화를 통해 매우 잘 해결할 수 있지만 동기화는 비용이 많이 듭니다 ( "이유"에 대해 자세히 설명하지 않음). 따라서 변경 불가능한 객체를 사용하는 경우 동시성 문제를 해결하기위한 동기화가 없습니다. 불변 객체는 변경할 수 없으며 상태를 변경할 수없는 경우 모든 스레드가 객체에 원활하게 액세스 할 수 있습니다. 따라서 불변 객체는 다중 스레드 환경의 공유 객체에 대한 훌륭한 선택입니다.
해시 기반 컬렉션으로 작업 할 때 가장 중요한 점 중 하나는 키가 hashCode()객체의 수명 동안 항상 동일한 값을 반환해야한다는 것입니다. 해당 값이 변경되면 해시 기반 컬렉션에 이전 항목이 만들어지기 때문입니다. 해당 개체를 사용하면 검색 할 수 없으므로 메모리 누수가 발생합니다. 불변 객체의 상태는 변경할 수 없으므로 해시 기반 컬렉션의 키로 훌륭한 선택을합니다. 따라서 변경 불가능한 객체를 해시 기반 수집의 키로 사용하는 경우 그로 인해 메모리 누수가 발생하지 않는다는 것을 확신 할 수 있습니다 (물론 키로 사용 된 객체가 어디에서든 참조되지 않는 경우에도 메모리 누수가 발생할 수 있습니다. 그렇지 않으면 여기서 요점이 아닙니다).
불변 객체는 일단 시작되면 상태가 변경되지 않는 인스턴스입니다. 이러한 개체의 사용은 요구 사항에 따라 다릅니다.
불변 클래스는 캐싱 목적에 적합하며 스레드로부터 안전합니다.
final 키워드를 사용한다고해서 반드시 변경할 수있는 것은 아닙니다.
public class Scratchpad {
public static void main(String[] args) throws Exception {
SomeData sd = new SomeData("foo");
System.out.println(sd.data); //prints "foo"
voodoo(sd, "data", "bar");
System.out.println(sd.data); //prints "bar"
}
private static void voodoo(Object obj, String fieldName, Object value) throws Exception {
Field f = SomeData.class.getDeclaredField("data");
f.setAccessible(true);
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(f, f.getModifiers() & ~Modifier.FINAL);
f.set(obj, "bar");
}
}
class SomeData {
final String data;
SomeData(String data) {
this.data = data;
}
}
프로그래머의 오류를 방지하기 위해 "final"키워드가 있다는 것을 보여주는 예제 일뿐입니다. 최종 키워드가없는 값을 재 할당하는 것은 우연히 쉽게 일어날 수있는 반면, 값을 변경하기 위해이 길이로 이동하는 것은 의도적으로 수행되어야합니다. 문서화 및 프로그래머 오류 방지를 위해 있습니다.
불변 데이터 구조는 재귀 알고리즘을 코딩 할 때도 도움이 될 수 있습니다. 예를 들어, 3SAT 문제 를 해결하려고한다고 가정 해보십시오 . 한 가지 방법은 다음을 수행하는 것입니다.
문제를 나타내는 가변 구조가있는 경우 TRUE 분기에서 인스턴스를 단순화 할 때 다음 중 하나를 수행해야합니다.
그러나 영리하게 코딩하면 모든 작업이 업데이트 된 (그러나 여전히 변경 불가능한) 버전의 문제를 반환하는 변경 불가능한 구조를 가질 수 있습니다 (예 : String.replace문자열을 대체하지 않고 새 버전 만 제공함). ). 이를 구현하는 순진한 방법은 "불변"구조를 모든 수정에 대해 복사하고 새로 만드는 것입니다. 그 오버 헤드와 함께 변경 가능한 것을 가질 때 두 번째 솔루션으로 줄이십시오.하지만 더 많은 작업을 수행 할 수 있습니다. 효율적인 방법.
불변 클래스가 "필요"한 이유 중 하나는 모든 것을 참조로 전달하고 객체의 읽기 전용 뷰를 지원하지 않는 조합입니다 (예 : C ++의 const )를 .
관찰자 패턴을 지원하는 클래스의 간단한 경우를 고려하십시오.
class Person {
public string getName() { ... }
public void registerForNameChange(NameChangedObserver o) { ... }
}
경우 string불변되지 않은 위해, 그것은 불가능하다 Person클래스가 구현하는 registerForNameChange()제대로 사람이, 다음 쓰기 효과적으로 어떤 통지를 트리거하지 않고 사람의 이름을 변경 할 수 있기 때문에.
void foo(Person p) {
p.getName().prepend("Mr. ");
}
C ++에서 getName()a const std::string&를 반환하면 참조로 반환하고 뮤 테이터에 대한 액세스를 방지하는 효과가 있습니다. 즉, 해당 컨텍스트에서는 변경할 수없는 클래스가 필요하지 않습니다.
그들은 또한 우리에게 보증을 제공합니다. 불변성의 보장은 우리가 그것들을 확장하고 그렇지 않으면 불가능한 효율성을 위해 새로운 패턴을 만들 수 있음을 의미합니다.
아직 호출되지 않은 불변 클래스의 한 가지 기능 : 완전 불변 클래스 객체에 대한 참조를 저장하는 것은 그 안에 포함 된 모든 상태를 저장하는 효율적인 수단입니다. 50K 상당의 상태 정보를 보유하기 위해 변경 불가능한 객체를 사용하는 변경 가능한 객체가 있다고 가정합니다. 더 나아가, 내가 원래 (변경 가능한) 객체의 "복사본"을 25 번 만들고 싶다고 가정하자 (예 : "실행 취소"버퍼를 위해); 상태는 복사 작업 사이에 변경 될 수 있지만 일반적으로 그렇지 않습니다. 변경 가능한 객체의 "복사본"을 만들려면 단순히 불변 상태에 대한 참조를 복사해야하므로 20 개의 복사본은 단순히 20 개의 참조에 해당합니다. 대조적으로 상태가 50K 가치의 변경 가능한 객체에 보관 된 경우 25 개의 복사 작업 각각은 50K 가치의 데이터에 대한 자체 사본을 생성해야합니다. 25 개 사본을 모두 보유하려면 대부분 중복 된 데이터를 1 메가 이상 보유해야합니다. 첫 번째 복사 작업은 절대 변경되지 않는 데이터의 복사본을 생성하고 다른 24 개 작업은 이론적으로 단순히 다시 참조 할 수 있지만 대부분의 구현에서는 두 번째 개체가 복사본을 요청할 방법이 없습니다. 불변 복사본이 이미 존재한다는 것을 알기위한 정보 (*).
(*) 때때로 유용 할 수있는 한 가지 패턴은 변경 가능한 객체가 상태를 유지하는 두 개의 필드 (하나는 변경 가능한 형식이고 다른 하나는 변경 불가능한 형식)를 갖는 것입니다. 객체는 변경 가능하거나 변경 불가능한 것으로 복사 할 수 있으며 하나 또는 다른 참조 세트로 생명을 시작합니다. 객체가 상태를 변경하려고하면 변경 불가능한 참조를 변경 가능한 참조 (아직 수행되지 않은 경우)에 복사하고 변경 불가능한 참조를 무효화합니다. 객체가 불변으로 복사 될 때 불변 참조가 설정되지 않은 경우 불변 복사본이 생성되고 불변 참조가이를 가리 킵니다. 이 접근 방식은 "본격적인 쓰기시 복사"보다 몇 가지 더 많은 복사 작업이 필요합니다 (예 : 마지막 복사 이후 변경된 객체를 복사하려면 복사 작업이 필요합니다.