다른 프로그래머에게 클래스 구현에 대해 경고하는 방법


96

"특정 방법으로 사용해야합니다"클래스를 작성하고 있습니다 (모든 클래스는 반드시 ...).

예를 들어 fooManager클래스를 만들려면을 호출 해야 합니다 Initialize(string,string). 그리고 예제를 조금 더 나아 가기 위해 클래스의 ThisHappened행동을 듣지 않으면 클래스는 쓸모가 없습니다 .

내 요점은, 내가 쓰고있는 클래스에는 메소드 호출이 필요하다는 것입니다. 그러나 이러한 메소드를 호출하지 않으면 컴파일이 잘되고 빈 FooManager가 생깁니다. 어떤 시점에서는 클래스와 클래스에 따라 작동하지 않거나 충돌 할 수 있습니다. 내 클래스를 구현하는 프로그래머는 분명히 내부를 들여다보고 "아, 나는 Initialize를 호출하지 않았다!"

그러나 나는 그것을 좋아하지 않습니다. 내가 이상적으로 원하는 것은 메소드가 호출되지 않은 경우 컴파일하지 않는 코드입니다. 나는 그것이 불가능하다는 것을 추측하고 있습니다. 또는 즉시 눈에 띄고 명확한 것.

나는 내가 여기에 현재 접근 방식에 문제가 있습니다.

클래스에 개인 부울 값을 추가하고 클래스가 초기화되었는지 여부를 확인하십시오. 그렇지 않은 경우 "클래스가 초기화되지 않았습니다 .Initialize(string,string). 전화 하시겠습니까?" 라는 예외가 발생 합니다.

나는 그 접근법에 대해 괜찮지 만, 결국 최종 사용자에게 필요하지 않은 많은 코드를 컴파일합니다.

또한 Initiliaze호출 하는 것보다 더 많은 메소드가있을 때 때로는 더 많은 코드 입니다. 수업을 너무 많은 공개 방법 / 행동으로 유지하려고 노력하지만 문제를 해결하지는 못하고 합리적으로 유지하십시오.

내가 찾고있는 것은 :

  • 내 접근 방식이 맞습니까?
  • 더 좋은 것이 있습니까?
  • 너희들은 무엇을 / 조언합니까?
  • 문제가 아닌 문제를 해결하려고합니까? 나는 동료들에게 수업을 사용하기 전에 수업을 확인해야한다고 들었습니다. 나는 정중하게 동의하지 않지만 그것은 내가 믿는 또 다른 문제입니다.

간단히 말해서, 나중에 클래스를 재사용 할 때 또는 다른 사람이 호출을 구현하는 것을 잊지 않는 방법을 찾으려고합니다.

설명 :

여기에 많은 질문을 명확히하기 위해 :

  • 클래스의 초기화 부분에 대해서만 이야기 할뿐만 아니라 평생 동안입니다. 동료가 메소드를 두 번 호출하지 못하게하여 Y보다 X 앞에 X를 호출하도록하십시오. 필수적이고 문서화되었지만 결국 코드에서 가능한 한 간단하고 작은 것을 원합니다. Asserts의 아이디어가 정말 마음에 들었지만 Asserts가 항상 가능하지는 않기 때문에 다른 아이디어를 혼합해야한다고 확신합니다.

  • C # 언어를 사용하고 있습니다! 나는 그것을 어떻게 언급하지 않았습니까?! 저는 Xamarin 환경에 있으며 PCL, iOS, Android 및 Windows 프로젝트를 포함한 솔루션에서 일반적으로 약 6-9 개의 프로젝트를 사용하는 모바일 앱을 구축하고 있습니다. 나는 약 1 년 반 동안 (학교와 일을 결합한) 개발자 였으므로 때로는 어리석은 진술 / 질문이 있습니다. 아마도 여기에는 관련이 없지만 너무 많은 정보가 항상 나쁜 것은 아닙니다.

  • 나는 플랫폼 제한과 의존성 주입의 사용으로 인해 생성자에 필수적인 모든 것을 항상 넣을 수는 없습니다. 인터페이스 이외의 매개 변수는 테이블에서 벗어납니다. 또는 내 지식이 충분하지 않아서 가능할 수도 있습니다. 대부분의 경우 초기화 문제는 아니지만 그 이상

그가 그 이벤트에 등록했는지 어떻게 확인할 수 있습니까?

그가 "어느 시점에서 프로세스를 중지"하는 것을 잊지 않도록 어떻게 할 수 있습니까?

여기에 Ad fetching 클래스가 있습니다. 광고가 표시되는보기가 표시되는 한 클래스는 1 분마다 새 광고를 가져옵니다. 해당 클래스는 광고를 표시 할 수있는 위치에 구성 할 때보기가 필요합니다. 그러나 일단 뷰가 사라지면 StopFetching ()을 호출해야합니다. 그렇지 않으면 클래스는 여전히 존재하지 않는보기에 대한 광고를 계속 가져 오며 이는 나쁘다.

또한 해당 클래스에는 "AdClicked"와 같이 들어야하는 이벤트가 있습니다. 듣지 않으면 모든 것이 잘 작동하지만 탭이 등록되지 않으면 분석 추적이 손실됩니다. 그래도 광고는 계속 작동하므로 사용자와 개발자에게는 차이가 없으며 분석 결과에 잘못된 데이터가 있습니다. 그것은 피해야하지만 개발자가 타오 이벤트에 등록해야한다는 것을 어떻게 알 수 있는지 잘 모르겠습니다. 그것은 간단한 예이지만, "그가 이용할 수있는 공개 행동을 사용하도록하십시오"는 물론 적절한시기에 아이디어가 있습니다!


13
클래스의 메소드에 대한 호출이 특정 순서로 발생하도록 보장하는 것은 일반적으로 불가능합니다 . 이것은 정지 문제와 동등한 많은 문제 중 하나입니다. 이 비 준수를 컴파일 타임 오류를 할 수있을 것입니다 있지만 어떤 경우, 어떤 일반적인 솔루션이 없습니다. 데이터 흐름 분석이 상당히 정교 해져도 최소한 명백한 경우를 진단 할 수있는 구문을 지원하는 언어를 지원하는 언어가 거의없는 이유 일 것입니다.
Kilian Foth


5
다른 질문에 대한 대답은 관련이 없으며 질문이 같지 않으므로 다른 질문입니다. 누군가 그 토론을 찾으면 다른 질문의 제목을 입력하지 않을 것입니다.
Gil Sand

6
ctor에서 initialize의 내용 을 병합 하지 못하게하는 것은 무엇입니까 ? initialize객체 생성 후 늦게 호출해야 합니까? ctor가 예외를 던지고 창작 체인을 깨뜨릴 수 있다는 점에서 너무 "위험"합니까?
발견

7
이 문제를 시간 결합 이라고 합니다. 가능하면 유효하지 않은 입력이 발생할 때 생성자에서 예외를 발생시켜 오브젝트를 유효하지 않은 상태에 두는 것을 피 new하십시오. 이렇게하면 오브젝트를 사용할 준비가되지 않은 경우 호출을 지나치지 않습니다. 초기화 방법에 의존하는 것은 당신을 괴롭히기 위해 돌아와야하며 절대적으로 필요한 경우가 아니면 피해야합니다. 적어도 그것은 내 경험입니다.
Seralize

답변:


161

이러한 경우, 적절한 초기화에 도움이되도록 언어의 유형 시스템을 사용하는 것이 가장 좋습니다. 우리는 어떻게 방지 할 수 있습니다 FooManager것을 사용 초기화하지 않고? 방지함으로써 FooManager되는 생성 적절히 초기화하는 데 필요한 정보없이. 특히, 모든 초기화는 생성자의 책임입니다. 생성자가 잘못된 상태에서 객체를 만들도록해서는 안됩니다.

그러나 호출자 FooManager는 초기화하기 전에를 만들어야합니다. 예를 들어 FooManager의존성이 전달 되기 때문 입니다.

없는 FooManager경우를 만들지 마십시오 . 대신 할 수있는 일은 초기화 정보 를 사용 하여 완전히 구성된 을 검색 할 수있는 객체를 전달하는 것입니다 FooManager. (함수 프로그래밍 연설에서는 생성자에 부분 응용 프로그램을 사용하는 것이 좋습니다.) 예 :

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

이것의 문제점은에 접근 할 때마다 초기화 정보를 제공해야한다는 것 FooManager입니다.

해당 언어로 필요한 경우 getFooManager()작업을 팩토리 또는 클래스와 같은 클래스로 래핑 할 수 있습니다 .

initialize()형식 시스템 수준 솔루션을 사용하는 대신 메서드가 호출 되었는지 런타임 검사를하고 싶습니다 .

타협점을 찾을 수 있습니다. 우리는를 반환 MaybeInitializedFooManager하는 get()메소드를 가진 래퍼 클래스 를 생성 FooManager하지만, FooManager완전히 초기화되지 않은 경우 발생합니다 . 이것은 랩퍼를 통해 초기화가 수행되거나 FooManager#isInitialized()메소드 가있는 경우에만 작동합니다 .

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

수업의 API를 변경하고 싶지 않습니다.

이 경우 if (!initialized) throw;각 방법마다 조건 을 피하고 싶을 것 입니다. 다행히도이를 해결하기위한 간단한 패턴이 있습니다.

사용자에게 제공하는 객체는 모든 호출을 구현 객체에 위임하는 빈 셸입니다. 기본적으로 구현 객체는 초기화되지 않은 각 메소드에 대해 오류를 발생시킵니다. 그러나이 initialize()메소드는 구현 오브젝트를 완전히 구성된 오브젝트로 바꿉니다.

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

클래스의 주요 동작을로 추출합니다 MainImpl.


9
나는 처음 두 솔루션 (+1)를 좋아하지만, 나는의 방법 중의 반복과 마지막 조각을 생각하지 않는다 FooManager와의 UninitialisedImpl어떤 반복보다 낫다 if (!initialized) throw;.
Bergi

3
@Bergi 어떤 사람들은 수업을 너무 좋아합니다. 많은 방법 FooManager이 있지만 일부 검사를 잊어 버리는 것보다 쉽습니다 . 그러나이 경우에는 수업을 나누는 것이 좋습니다. if (!initialized)
immibis

1
MaybeInitializedFoo는 초기화 된 Foo보다 나아 보이지는 않지만 옵션 / 아이디어를 제공하기 위해 +1입니다.
Matthew James Briggs

7
+1 공장은 올바른 옵션입니다. 디자인을 변경하지 않고는 디자인을 개선 할 수 없으므로 IMHO가 절반을 줄이는 방법에 대한 답변의 마지막 부분은 OP에 도움이되지 않습니다.
Nathan Cooper

6
@Bergi 마지막 섹션에 제시된 기술이 매우 유용하다는 것을 알았습니다 (“다형성을 통한 조건 바꾸기”리팩토링 기술 및 상태 패턴 참조). if / else를 사용하면 한 가지 방법으로 검사를 잊어 버리기 쉽고 인터페이스에 필요한 방법을 구현하는 것을 잊기가 훨씬 어렵습니다. 가장 중요한 것은 MainImpl이 initialize 메소드가 생성자와 병합되는 오브젝트와 정확히 일치한다는 것입니다. 이것은 MainImpl의 구현이 이것이 제공하는 더 강력한 보장을 누릴 수 있음을 의미하며, 이는 메인 코드를 단순화시킵니다. …
amon

79

클라이언트가 객체를 "오용"하는 것을 방지하는 가장 효과적이고 유용한 방법은 불가능한 것입니다.

가장 간단한 해결책은 Initialize생성자와 병합 하는 것입니다. 이렇게하면 클라이언트가 초기화되지 않은 상태에서 개체를 사용할 수 없으므로 오류가 발생하지 않습니다. 생성자 자체에서 초기화를 수행 할 수없는 경우 팩토리 메소드를 작성할 수 있습니다. 예를 들어, 클래스에 특정 이벤트를 등록해야하는 경우 생성자 또는 팩토리 메소드 서명에서 이벤트 리스너를 매개 변수로 요구할 수 있습니다.

초기화하기 전에 초기화되지 않은 객체에 액세스 할 수 있어야하는 경우 두 가지 상태를 별도의 클래스로 구현 할 수 있으므로 UnitializedFooManager인스턴스 를 시작하여 Initialize(...)를 반환 하는 메소드가 InitializedFooManager있습니다. 초기화 된 상태에서만 호출 할 수있는 메소드는에 만 존재합니다 InitializedFooManager. 필요한 경우이 접근 방식을 여러 상태로 확장 할 수 있습니다.

런타임 예외와 비교하여 상태를 별개의 클래스로 표현하는 것이 더 효과적이지만 컴파일 타임은 객체 상태에 유효하지 않은 메소드를 호출하지 않으며 코드에서 상태 전환을보다 명확하게 문서화합니다.

그러나보다 일반적으로 이상적인 솔루션은 특정 시간에 특정 순서로 메서드를 호출해야하는 등의 제약없이 클래스와 메서드를 디자인하는 것입니다. 항상 가능하지는 않지만 적절한 패턴을 사용하면 많은 경우에 피할 수 있습니다.

복잡한 시간 결합이있는 경우 (특정 순서로 여러 메소드를 호출해야 함) 한 가지 해결책 은 제어 반전 을 사용하는 것이므로 적절한 순서로 메소드를 호출하지만 템플리트 메소드 또는 이벤트를 사용하는 클래스를 작성하십시오 클라이언트가 흐름의 적절한 단계에서 사용자 지정 작업을 수행 할 수 있도록합니다. 이렇게하면 올바른 순서로 작업을 수행하는 책임이 클라이언트에서 클래스 자체로 전달됩니다.

간단한 예 : File파일에서 읽을 수 있는 -object가 있습니다. 그러나 메소드 를 호출 Open하기 전에 클라이언트가 호출 해야하며, ReadLine메소드가 더 이상 호출되어서는 안되는 Close이후에는 항상 예외가 발생하더라도 호출 ReadLine해야합니다. 이것은 시간적 결합입니다. 콜백이나 델리게이트를 인수로 취하는 단일 메소드를 사용하면 피할 수 있습니다. 이 메소드는 파일 열기, 콜백 호출 및 파일 닫기를 관리합니다. Read콜백에 메소드가 있는 고유 한 인터페이스를 전달할 수 있습니다 . 그렇게하면 클라이언트가 올바른 순서로 메소드를 호출하는 것을 잊을 수 없습니다.

시간적 결합을 가진 인터페이스 :

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

시간적 결합이없는 경우 :

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

더 무거운 솔루션은 File열린 파일에서 실행될 (보호 된) 메소드를 구현해야하는 추상 클래스 로 정의 하는 것입니다. 이것을 템플릿 메소드 패턴 이라고합니다 .

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

장점은 동일합니다. 클라이언트는 특정 순서로 메소드를 호출하는 것을 기억할 필요가 없습니다. 단순히 잘못된 순서로 메소드를 호출하는 것은 불가능합니다.


2
또한 나는 단순히 생성자로부터 갈 수 없었던 경우를 기억하지만, 지금 당장 보여줄 예는 없습니다
Gil Sand

2
이 예는 이제 팩토리 패턴을 설명하지만 첫 번째 문장은 실제로 RAII 패러다임을 설명 합니다. 그렇다면이 답변 중 어느 것이 좋습니다?
Ext3h

11
@ Ext3h 나는 공장 패턴과 RAII가 상호 배타적이라고 생각하지 않습니다.
Pieter B

@PieterB 아니요, 공장이 RAII를 보장하지 않을 수도 있습니다. 그러나 템플릿 / 생성자 인수 ( UnitializedFooManager) 는 실제로 의미 론적 상태와 같이 인스턴스 ( InitializedFooManager) 와 상태를 공유해서는 안된다는 추가 의미가 있습니다. 그렇지 않으면이 예제는 개발자가 동일한 템플리트를 두 번 사용하는 경우 새로운 문제점을 야기합니다. 그리고 언어가 템플릿을 안정적으로 사용하기 위해 의미를 명시 적으로 지원하지 않는 경우에도 정적으로 방지 / 확인할 수 없습니다 . Initialize(...)Instantiate(...)move
Ext3h

2
@ Ext3h : 가장 간단한 솔루션이므로 첫 번째 솔루션을 권장합니다. 그러나 클라이언트 초기화를 호출하기 전에 객체에 액세스 할 수 있어야하는 경우 사용할 수 없지만 두 번째 솔루션을 사용할 수 있습니다.
JacquesB

39

나는 일반적으로 IllegalStateException초기화를 확인하고 초기화하지 않은 상태에서 사용하려고하면 던져 버립니다 .

그러나 컴파일 타임 안전을 원하고 (그리고 칭찬 할 만하고 바람직하다면) 초기화를 구성되고 초기화 된 객체를 반환하는 팩토리 메소드로 취급하지 않는 이유는 무엇입니까?

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

그래서 당신은 객체 생성 및 라이프 사이클을 제어하고 고객이 얻을 Component귀하의 초기화의 결과로. 효과적으로 게으른 인스턴스입니다.


1
좋은 답변-간결한 답변으로 2 가지 좋은 옵션. 위의 다른 답변은 (이론적으로) 유효한 형식 시스템 체조를 제시합니다. 그러나 대부분의 경우이 두 가지 간단한 옵션이 이상적인 실용적인 선택입니다.
Thomas W

1
참고 : .NET에서 InvalidOperationException 은 Microsoft에서 사용자가 호출 한 것으로 승격됩니다 IllegalStateException.
miroxlav

1
나는이 답변을 다운 투표하지 않지만 실제로는 좋은 대답이 아닙니다. 파란 색을 던지 IllegalStateException거나 내버려두면 InvalidOperationException사용자는 자신이 잘못한 것을 결코 이해하지 못합니다. 이러한 예외가 설계 결함을 수정하는 우회가되어서는 안됩니다.
displayName

예외 발생은 하나의 가능한 옵션이며 필자가 선호하는 옵션을 자세히 설명하여 컴파일 타임을 안전하게 유지합니다. 나는 이것을 강조하기 위해 내 대답을 편집했다
Brian Agnew

14

나는 다른 답변들과 약간의 의견을 나누고 동의하지 않을 것입니다. 현재 어떤 언어를 사용하고 있는지 모른 채로 대답 할 수는 없습니다. 사용자는 언어가 제공하는 메커니즘과 해당 언어의 다른 개발자가 따르는 경향에 전적으로 의존합니다.

당신이있는 경우 FooManager하스켈, 것 범죄 사용자가 관리 할 수없는 일 구축 할 수 있도록하는 Foo타입 시스템은 너무 쉽게 그 하스켈 개발자가 기대하는 규칙을하기 때문에,들.

반면에, 당신은 C를 작성하는 경우, 동료는 당신을 꺼내 별도의 정의를 촬영 전적으로 자신의 권리 내 것 struct FooManagerstruct UninitializedFooManager는 약간의 이익을 위해 불필요하게 복잡한 코드로 이어질 것이기 때문에, 다른 작업을 지원 유형 .

파이썬을 작성하는 경우 언어를 지원하는 메커니즘이 없습니다.

아마도 Haskell, Python 또는 C를 작성하지는 않지만 형식 시스템이 얼마나 많은 작업을 수행하고 수행 할 수 있는지에 대한 예시적인 예입니다.

당신의 언어로 개발자의 합리적인 기대에 따라 , 그리고 자연, 관용적 구현이없는 솔루션을-엔지니어링을 통해 충동에 저항 실수가 극단적으로 갈 가치가 있음을 잡을 수 있도록 너무 쉽게 그리고 너무 힘들어하지 않는 한을 ( 불가능한 길이). 자신의 언어에 대한 경험이 부족하여 합리적인 것이 무엇인지 판단하지 못하는 경우 자신보다 더 잘 아는 사람의 조언을 따르십시오.


"Python을 작성하는 경우 언어로 할 수있는 메커니즘이 없습니다." 파이썬에는 생성자가 있으며 팩토리 메소드를 만드는 것은 매우 간단합니다. 어쩌면 "이것"이 무슨 뜻인지 이해가 안되나요?
AL Flanagan

3
"this"는 런타임과 달리 컴파일 타임 오류를 의미했습니다.
Patrick Collins

현재 버전 인 Python 3.5는 플러그 가능한 유형 시스템을 통해 유형이 있습니다. 코드를 가져 오기 때문에 코드를 실행하기 전에 Python을 오류로 만드는 것이 가능합니다. 3.5까지는 완전히 정확했습니다.
Benjamin Gruenbaum

오 그래. Belated +1. 혼란 스러웠지만 이것은 매우 통찰력이있었습니다.
AL Flanagan

나는 당신의 스타일 Patrick을 좋아합니다.
Gil Sand

4

코드 검사를 고객에게 제공하지 않으려는 것처럼 보이지만 프로그래밍 언어로 제공되는 경우 어설 션 함수를 사용할 수 있습니다.

그렇게하면 개발 환경에서 검사를하고 동료 개발자가 호출 할 수있는 모든 테스트는 예상대로 실패하지만 어설 션 (적어도 Java에서는)이 선택적으로 컴파일되기 때문에 고객에게 코드를 제공하지 않습니다.

따라서 이것을 사용하는 Java 클래스는 다음과 같습니다.

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

어설 션은 필요한 프로그램의 런타임 상태를 확인하는 데 유용한 도구이지만 실제 환경에서 실제로 누군가가 잘못한 것을 기대하지는 않습니다.

또한 그들은 상당히 슬림하고 if () throw ... 구문의 큰 부분을 차지하지 않으며 잡을 필요가 없습니다.


3

이 문제를 현재 답변보다 일반적으로 살펴보면 초기화에 주로 중점을 두었습니다. 두 가지 방법이있을 것이다 객체를 고려 a()하고 b(). 요구 사항은 a()항상 전에 호출 b()됩니다. 새로운 객체를 반환 하고 원래 객체가 아닌 새로운 객체로 a()이동 b()하여 컴파일 타임 검사를 수행 할 수 있습니다 . 구현 예 :

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

이제 a ()를 호출 할 때까지 숨겨진 인터페이스에서 구현되므로 a ()를 먼저 호출하지 않고 b ()를 호출 할 수 없습니다. 분명히 이것은 많은 노력이 필요하므로 일반적 으로이 접근법을 사용하지는 않지만 유익 할 수있는 상황이 있습니다 (특히 클래스가 많은 상황에서 재사용 될 수없는 프로그래머가 재사용 할 경우) 구현에 익숙하거나 신뢰성이 중요한 코드). 또한 이것은 많은 기존 답변에서 제안한대로 빌더 패턴의 일반화입니다. 그것은 거의 같은 방식으로 작동합니다. 실제 차이점은 데이터가 저장되는 위치 (반환 된 객체가 아닌 원래 객체에)와 사용하려는 시점 (초기화 중 만)입니다.


2

유용하기 전에 추가 초기화 정보가 필요하거나 다른 객체에 대한 링크가 필요한 기본 클래스를 구현할 때 기본 클래스를 추상으로 만든 다음 기본 클래스의 흐름에 사용되는 해당 클래스에서 여러 추상 메소드를 정의하는 경향이 있습니다 (예 : abstract Collection<Information> get_extra_information(args);abstract Collection<OtherObjects> get_other_objects(args);상속의 계약에 의해 기본 클래스에 필요한 모든 것을 제공하는 기본 클래스의 사용자를 강제 구체적인 클래스에 의해 구현되어야한다.

따라서 기본 클래스를 구현할 때 추상 메서드를 구현하기 만하면되기 때문에 기본 클래스가 올바르게 동작하도록 작성해야하는 내용을 즉시 명시 적으로 알고 있습니다.

편집 : 분명히하기 위해 이것은 기본 클래스의 생성자에 인수를 제공하는 것과 거의 동일하지만 추상 메소드 구현을 통해 구현에서 추상 메소드 호출에 전달 된 인수를 처리 할 수 ​​있습니다. 또는 인수를 사용하지 않는 경우에도 추상 메소드의 반환 값이 메소드 본문에서 정의 할 수있는 상태에 의존하는 경우에도 여전히 유용 할 수 있습니다. 이는 변수를 인수로 전달할 때 불가능합니다. 생성자 상속보다 컴포지션을 사용하려는 경우 동일한 원칙에 따라 동적 동작을 갖는 인수를 여전히 전달할 수 있습니다.


2

대답은 다음과 같습니다. 예, 아니요, 때로는. :-)

설명하는 문제 중 일부는 최소한 많은 경우에 쉽게 해결됩니다.

컴파일 타임 검사는 런타임 검사보다 확실히 바람직하며, 검사하지 않는 것이 좋습니다.

RE 런타임 :

런타임 검사를 통해 함수를 특정 순서 등으로 강제 호출하는 것은 개념적으로 간단합니다. 어떤 함수가 실행되었는지를 나타내는 플래그를 넣고 각 함수가 "사전 조건 -1이 아니거나 전제 조건 -2가 아닌 경우 예외를 던지십시오"와 같은 것으로 시작하십시오. 수표가 복잡해지면 개인 기능으로 밀어 넣을 수 있습니다.

어떤 일들은 자동적으로 일어날 수 있습니다. 간단한 예를 들기 위해 프로그래머는 종종 객체의 "게으른 인구"에 대해 이야기합니다. 인스턴스가 작성되면 is-populated 플래그를 false로 설정하거나 일부 키 오브젝트 참조를 널로 설정합니다. 그런 다음 관련 데이터가 필요한 함수가 호출되면 플래그를 확인합니다. False이면 데이터를 채우고 플래그를 true로 설정합니다. 사실이라면 데이터가 있다고 가정하면 계속 진행됩니다. 다른 전제 조건과 동일한 작업을 수행 할 수 있습니다. 생성자 또는 기본값으로 플래그를 설정하십시오. 그런 다음 일부 사전 조건 함수가 호출되어야 할 지점에 도달했을 때 아직 호출되지 않은 경우 호출하십시오. 물론이 시점에서 호출 할 데이터가있는 경우에만 작동하지만 많은 경우 ""의 함수 매개 변수에 필요한 데이터를 포함 할 수 있습니다

재 컴파일 타임 :

다른 사람들이 말했듯이 객체를 사용하기 전에 초기화 해야하는 경우 초기화를 생성자에 넣습니다. 이것은 OOP에 대한 일반적인 조언입니다. 가능하면 생성자가 유효하고 사용 가능한 개체 인스턴스를 생성하도록해야합니다.

객체가 초기화되기 전에 객체에 대한 참조를 전달 해야하는 경우 수행 할 작업에 대한 토론이 있습니다. 나는 머리 꼭대기에서 그 실제 사례를 생각할 수는 없지만 일어날 수 있다고 생각합니다. 솔루션이 제안되었지만 여기에서 본 솔루션과 내가 생각할 수있는 솔루션은 지저분하고 코드가 복잡합니다. 어떤 시점에서, 런타임 검사보다는 컴파일 타임 검사를 할 수 있도록 추악하고 이해하기 어려운 코드를 만드는 것이 가치가 있습니까? 아니면 좋은 목표는 아니지만 자신과 다른 사람들을 위해 많은 일을하고 있습니까?

두 기능을 항상 함께 실행해야하는 경우 간단한 해결책은 하나의 기능으로 만드는 것입니다. 광고를 페이지에 추가 할 때마다 해당 페이지에 대한 메트릭 핸들러도 추가 한 다음 "광고 추가"기능 내에 메트릭 핸들러를 추가하는 코드를 추가하십시오.

컴파일 타임에 확인하는 것이 거의 불가능합니다. 특정 함수를 두 번 이상 호출 할 수 없다는 요구 사항과 같습니다. (저는 그와 같은 많은 함수를 작성했습니다. 최근에 매직 디렉토리에서 파일을 찾고 찾은 파일을 처리 한 다음 삭제하는 함수를 작성했습니다. 물론 파일을 삭제 한 후에는 다시 실행할 수 없습니다. 등) 나는 컴파일 타임에 같은 인스턴스에서 함수가 두 번 호출되는 것을 막을 수있는 기능이있는 언어를 모른다. 컴파일러가 그 일이 일어나고 있음을 확실하게 알 수있는 방법을 모르겠습니다. 런타임 검사 만하면됩니다. 특히 런타임 검사는 분명하지 않습니까? "이미 존재 한 경우 = true이면 예외를 처리하고 그렇지 않으면 이미 존재 한 경우 = true로 설정하십시오".

시행 불가능

클래스가 완료되면 파일이나 연결을 닫고, 메모리를 비우고, 최종 결과를 DB에 쓰는 등의 정리를 요구하는 것은 드문 일이 아닙니다. 컴파일 타임에 강제로 발생하는 쉬운 방법은 없습니다. 또는 런타임. 대부분의 OOP 언어에는 인스턴스가 가비지 수집 될 때 호출되는 "완료 자"함수에 대한 일종의 프로비저닝이 있다고 생각하지만, 이것이 실행될 것이라고 보장 할 수는 없다고 생각합니다. OOP 언어에는 종종 "use"또는 "with"절과 인스턴스 사용에 대한 범위를 설정하기위한 일종의 규정과 함께 "dispose"함수가 포함되어 있으며 프로그램이 해당 범위를 종료하면 처리가 실행됩니다. 그러나이를 위해서는 프로그래머가 적절한 범위 지정자를 사용해야합니다. 프로그래머가 제대로하도록 강요하지는 않습니다.

프로그래머가 클래스를 올바르게 사용하도록 강제 할 수없는 경우 잘못된 결과를 제공하지 않고 프로그램이 잘못 수행되도록 프로그램을 종료하려고합니다. 간단한 예 : 많은 데이터를 더미 값으로 초기화하는 많은 프로그램을 보았으므로 사용자가 데이터를 올바르게 채우는 함수를 호출하지 않아도 null 포인터 예외가 발생하지 않습니다. 그리고 나는 항상 왜 궁금합니다. 버그를 찾기가 더 어려워지고 있습니다. 프로그래머가 "데이터로드"기능을 호출하지 못하고 데이터를 사용하려고 할 때 널 포인터 예외가 발생하면 예외는 문제의 위치를 ​​신속하게 보여줍니다. 그러나 이러한 경우 데이터를 비워 두거나 0으로 표시하여 숨기면 프로그램이 완료 될 수 있지만 결과가 정확하지 않을 수 있습니다. 경우에 따라 프로그래머는 문제가 있음을 인식하지 못할 수도 있습니다. 그가 알아 차리면, 길을 잘못 따라 가기 위해 긴 길을 따라야 할 수도 있습니다. 용감하게 수행하는 것보다 일찍 실패하는 것이 좋습니다.

일반적으로 객체를 잘못 사용할 수없는 경우 항상 좋습니다. 프로그래머가 단순히 유효하지 않은 호출을 할 수있는 방법이 없거나 유효하지 않은 호출이 컴파일 타임 오류를 생성하는 방식으로 함수가 구조화되어 있으면 좋습니다.

그러나 프로그래머가 함수에 대한 문서를 읽어야 할 시점이 있습니다.


1
정리 강제와 관련하여 특정 메소드 (예 : 일반적인 OO 언어에서는 개인 생성자를 가질 수 있음)를 호출하여 자원을 할당 할 수있는 패턴을 사용할 수 있습니다. 이 메서드는 리소스를 할당하고 리소스를 참조하여 리소스에 전달 된 함수 (또는 인터페이스의 메서드)를 호출 한 다음이 함수가 반환되면 리소스를 삭제합니다. 갑작스러운 스레드 종료 외에이 패턴은 리소스가 항상 소멸되도록합니다. 기능적 언어에서 일반적인 접근 방식이지만 OO 시스템에서 효과가 좋은 것으로 나타났습니다.
Jules


2

그렇습니다. 본능이 정해져 있습니다. 몇 년 전 저는 디버깅 , 애비 게이션안티 버깅에 대해 다루는 잡지 (이곳과 비슷한 종류이지만 죽은 나무에 매달 한 번 씩 내림 )에 기사를 썼습니다 .

컴파일러가 오용을 잡을 수 있다면 이것이 가장 좋은 방법입니다. 사용 가능한 언어 기능은 언어에 따라 다르며 지정하지 않았습니다. 어쨌든이 사이트의 개별 질문에 대한 구체적인 기술이 자세히 설명되어 있습니다.

그러나 런타임 대신 컴파일 타임에 사용량을 감지하는 테스트는 실제로 동일한 종류의 테스트입니다. 여전히 올바른 함수를 먼저 호출하고 올바른 방식으로 사용해야한다는 것을 알아야합니다.

처음에는 문제가되는 것을 피하기 위해 구성 요소를 디자인하는 것이 훨씬 미묘하며 버퍼 예제 는 요소 예제 와 같습니다 . Part 4 (20 년 전에 게시되었습니다! 와우!)에는 사용자의 경우와 매우 유사한 토론이 포함 된 예제가 포함되어 있습니다. read () 사용을 시작한 후 buffer ()를 호출 할 수 없으므로이 클래스를주의해서 사용해야합니다. 오히려 시공 직후 한 번만 사용해야합니다. 호출자가 클래스를 자동 및 보이지 않게 버퍼를 관리하게하면 개념적으로 문제를 피할 수 있습니다.

적절한 사용을 보장하기 위해 런타임에 테스트를 수행 할 수 있는지 물어보아야합니까?

나는 그렇다고 말할 것이다 . 코드가 유지되고 특정 사용법이 혼란스러워지면 팀이 언젠가는 많은 디버깅 작업을 줄일 수 있습니다. 테스트는 테스트 할 수있는 공식적인 제약 조건불변 값으로 설정 될 수 있습니다. 그렇다고해서 상태를 추적하고 점검을 수행하기위한 추가 공간의 오버 헤드가 항상 모든 통화에 남아 있음을 의미하지는 않습니다. 클래스에 있으므로 호출 할 있으며 코드는 실제 제약 조건과 가정을 문서화합니다.

아마도 검사는 디버그 빌드에서만 수행됩니다.

아마도 검사는 복잡하지만 (예를 들어 힙 검사를 생각하면) 존재 하고 잠시 동안 수행 할 수 있거나 코드가 작업 중이거나 문제가 발생하거나 여기에 특정 테스트를 수행 할 때 여기 저기 호출을 추가 할 수 있습니다 동일한 클래스에 연결 되는 단위 테스트 또는 통합 테스트 프로그램에 이러한 문제가 없는지 확인하십시오 .

결국 오버 헤드가 사소한 것이라고 결정할 수 있습니다. 클래스가 파일 I / O를 수행하고 여분의 상태 바이트 및 그러한 테스트는 아무것도 아닙니다. 일반적인 iostream 클래스는 스트림이 잘못된 상태인지 확인합니다.

당신의 본능은 좋습니다. 유지하십시오!


0

정보 숨기기 (봉지)의 원칙은 클래스 외부의 엔터티가이 클래스를 올바르게 사용하는 데 필요한 것 이상을 알아야한다는 것입니다.

귀하의 사례는 외부 사용자에게 클래스에 대해 충분히 알리지 않고 클래스 객체를 올바르게 실행하려고하는 것처럼 보입니다. 필요한 것보다 더 많은 정보를 숨겼습니다.

  • 생성자에서 필요한 정보를 명시 적으로 요청하십시오.
  • 또는 생성자를 그대로 유지하고 메소드를 수정하여 메소드를 호출하려면 누락 된 데이터를 제공해야합니다.

지혜의 말씀:

* 어느 쪽이든 클래스 및 / 또는 생성자 및 / 또는 메서드를 다시 디자인해야합니다.

* 올바른 정보를 바탕으로 수업을 제대로 설계하지 않고 굶주 리면 수업이 한 곳이 아닌 여러 곳에서 중단 될 위험이 있습니다.

* 예외와 오류 메시지를 잘못 작성했다면, 수업은 그 자체에 대해 더 많은 것을 줄 것입니다.

* 마지막으로, 외부인이 a, b 및 c를 초기화 한 다음 d (), e (), f (int)를 호출해야한다는 사실을 알고 있어야하는 경우 추상화에 누출이 발생합니다.


0

C #을 사용하도록 지정 했으므로 상황에 적합한 솔루션은 Roslyn Code Analyzer 를 활용하는 것 입니다. 이를 통해 위반 사항을 즉시 포착하고 코드 수정제안 할 수 있습니다 .

이를 구현하는 한 가지 방법은 시간적으로 결합 된 메소드를 메소드를 호출해야하는 순서를 지정하는 속성으로 장식하는 것입니다 1 . 분석기가 클래스에서 이러한 속성을 찾으면 메소드가 순서대로 호출되는지 검증합니다. 그러면 수업이 다음과 같이 보일 것입니다.

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1 : Roslyn Code Analyzer를 작성한 적이 없으므로 이것이 최선의 구현이 아닐 수도 있습니다. Roslyn Code Analyzer를 사용하여 API가 올바르게 사용되고 있는지 확인하는 아이디어는 100 % 소리입니다.


-1

이전 에이 문제를 해결 한 방법은 개인 생성자와 MakeFooManager()클래스 내 정적 메서드를 사용하는 것입니다. 예:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

생성자가 구현되었으므로 누구나을 FooManager거치지 않고 인스턴스를 만들 수있는 방법이 없습니다 MakeFooManager().


1
이것은 단지 몇 시간 전에 게시 된 이전 답변 에서 반복적으로 언급되고 설명 된 것처럼 보인다
gnat

1
이것은 ctor에서 단순히 null 검사보다 이점이 있습니까? 다른 방법을 래핑하는 것 외에는 아무것도하지 않는 방법을 만든 것 같습니다. 왜?
sara

또한 왜 foo와 bar 멤버 변수가 있습니까?
Patrick M

-1

몇 가지 접근 방식이 있습니다.

  1. 수업을 올바르게 사용하기 쉽고 잘못 사용하기 어렵게 만드십시오. 다른 답변 중 일부는이 시점에만 초점을 맞추기 때문에 RAII빌더 패턴을 간단히 언급하겠습니다 .

  2. 주석 사용 방법에 유의하십시오. 수업이 자체 설명 인 경우 의견을 쓰지 마십시오. * 그렇게하면 사람들이 실제로 수업에 필요할 때 의견을 읽을 가능성이 높습니다. 클래스를 올바르게 사용하기가 어렵고 주석이 필요한 드문 경우 중 하나가 있다면 예제를 포함하십시오.

  3. 디버그 빌드에서 assert 를 사용하십시오 .

  4. 클래스 사용법이 유효하지 않은 경우 예외를 처리하십시오.

* 이러한 말은 쓰지 말아야합니다.

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