널 포인터 대 널 오브젝트 패턴


27

귀속 : 이것은 관련 P.SE 질문에서 자랐습니다.

내 배경은 C / C ++이지만 Java에서 상당한 양의 일을했으며 현재 C #을 코딩하고 있습니다. 내 C 배경으로 인해 전달 및 반환 된 포인터를 확인하는 것이 간접적이지만 내 관점을 편견으로 인정합니다.

나는 최근 에 객체가 항상 반환된다는 아이디어가 있는 Null Object Pattern에 대한 언급을 보았습니다 . 일반적인 경우는 채워지고 예상 된 개체를 반환하고 오류 경우는 널 포인터 대신 빈 개체를 반환합니다. 호출 함수는 항상 액세스 할 오브젝트가 있으므로 널 액세스 메모리 위반을 피해야한다는 전제입니다.

  • 그렇다면 널 체크 패턴의 장점과 단점은 무엇입니까?

NOP로 더 깨끗한 호출 코드를 볼 수 있지만 그렇지 않은 경우 숨겨진 실패가 발생하는 위치도 볼 수 있습니다. 차라리 실수로 야생으로 빠져 나가는 것보다 응용 프로그램을 개발하는 동안 응용 프로그램이 열심히 실패하는 경우가 있습니다 (일명 예외).

  • 널 오브젝트 패턴이 널 점검을 수행하지 않는 것과 유사한 문제점을 가질 수 없습니까?

내가 작업 한 많은 객체는 객체 또는 컨테이너를 보유합니다. 주 객체의 모든 컨테이너에 빈 객체가 있음을 보장하기 위해 특별한 경우가 있어야합니다. 여러 계층의 중첩으로 인해 추한 것처럼 보일 수 있습니다.


"오류 사례"가 아니라 "모든 경우에 유효한 객체"입니다.

@ ThorbjørnRavnAndersen-좋은 지적, 그리고 그것을 반영하기 위해 질문을 편집했습니다. 여전히 NOP의 전제를 놓치고 있다면 알려주십시오.

6
짧은 대답은 "널 오브젝트 패턴"이 잘못되었다는 것입니다. 보다 정확하게는 "널 오브젝트 안티 패턴"이라고합니다. 일반적으로 클래스의 전체 인터페이스를 구현하는 유효한 객체 인 "오류 객체"를 사용하는 것이 좋지만,이를 사용하면 큰 소리로 갑작스럽게 사망 할 수 있습니다.
Jerry Coffin

1
@JerryCoffin이 경우는 예외로 표시해야합니다. 아이디어는 점이다 결코 널을 얻을 수 있으며, 따라서 널 (null)을 검사 할 필요가 없습니다.

1
나는이 "패턴"을 좋아하지 않는다. 그러나 모든 외래 키가 완벽하게 null이 아닌 최종 업데이트 전에 편집 가능한 데이터를 준비하는 동안 외래 키의 특수 "널 행"을 쉽게 참조 할 수있는 과도하게 구속 된 관계형 데이터베이스에 뿌리를두고있을 수 있습니다.

답변:


24

치명적인 오류가 발생하여 null (또는 Null Object)이 반환되는 위치에는 Null 개체 패턴을 사용하지 않습니다. 그 장소에서는 계속 null을 반환합니다. 경우에 따라 복구가없는 경우 최소한 크래시 덤프가 문제가 발생한 위치를 정확하게 나타내므로 크래시가 발생할 수 있습니다. 이러한 경우 자체 오류 처리를 추가해도 프로세스가 계속 종료되지만 복구가없는 경우에는 오류 처리가 충돌 덤프가 제공 한 매우 중요한 정보를 숨 깁니다.

널 오브젝트 패턴은 오브젝트를 찾을 수없는 경우에 수행 될 수있는 기본 동작이있는 위치에 더 적합합니다. 예를 들어 다음을 고려하십시오.

User* pUser = GetUser( "Bob" );

if( pUser )
{
    pUser->SetAddress( "123 Fake St." );
}

NOP를 사용하면 다음과 같이 작성됩니다.

GetUser( "Bob" )->SetAddress( "123 Fake St." );

이 코드의 동작은 "Bob이 있으면 주소를 업데이트하고 싶습니다"입니다. 분명히 응용 프로그램에 Bob이 있어야한다면 조용히 성공하기를 원하지 않습니다. 그러나 이런 유형의 행동이 적절한 경우가 있습니다. 이 경우 NOP가 훨씬 더 깨끗하고 간결한 코드를 생성하지 않습니까?

Bob없이 실제로 살 수없는 곳에서는 GetUser ()가 상위 수준에서 처리되고 일반적인 작업 실패를보고하는 응용 프로그램 예외 (예 : 액세스 위반 등)를 발생시킵니다. 이 경우 NOP는 필요하지 않지만 명시 적으로 NULL을 확인할 필요는 없습니다. NULL을 검사하는 IMO는 코드를 더 크게 만들고 가독성을 없애줍니다. NULL 검사는 여전히 일부 인터페이스에서 올바른 디자인 선택이지만 일부 사람들이 생각하는 것만 큼 많지는 않습니다.


4
null 객체 패턴의 사용 사례에 동의하십시오. 그러나 치명적인 오류가 발생했을 때 왜 null을 반환합니까? 왜 예외를 던지지 않습니까?
Apoorv Khurasia 2018 년

2
@MonsterTruck : 그 반대입니다. 코드를 작성할 때 외부 구성 요소에서 수신 한 대부분의 입력을 확인해야합니다. 그러나 내부 클래스 사이에서 한 클래스의 함수가 NULL을 반환하지 않도록 코드를 작성하면 호출 측에서 "안전"하기 위해 null 검사를 추가하지 않습니다. 해당 값이 NULL 일 수있는 유일한 방법은 내 응용 프로그램에서 치명적인 논리 오류가 발생한 경우입니다. 이 경우 멋진 크래시 덤프 파일을 가져 와서 스택 및 객체 상태를 되돌려 논리 오류를 식별하기 때문에 프로그램이 해당 지점에서 정확하게 충돌하기를 원합니다.
DXM

2
@MonsterTruck : "reverse that"은 치명적인 오류를 식별 할 때 명시 적으로 NULL을 반환하지 않지만 예기치 않은 치명적인 오류로 인해 NULL이 발생할 것으로 예상합니다.
DXM

이제 개체 할당 자체가 실패하는 치명적인 오류의 의미를 이해했습니다. 권리? 편집 : 닉네임의 길이가 3 자이므로 @ DXM을 수행 할 수 없습니다.
Apoorv Khurasia

@MonsterTruck : "@"에 대해 이상합니다. 나는 다른 사람들이 전에 그것을 한 것을 알고 있습니다. 그들이 최근에 웹 사이트를 조정했는지 궁금합니다. 할당 할 필요는 없습니다. 클래스를 작성하고 API의 의도가 항상 유효한 객체를 반환하는 것이라면 호출자가 null 검사를하지 않아도됩니다. 그러나 치명적인 오류는 프로그래머 오류 (오타가 리턴되는 오타) 또는 시간 전에 오브젝트를 지운 경쟁 조건 또는 스택 손상과 같은 것일 수 있습니다. 기본적으로 NULL을 반환하는 예기치 않은 코드 경로가 반환됩니다. 그런 일이 생기면 ...
DXM

7

그렇다면 널 체크 패턴의 장점과 단점은 무엇입니까?

찬성

  • 더 많은 경우를 해결하므로 널 검사가 더 좋습니다. 모든 객체가 제정신 기본 또는 동작하지 않는 동작을 갖는 것은 아닙니다.
  • null 확인이 더 확실합니다. 제정신 기본값을 가진 개체도 제정신 기본값이 유효하지 않은 장소에서 사용됩니다. 실패 할 경우 근본 원인에 근접한 코드가 실패해야합니다. 코드가 실패하면 분명히 실패합니다.

단점

  • Sane 기본값은 일반적으로 코드가 더 깨끗합니다.
  • Sane 기본값은 일반적으로 문제가 발생할 경우 치명적인 오류가 줄어 듭니다.

이 마지막 "프로"는 각각의 적용 시점과 관련하여 (내 경험상) 주요 차별화 요소입니다. "실패가 시끄러울 까?" 어떤 경우에는 실패가 즉각적이고 어려워지기를 원합니다. 어떤 시나리오에서는 결코 일어나지 않아야하는 경우가 있습니다. 중요한 자원을 찾을 수 없다면 ... 등. 어떤 경우에는 실제로 오류 가 아니기 때문에 정상적인 기본값으로 괜찮습니다 . 사전에서 값을 얻는 것이지만 키가 누락되었습니다.

다른 디자인 결정과 마찬가지로 필요에 따라 장점과 단점이 있습니다.


1
전문가 섹션에서 : 첫 번째 포인트에 동의합니다. 두 번째 요점은 설계자가 잘못한 것인데, 널 (null)도 사용할 수없는 곳에 사용할 수 있기 때문입니다. 패턴에 대해 나쁜 점은 패턴을 잘못 사용한 개발자에게만 반영됩니다.
Apoorv Khurasia 1

받는 사람없이 돈을 보내거나 돈을 보내지 못하여 더 이상 돈을 보내지 못하고 명백한 수표 전에 알 수없는 치명적인 실패 란 무엇입니까?
user470365

단점 : Sane 기본값을 사용하여 새 객체를 만들 수 있습니다. 널이 아닌 오브젝트가 리턴되면 다른 오브젝트를 작성할 때 일부 속성을 수정할 수 있습니다. 널 오브젝트가 리턴되면 새 오브젝트를 작성하는 데 사용될 수 있습니다. 두 경우 모두 동일한 코드가 작동합니다. copy-modify와 new를 위해 별도의 코드가 필요하지 않습니다. 이것은 모든 코드 줄이 버그의 또 다른 장소라는 철학의 일부입니다. 줄을 줄이면 버그가 줄어 듭니다.
shawnhcorey

@shawnhcorey-무엇?
Telastyn

null 객체는 마치 실제 객체 인 것처럼 사용할 수 있어야합니다. 예를 들어 IEEE의 NaN (숫자가 아님)을 고려하십시오. 방정식이 잘못되면 반환됩니다. 그리고 어떤 연산이라도 NaN을 반환하기 때문에 추가 계산에 사용할 수 있습니다. 모든 산술 연산 후에 NaN을 확인할 필요가 없으므로 코드가 간단 해집니다.
shawnhcorey

6

나는 객관적인 정보의 좋은 출처는 아니지만 주관적으로 :

  • 나는 내 시스템이 죽기를 원하지 않습니다. 나에게 그것은 잘못 설계되었거나 불완전한 시스템의 표시입니다. 완전한 시스템은 모든 가능한 경우를 처리해야하며 예상치 못한 상태가되지 않아야합니다. 이를 달성하기 위해서는 모든 흐름과 상황을 모델링 할 때 명시 적이어야합니다. null을 사용하는 것은 어쨌든 명시 적이 지 않으며 하나의 umrella에서 많은 다른 경우를 그룹화합니다. NonExistingUser 객체를 null보다 선호하고 NaN을 null보다 선호합니다. ... 이러한 접근 방식을 사용하면 그러한 상황과 처리를 원하는만큼 자세하게 설명 할 수 있지만 null은 null 옵션 만 남겨 둡니다. Java와 같은 환경에서 모든 객체는 null 일 수 있으므로 특정 사례와 특정 사례를 숨길 것을 선호하는 이유는 무엇입니까?
  • null 구현은 객체와 동작이 크게 다르며 대부분 모든 객체 세트에 붙어 있습니다 (왜? 왜? 왜?). 나에게 그러한 극적인 차이는 명시 적으로 처리하지 않는 한 시스템이 죽을 가능성이 가장 큰 오류를 나타내는 null 디자인의 결과처럼 보입니다 (나는 많은 사람들이 실제로 위의 게시물에서 죽는 것을 선호합니다). 그것은 근본적으로 결함이있는 접근 방식입니다. 명시 적으로주의를 기울이지 않으면 시스템이 기본적으로 죽을 수 있습니다. 매우 안전하지 않습니까? 그러나 어쨌든 null과 객체를 같은 방식으로 처리하는 코드를 작성하는 것은 불가능합니다. 기본 내장 연산자 (할당, 같음, 매개 변수 등)는 거의 작동하지 않으며 다른 모든 코드는 실패하고 필요합니다. 완전히 다른 두 경로;
  • Null Object 스케일을 더 잘 사용하면 원하는만큼 많은 정보와 구조를 넣을 수 있습니다. NonExistingUser는 매우 좋은 예입니다. 수신하려는 사용자의 전자 메일을 포함하고 해당 정보를 기반으로 새 사용자를 만들 것을 제안 할 수 있지만 null 솔루션을 사용하면 사람들이 액세스하려고 시도한 전자 메일을 유지하는 방법을 생각해야합니다 널 결과 처리에 가깝습니다.

Java 또는 C #과 같은 환경에서는 명시 성의 주요 이점을 제공하지 않습니다. 솔루션의 안전이 완벽합니다. 시스템의 객체 대신 null을받을 수 있으며 Java는 아무런 의미가 없습니다 (Beans에 대한 사용자 정의 주석 제외) 등을 제외하고는 명시적인 ifs를 제외하고는 그러나 null 구현이없는 시스템에서 문제 해결 방법에 접근하면 예외적 인 경우를 나타내는 객체로 언급 된 모든 이점을 얻을 수 있습니다. 따라서 주류 언어가 아닌 다른 것에 대해 읽으면 코딩 방식이 바뀌는 것을 알게 될 것입니다.

일반적으로 Null / Error 객체 / 반환 유형은 매우 견고한 오류 처리 전략으로, 수학적으로는 건전한 것으로 간주되지만 Java 또는 C의 예외 처리 전략은 "die ASAP"전략의 한 단계에 지나지 않으므로 기본적으로 모든 부담을 남겨 두십시오. 개발자 또는 관리자의 시스템 불안정성 및 예측 불가능 성.

이 전략보다 우월한 것으로 간주 될 수있는 모나드 (처음에는 이해의 복잡성이 뛰어남)도 있지만, 유형의 외부 측면을 모델링해야하는 경우와 Null 개체는 내부 측면을 모델링하는 경우에 대한 것입니다. 따라서 NonExistingUser는 아마도 monad maybe_existing으로 모델링하는 것이 좋습니다.

마지막으로 Null Object 패턴과 자동 처리 오류 상황 사이에 연결이 있다고 생각하지 않습니다. 다른 방법으로 사용해야합니다. 각 오류 사례를 명시 적으로 모델링하고 적절하게 처리해야합니다. 이제 당연히 (무슨 이유가 있든) 오류 사례를 명시 적으로 처리하지 않고 일반적으로 처리하도록 결정할 수 있습니다.


9
예기치 않은 일이 발생했을 때 응용 프로그램이 실패하는 것을 좋아하는 이유는 예기치 않은 일이 발생했기 때문입니다. 어떻게이 일이 일어 났어요? 왜? 충돌 덤프가 발생하여 잠재적으로 위험한 문제를 조사하여 코드에 숨기지 않고 해결할 수 있습니다. C #에서는 응용 프로그램을 복구하려고 모든 예기치 않은 예외를 포착 할 수 있지만 문제가 발생한 위치를 기록하고 별도의 처리가 필요한지 여부를 확인하고 싶습니다 ( " 이 오류를 기록하면 실제로 예상되고 복구 가능합니다 ").
Luaan

3

Null 객체의 실행 가능성은 언어가 정적으로 입력되었는지 여부에 따라 약간 다르게 보입니다. 루비 는 Null (또는 Nil언어의 용어로) 객체를 구현하는 언어입니다 .

초기화되지 않은 메모리에 대한 포인터를 다시 가져 오는 대신 언어는 객체를 반환합니다 Nil. 이는 언어가 동적으로 입력된다는 사실에 의해 촉진됩니다. 함수가 항상을 반환한다고 보장하지는 않습니다 int. Nil그것이 필요한 일이면 돌아올 수 있습니다 . 정적으로 형식이 지정된 언어에서는이 작업을보다 복잡하고 어렵게 만듭니다. 참조로 호출 할 수있는 모든 개체에 null이 자연스럽게 적용될 수 있기 때문입니다. 널이 될 수있는 모든 객체의 널 버전 구현을 시작해야합니다 (또는 스칼라 / 하스켈 경로로 이동하여 일종의 "아마도"래퍼를 가짐).

MrLister는 C ++에서 처음 참조 할 때 초기화 참조 / 포인터가 있다고 보장하지 않습니다. 원시 널 포인터 대신 전용 널 오브젝트를 사용하면 멋진 추가 기능을 얻을 수 있습니다. 루비 Nil에는 to_s빈 텍스트를 생성 하는 (toString) 함수가 포함되어 있습니다. 문자열에서 null 반환을 신경 쓰지 않고 충돌없이보고하지 않으려는 경우에 좋습니다. 오픈 클래스를 사용 Nil하면 필요한 경우 출력을 수동으로 무시할 수도 있습니다.

이것은 모두 소금 한 알로 먹을 수 있습니다. 동적 언어에서는 null 객체가 올바르게 사용될 때 큰 편의가 될 수 있다고 생각합니다. 불행히도, 그것을 최대한 활용하려면 기본 기능을 편집해야 할 수 있습니다. 이로 인해 프로그램을 더 표준적인 형태로 작업하는 데 익숙한 사람들이 프로그램을 이해하고 유지하기가 어려울 수 있습니다.


1

추가적인 관점에서 Objective-C를 살펴보십시오. 여기서 Null 객체 대신 nil kind-of 값은 객체처럼 작동합니다. nil 객체로 전송 된 모든 메시지는 아무런 영향을 미치지 않으며 메서드의 반환 값 유형에 관계없이 0 / 0.0 / NO (거짓) / NULL / nil 값을 반환합니다. 이것은 MacOS X 및 iOS 프로그래밍에서 매우 널리 사용되는 패턴으로 사용되며 일반적으로 0을 반환하는 nil 객체가 합리적인 결과가되도록 메소드가 설계됩니다.

예를 들어 문자열에 하나 이상의 문자가 있는지 확인하려면 다음과 같이 쓸 수 있습니다.

aString.length > 0

존재하지 않는 문자열에는 문자가 없으므로 aString == nil 인 경우 합리적인 결과를 얻습니다. 사람들은 익숙해지기까지 다소 시간이 걸리지 만, 다른 언어로 어디서나 nil 값을 확인하는 데 익숙해지는 데는 시간이 걸릴 것입니다.

(NSNull 클래스의 싱글 톤 객체가있어서 실제로 다르게 동작하지는 않습니다.)


Objective-C는 Null Object / Null Pointer를 실제로 흐릿하게 만들었습니다. nil얻을 #define널 객체와 같은 NULL 및 동작합니다에 'D를. 이는 C # / Java에서 널 포인터가 오류를 발생시키는 동안 런타임 기능입니다.
Maxthon Chan

0

피할 수 있으면 어느 쪽도 사용하지 마십시오. 옵션 유형, 튜플 또는 전용 합계 유형을 반환하는 팩토리 함수를 사용하십시오. 다시 말해, 기본이 아닌 보장 된 값과 다른 유형의 잠재적으로 기본값이있는 값을 나타냅니다.

먼저 desiderata를 나열한 다음 몇 가지 언어 (C ++, OCaml, Python)로이를 표현할 수있는 방법에 대해 생각해 봅시다.

  1. 컴파일 타임에 기본 객체의 원치 않는 사용을 잡습니다.
  2. 코드를 읽는 동안 주어진 값이 기본값이 될 수 있는지 여부를 분명히하십시오.
  3. 해당되는 경우 유형 당 한 번씩 합리적인 기본값을 선택하십시오 . 모든 통화 사이트에서 잠재적으로 다른 기본값을 선택 하지 마십시오 .
  4. 정적 분석 도구 또는 사람이 grep잠재적 인 실수를 쉽게 찾을 수 있도록합니다 .
  5. 일부 응용 프로그램의 경우 예기치 않게 기본값이 지정되면 프로그램이 정상적으로 계속 진행되어야합니다. 다른 응용 프로그램의 경우, 기본값으로 정보를 제공하면 프로그램이 즉시 중지되어야합니다.

Null Object Pattern과 Null 포인터 사이의 장력은 (5)에서 비롯된 것 같습니다. 그래도 오류를 조기에 감지 할 수 있다면 (5)는 약해집니다.

이 언어를 언어별로 고려해 봅시다 :

C ++

제 생각에는 C ++ 클래스는 일반적으로 라이브러리와의 상호 작용을 용이하게하고 컨테이너에서 클래스를 사용하기 쉽기 때문에 기본 구성 가능해야합니다. 또한 어떤 수퍼 클래스 생성자를 호출 할 필요가 없으므로 상속을 단순화합니다.

그러나 이는 유형 값이 MyClass"기본 상태" 인지 여부를 확실하게 알 수 없음을 의미합니다 . bool nonempty런타임시 표면 기본값에 유사하거나 유사한 필드를 넣는 것 외에도 사용자가 확인하도록 강제하는 새로운 인스턴스를 생성MyClass 하는 것이 가장 좋습니다 .

나는를 반환하는 공장 기능을 사용하는 것이 좋습니다 것 std::optional<MyClass>, std::pair<bool, MyClass>A를 또는 r 값 참조를 std::unique_ptr<MyClass>가능합니다.

팩토리 함수가 default-constructed와는 다른 일종의 "자리 표시 자"상태를 리턴 MyClass하도록하려면 a를 사용하고 std::pair함수가이를 수행하는지 문서화해야합니다.

팩토리 기능에 고유 한 이름이 있으면 이름을 쉽게 작성하고 grep구할 수 있습니다. 그러나 grep프로그래머 가 팩토리 기능을 사용해야했지만 그렇지 않은 경우에는 어렵습니다 .

오캄

OCaml과 같은 언어를 사용하는 경우 option유형 (OCaml 표준 라이브러리) 또는 either유형 (표준 라이브러리는 아니지만 자신 만의 롤링)을 사용할 수 있습니다. 또는 defaultable유형 (나는 용어를 구성하고 있습니다 defaultable).

type 'a option =
    | None
    | Some of 'a

type ('a, 'b) either =
    | Left of 'a
    | Right of 'b

type 'a defaultable =
    | Default of 'a
    | Real of 'a

defaultable사용자가 패턴 일치를 추출해야 'a하고 쌍의 첫 번째 요소를 무시할 수 없기 때문에 위에 표시된 것처럼 A 는 쌍보다 낫습니다 .

등가의 defaultable위와 같은 유형은 C ++를 사용하여 사용될 수있다 std::variant동일한 유형의 두 인스턴스하지만 일부 std::variant두 종류가 동일한 경우 API를 사용할 수 없게된다. 또한 std::variant형식 생성자의 이름이 지정되지 않았기 때문에 이상한 사용입니다 .

파이썬

어쨌든 파이썬에 대한 컴파일 타임 검사는 없습니다. 그러나 동적으로 유형을 지정하면 일반적으로 컴파일러를 대체하기 위해 특정 유형의 자리 표시 자 인스턴스가 필요한 상황이 없습니다.

기본 인스턴스를 만들어야 할 때 예외를 던지는 것이 좋습니다.

그것이 받아 들일 수 없다면, 나는 공장 함수에서 DefaultedMyClass상속 하고 그것을 MyClass반환하는 것을 만드는 것이 좋습니다 . 필요한 경우 기본 인스턴스에서 "스터 빙 아웃 (stubbing out)"기능 측면에서 유연성을 확보합니다.

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