생성자에서 setter를 사용하는 것이 일반적인 패턴이 아닌 이유는 무엇입니까?


23

접근 자와 수정 자 (일명 setter 및 getter)는 세 가지 주요 이유로 유용합니다.

  1. 변수에 대한 액세스를 제한합니다.
    • 예를 들어 변수에 액세스 할 수는 있지만 수정할 수는 없습니다.
  2. 그들은 매개 변수의 유효성을 검사합니다.
  3. 부작용이 발생할 수 있습니다.

웹의 대학교, 온라인 강좌, 튜토리얼, 블로그 기사 및 코드 예제는 모두 접근 자와 수정 자의 중요성에 대해 강조하고 있으며, 요즘에는 코드에 대해 "필수"인 것 같습니다. 따라서 아래 코드와 같이 추가 값을 제공하지 않아도 찾을 수 있습니다.

public class Cat {
    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

즉, 더 유용한 수정자를 찾는 것이 일반적입니다. 실제로 매개 변수의 유효성을 검사하고 유효하지 않은 입력이 제공된 경우 예외를 발생 시키거나 부울을 반환하는 수정자는 다음과 같습니다.

/**
 * Sets the age for the current cat
 * @param age an integer with the valid values between 0 and 25
 * @return true if value has been assigned and false if the parameter is invalid
 */
public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

그러나 그때조차도 수정자가 생성자에서 호출되는 것을 거의 보지 못하므로 내가 직면 한 간단한 클래스의 가장 일반적인 예는 다음과 같습니다.

public class Cat {
    private int age;

    public Cat(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

그러나이 두 번째 접근 방식이 훨씬 안전하다고 생각할 것입니다.

public class Cat {
    private int age;

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

당신의 경험에 비슷한 패턴이 보입니까, 아니면 단지 운이 좋지 않습니까? 그리고 그렇게한다면, 그 원인이 무엇이라고 생각하십니까? 생성자에서 수정자를 사용하는 데 명백한 단점이 있습니까? 아니면 더 안전한 것으로 간주됩니까? 다른 것입니까?


1
@stg, 감사합니다, 재미있는 포인트! 그것을 확장하고 아마도 일어날 수있는 나쁜 일의 예를 들어 대답에 넣을 수 있습니까?
Vlad Spreys


8
@stg 실제로 생성자에서 재정의 가능한 메서드를 사용하는 것은 안티 패턴이며 많은 코드 품질 도구가이를 오류로 지적합니다. 당신은 재정의 된 버전이 무엇을하는지 모른다. 그리고 생성자가 끝나기 전에 "this"를 빠져 나가게하는 것과 같은 불쾌한 일을 할 수있다.
Michał Kosmulski

1
@ gnat : 이것이 복제본이라고 생각하지 않습니다 . 클래스의 메소드가 자체 getter 및 setter를 호출해야합니까? 완전히 생성되지 않은 객체에서 생성자 호출이 호출되는 특성 때문입니다.
Greg Burghardt

3
또한 "유효성 검사"는 연령을 초기화하지 않은 상태로 둡니다 (또는 언어에 따라 0 또는 다른 것). 생성자가 실패하도록하려면 반환 값을 확인하고 실패시 예외를 발생시켜야합니다. 알 수 있듯이 유효성 검사에서 다른 버그가 발생했습니다.
쓸모없는

답변:


37

매우 일반적인 철학적 추론

일반적으로 생성자는 생성 된 객체의 상태에 대한 보증을 사후 조건으로 제공해야합니다.

일반적으로 인스턴스 메소드는 호출 할 때 이러한 보장이 이미 보유하고 있다고 가정하고 사전 조건으로 가정 할 수 있으며이를 중단하지 않아야합니다.

생성자 내부에서 인스턴스 메서드를 호출하면 해당 보증 중 일부 또는 전부가 아직 설정되지 않았으므로 인스턴스 메서드의 사전 조건이 충족되는지 여부를 추론하기가 어렵습니다. 예를 들어, 제대로 이해하더라도 매우 취약 할 수 있습니다. 인스턴스 메소드 호출 또는 기타 작업의 순서를 다시 지정합니다.

언어는 생성자가 여전히 실행되는 동안 기본 클래스에서 상속되거나 하위 클래스로 재정의되는 인스턴스 메서드에 대한 호출을 해결하는 방법도 다양합니다. 이것은 또 다른 복잡성 계층을 추가합니다.

구체적인 예

  1. 이것이 어떻게 보일지에 대한 자신의 예는 그 자체가 잘못되었습니다.

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    의 반환 값을 확인하지 않습니다 setAge. 분명히 setter를 호출한다고해서 정확성이 보장되는 것은 아닙니다.

  2. 초기화 순서에 따라 다음과 같은 매우 쉬운 실수 :

    class Cat {
      private Logger m_log;
      private int m_age;
    
      public void setAge(int age) {
        // FIXME temporary debug logging
        m_log.write("=== DEBUG: setting age ===");
        m_age = age;
      }
    
      public Cat(int age, Logger log) {
        setAge(age);
        m_log = log;
      }
    };

    내 임시 벌목이 모든 것을 깨뜨 렸어요 으악!

생성자에서 setter를 호출하면 기본 초기화 낭비를 의미하는 C ++과 같은 언어도 있습니다 (일부 구성원 변수의 경우 피할 가치가 있음)

간단한 제안

대부분의 코드 이런 식으로 작성 되지 않았지만 생성자를 깨끗하고 예측 가능하게 유지하고 사전 및 사후 조건 논리를 재사용하려는 경우 더 나은 솔루션은 다음과 같습니다.

class Cat {
  private int m_age;
  private static void checkAge(int age) {
    if (age > 25) throw CatTooOldError(age);
  }

  public void setAge(int age) {
    checkAge(age);
    m_age = age;
  }

  public Cat(int age) {
    checkAge(age);
    m_age = age;
  }
};

또는 가능하면 더 좋을 수도 있습니다. 속성 유형으로 제약 조건을 인코딩하고 할당시 자체 값을 확인하도록하십시오.

class Cat {
  private Constrained<int, 25> m_age;

  public void setAge(int age) {
    m_age = age;
  }

  public Cat(int age) {
    m_age = age;
  }
};

그리고 마지막으로, 완전성을 위해 C ++의 자체 유효성 검사 래퍼입니다. 여전히 지루한 유효성 검사를 수행하고 있지만이 클래스는 다른 작업을 수행하지 않기 때문에 비교적 쉽게 확인할 수 있습니다.

template <typename T, T MAX, T MIN=T{}>
class Constrained {
    T val_;
    static T validate(T v) {
        if (MIN <= v && v <= MAX) return v;
        throw std::runtime_error("oops");
    }
public:
    Constrained() : val_(MIN) {}
    explicit Constrained(T v) : val_(validate(v)) {}

    Constrained& operator= (T v) { val_ = validate(v); return *this; }
    operator T() { return val_; }
};

자, 완전하지는 않습니다. 다양한 복사 및 이동 생성자와 할당을 생략했습니다.


4
심하게 응답 한 답변! OP가 교과서에 설명 된 상황을 보았 기 때문에 좋은 해결책이되지 않기 때문에 추가하겠습니다. 클래스에 속성을 설정하는 두 가지 방법이 있고 (생성자 및 설정자에 의해) 해당 속성에 대한 제약 조건을 적용하려는 경우 두 방법 모두 제약 조건을 확인해야합니다. 그렇지 않으면 디자인 버그 입니다.
Doc Brown

따라서 1) setter에 논리가 없거나 2) setter의 논리가 상태와 무관 한 경우 생성자에서 setter 메소드를 사용하는 것이 좋습니다 . 항상 멤버 변수에 적용해야 세터의에 조건의 몇 가지 유형이있을 때 (나는 종종 그것을 해달라고하지만 난 개인적으로 정확히 후자의 이유 생성자에서 사용되는 setter 메소드를 가지고
크리스 Maggiulli

반드시 항상 적용 할 것을 조건으로 어떤 종류가있는 경우, 그 입니다 세터의 논리. 이 논리를 멤버로 인코딩 하는 것이 가능 하면 더 좋을 것이라고 생각합니다 . 따라서 자체적으로 포함되어야하며 나머지 클래스에 대한 종속성을 얻을 수 없습니다.
쓸모없는

13

오브젝트가 구성 될 때, 정의에 의해 완전히 형성되지 않습니다. 세터가 제공 한 방법을 고려하십시오.

public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

유효성 검사의 일부에 개체의 나이 만 증가 할 수 있도록 속성에 setAge()대한 검사가 포함 된 경우 age어떻게됩니까? 그 상태는 다음과 같습니다.

if (age > this.age && age < 25) {
    //...

고양이는 한 방향으로 만 시간을 움직일 수 있기 때문에 꽤 순진한 변화처럼 보입니다. 유일한 문제는 이미 유효한 값이 this.age있다고 가정하기 때문에 초기 값을 설정하는 데 사용할 수 없다는 것 this.age입니다.

생성자에서 setter를 피 하면 생성자가 수행 하는 유일한 작업은 인스턴스 변수를 설정하고 setter에서 발생하는 다른 동작을 건너 뛰는 것입니다.


좋아 ...하지만 실제로 객체 구성을 테스트하지 않고 잠재적 인 버그를 노출시키지 않으면 잠재적 인 버그가 있습니다 . 그리고 이제 두 가지 문제가 있습니다. 숨겨진 잠재적 버그가 있고 두 범위에서 연령 범위 검사를 시행해야합니다.
svidgen

Java / C #에서 생성자 호출 전에 모든 필드가 0으로 설정되므로 문제가 없습니다.
중복 제거기

2
예, 생성자에서 인스턴스 메소드를 호출하는 것은 추론하기 어려울 수 있으며 일반적으로 바람직하지 않습니다. 이제 유효성 검사 논리를 개인의 최종 클래스 / 정적 메서드로 옮기고 생성자와 setter에서 호출하려면 괜찮을 것입니다.
쓸모없는

1
아니요, 인스턴스가 아닌 메소드는 부분적으로 구성된 인스턴스를 건드리지 않기 때문에 인스턴스 메소드의 까다로운 문제를 피합니다. 물론, 검증 결과를 확인하여 시공 성공 여부를 결정해야합니다.
쓸모없는

1
이 대답은 IMHO가 요점을 놓친 것입니다. "객체가 구성 될 때는 정의에 의해 완전히 형성되지 않습니다" -구성 과정의 중간에 물론 생성자가 완료 될 때 속성에 대한 의도 된 제한 조건이 있어야합니다. 그렇지 않으면 해당 제한 조건을 피할 수 있습니다. 나는 이것을 심각한 디자인 결함이라고 부를 것이다.
Doc Brown

6

여기에 좋은 대답이 있지만 지금까지 아무도 당신이 여기에 두 가지를 요구하는 것을 보지 못했습니다. IMHO 게시물은 다음 두 가지 질문 으로 가장 잘 나타납니다 .

  1. setter에 제약 조건의 유효성 검사가있는 경우이를 무시할 수 없도록 클래스의 생성자에도 있지 않아야합니까?

  2. 왜 생성자 로부터이 목적으로 setter를 직접 호출하지 않습니까?

첫 번째 질문에 대한 대답은 분명히 "예"입니다. 예외를 발생시키지 않는 생성자는 객체를 유효한 상태로 두어야하며 특정 속성에 대해 특정 값이 금지 된 경우 ctor가이를 우회하도록하는 것은 전혀 의미가 없습니다.

그러나 파생 클래스에서 setter를 재정의하지 않도록 조치를 취하지 않는 한 두 번째 질문에 대한 대답은 일반적으로 "아니오"입니다. 따라서 더 나은 대안은 생성자뿐만 아니라 setter에서 재사용 할 수있는 전용 메소드에서 제한 조건 유효성 검증을 구현하는 것입니다. 나는이 디자인을 정확하게 보여주는 @Useless 예제를 반복하지 않을 것입니다.


생성자가 논리를 사용할 수 있도록 setter에서 논리를 추출하는 것이 더 나은 이유를 여전히 이해하지 못합니다. ... 어떻게이는 정말 단순히 합리적인 위해 setter를 호출하는 것보다 다른? 특히 세터가 "
필수

2
@ svidgen : JimmyJames 예제 참조 : 간단히 말하면 ctor에서 재정의 가능한 메소드를 사용하는 것은 좋지 않습니다. 문제가 발생할 수 있습니다. cter에서 setter를 사용하는 것이 최종적이고 다른 재정의 가능한 메소드를 호출하지 않으면 사용할 수 있습니다. 또한 실패시 처리 방법에서 유효성 검증을 분리하면 ctor 및 setter에서 다르게 처리 할 수 ​​있습니다.
Doc Brown

글쎄, 나는 지금 덜 확신합니다! 그러나 다른 답변을 알려 주셔서 감사합니다 ...
svidgen

2
하여 합리적인 위해취성으로, 눈에 보이지 않는 주문 의존성 쉽게 무고한적인 변화에 의해 파손 , 맞죠?
쓸모없는

@ 쓸모없는, 아니 ... 내 말은, 당신이 코드를 실행하는 순서가 중요하지 않다는 것을 제안하는 것이 재미있다. 그러나 그것은 요점이 아닙니다. 일 리드의 종류 - 인위적인 예를 넘어, 난 그냥 그것을 세터에서 크로스 속성 검증해야 할 좋은 아이디어라고 생각했습니다 내 경험의 예를 불러올 수 없습니다 반드시 "보이지 않는"호출 순서 요구 사항을 . 이러한 호출이 생성자에서 발생하는지 여부에 관계없이 ..
svidgen

2

다음은 생성자에서 최종이 아닌 메소드를 사용하여 발생할 수있는 문제의 종류를 보여주는 바보 같은 Java 코드입니다.

import java.util.regex.Pattern;

public class Problem
{
  public static final void main(String... args)
  {
    Problem problem = new Problem("John Smith");
    Problem next = new NextProblem("John Smith", " ");

    System.out.println(problem.getName());
    System.out.println(next.getName());
  }

  private String name;

  Problem(String name)
  {
    setName(name);
  }

  void setName(String name)
  {
    this.name = name;
  }

  String getName()
  {
    return name;
  }
}

class NextProblem extends Problem
{
  private String firstName;
  private String lastName;
  private final String delimiter;

  NextProblem(String name, String delimiter)
  {
    super(name);

    this.delimiter = delimiter;
  }

  void setName(String name)
  {
    String[] parts = name.split(Pattern.quote(delimiter));

    this.firstName = parts[0];
    this.lastName = parts[1];
  }

  String getName()
  {
    return firstName + " " + lastName;
  }
}

이것을 실행하면 NextProblem 생성자에서 NPE를 얻습니다. 이것은 물론 사소한 예이지만 상속 수준이 여러 개인 경우 상황이 빠르게 복잡해질 수 있습니다.

이것이 일반적이지 않은 더 큰 이유는 코드를 이해하기 어렵게 만들기 때문입니다. 개인적으로, 나는 setter 메소드를 거의 사용하지 않았고 멤버 변수는 거의 변하지 않습니다 (pun 예정). 따라서 생성자에서 값을 설정해야합니다. 불변의 객체를 사용하는 경우 (그리고 그렇게해야 할 많은 이유가있는 경우) 문제는 무의미합니다.

어쨌든 유효성 검사 또는 다른 논리를 재사용하는 것이 좋은 목표이며이를 정적 메서드에 넣고 생성자와 설정자에서 호출 할 수 있습니다.


상속이 잘못 구현 된 문제 대신 생성자에서 setter를 사용하는 문제는 무엇입니까 ??? 다시 말해, 기본 생성자를 효과적으로 무효화하는 이유는 무엇입니까?
svidgen

1
요점은 세터를 재정의하면 생성자를 무효화해서는 안된다는 것입니다.
Chris Wohlert

@ChrisWohlert Ok ... 그러나 생성자 논리와 충돌하도록 setter 논리를 변경하면 생성자가 setter를 사용하는지 여부에 관계없이 문제가됩니다. 건설 시간에 실패하거나 나중에 실패하거나 보이지 않게 실패합니다 (비즈니스 규칙을 우회 한 b / c).
svidgen

기본 클래스의 생성자가 어떻게 구현되는지 모르는 경우가 있습니다 (어딘가에 타사 라이브러리에있을 수 있음). 재정의 가능한 메소드를 재정의하더라도 기본 구현 계약을 위반해서는 안됩니다 (그리고이 예제에서는 name필드를 설정하지 않음 ).
Hulk

@ svidgen 여기서 문제는 생성자에서 재정의 가능한 메서드를 호출하면 재정의의 유용성이 감소한다는 것입니다. 하위 클래스의 필드는 아직 초기화되지 않았으므로 재정의 된 버전에서 사용할 수 없습니다. 더 나쁜 것은, 당신은 통과해서는 안됩니다this 아직 완전히 초기화되었는지 확신 할 수 없기 때문에 다른 방법에 .
Hulk

1

그것은 달려있다!

속성을 설정할 때마다 적중해야하는 setter에서 간단한 유효성 검사를 수행하거나 나쁜 부작용을 일으키는 경우 setter를 사용하십시오. 대안은 일반적으로 setter에서 setter 로직을 추출하고 setter 생성자 모두에서 실제 set이 뒤 따르는 새 메소드를 호출하는 것입니다. 실제로 setter 를 사용하는 것과 동일합니다! (지금은 두 곳에서 작고 두 줄짜리 세터를 유지 관리하는 것을 제외하고)

그러나 특정 setter (또는 2!)가 성공적으로 실행 되기 전에 객체를 정렬하기 위해 복잡한 작업을 수행해야하는 경우 setter를 사용하지 마십시오.

두 경우 모두 테스트 사례가 있습니다. 어느 경로로 이동하든 단위 테스트를 수행하면 두 옵션 중 하나와 관련된 함정을 노출시킬 수 있습니다.


또한 저 멀리에서 내 기억이 원격으로 정확하다면, 이것은 내가 대학에서 가르친 일종의 추론이기도합니다. 그리고 내가 일한 특권을 얻은 성공적인 프로젝트와 전혀 관련이 없습니다.

추측해야 할 경우, 생성자에서 setter를 사용하여 발생할 수있는 일반적인 버그를 식별 한 "깨끗한 코드"메모가 누락되었습니다. 그러나 내 입장으로는 그러한 버그에 희생되지 않았습니다 ...

그래서 저는이 특정한 결정이 쓸데없고 독단적 일 필요는 없다고 주장합니다.

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