'최종'키워드가 유용한 이유는 무엇입니까?


54

Java는 오랫동안 클래스를 파생 할 수없는 클래스를 선언 할 수있는 힘을 가지고 있으며 C ++에도 클래스가 있습니다. 그러나 SOLID의 열기 / 닫기 원칙에 비추어 볼 때 이것이 왜 유용한가? 나에게 final키워드는 똑같이 들리지만 friend합법적이지만 키워드 를 사용하는 경우 디자인이 잘못되었을 수 있습니다. 파생 할 수없는 클래스가 훌륭한 아키텍처 또는 디자인 패턴의 일부가되는 몇 가지 예를 제공하십시오.


43
클래스에 주석이 달린 경우 클래스가 잘못 설계되었다고 생각하는 이유는 무엇 final입니까? 저를 포함한 많은 사람들이 모든 비 추상적 인 수업을 만드는 것이 좋은 디자인이라는 것을 알게되었습니다 final.
발견

20
상속보다 구성을 선호하고 모든 비 추상 클래스를 가질 수 있습니다 final.
Andy

24
오픈 / 클로즈 원칙은 어떤 의미에서 만트라가 다른 클래스에서 상속 된 클래스에서 상속 된 클래스의 계층 구조를 만드는 20 세기의 시대에 뒤 떨어진 것이다. 이것은 객체 지향 프로그래밍을 가르치는 데 좋지만 실제 문제에 적용될 때 엉키고 유지할 수없는 혼란을 일으키는 것으로 밝혀졌습니다. 확장 가능한 클래스를 디자인하는 것은 어렵습니다.
David Hammen

34
@DavidArno 말도 안돼. 상속은이다 사인 ...로서 비 객체 지향 프로그래밍 및 특정 지나치게 독단적 인 개인이 설교 싶은대로 아무데도 근처와 같이 복잡하거나 지저분한입니다. 그것은 다른 도구와 마찬가지로 도구이며 훌륭한 프로그래머는 작업에 적합한 도구를 사용하는 방법을 알고 있습니다.
메이슨 휠러

48
회복력있는 개발자로서, 필자는 최종적으로 유해한 행동이 중요한 부분에 들어가는 것을 막는 훌륭한 도구라는 것을 기억합니다. 또한 상속은 다양한 방법으로 강력한 도구라는 것을 기억합니다. 거의 것처럼입니다 말하다 다른 도구는 장단점을 가지고 엔지니어로 우리는 우리가 우리의 소프트웨어를 생산으로 그 요소의 균형을 가지고!
corsiKa

답변:


136

final 의도를 표현 합니다. 이 클래스는 사용자에게 클래스, 메소드 또는 변수를 알려줍니다. "이 요소는 변경되어서는 안되며 변경하려는 경우 기존 디자인을 이해하지 못했습니다."

이것은 모든 클래스와 작성한 모든 메소드 가 서브 클래스에 의해 완전히 다른 것을 수행하도록 변경 될 것으로 예상해야한다면 프로그램 아키텍처가 실제로 매우 어려울 것이기 때문에 중요 합니다. 어떤 요소가 변경 가능하고 변경되지 않아야하는지 미리 결정하고를 통해 변경 불가능 성을 강화하는 것이 훨씬 좋습니다 final.

주석과 아키텍처 문서를 통해이 작업을 수행 할 수도 있지만, 향후 사용자가 문서를 읽고 준수하기를 기대하는 것보다 컴파일러가 가능한 일을 강제로 수행하도록하는 것이 좋습니다.


14
그러나 프레임 워크 나 미디어 라이브러리와 같이 널리 재사용되는 기본 클래스를 작성한 사람은 응용 프로그램 프로그래머가 제정신의 방식으로 행동하는 것보다 더 잘 알고 있습니다. 철제 손잡이로 잠그지 않으면 불가능 하다고 생각했던 방식으로 발명품을 파괴, 오용 및 왜곡시킬 수 있습니다 .
Kilian Foth

8
@KilianFoth Ok, 솔직히 말해서, 당신의 문제는 어떻게 응용 프로그램 프로그래머가 하는가?
coredump

20
@coredump 내 라이브러리를 사용하는 사람들은 나쁜 시스템을 잘못 만듭니다. 나쁜 시스템은 나쁜 평판을 낳습니다. 사용자는 Kilian의 훌륭한 코드를 Fred Random의 엄청나게 불안정한 앱과 구별 할 수 없습니다. 결과 : 나는 프로그래밍 신념과 고객을 잃는다. 코드를 잘못 사용하기 어렵게 만드는 것은 달러와 센트의 문제입니다.
Kilian Foth

21
"이 요소는 변경되지 않아야하며 변경하려는 경우 기존 디자인을 이해하지 못했습니다." 엄청나게 거만하고 내가 함께 일한 도서관의 일부로 원하는 태도가 아닙니다. 나는 모든 시간을 한푼도했다 경우에만 일부 안되게 이상 - 캡슐화 된 라이브러리는 내부 상태의 일부 조각을 변경하는 메커니즘으로 나를 남겨 변경해야을 저자가 중요한 유스 케이스 ... 예상하지 못했기 때문에
메이슨 윌러

31
@JimmyB 경험의 법칙 : 자신의 창작물이 무엇인지 알고 있다고 생각한다면 이미 틀린 것입니다. Bell은 전화를 본질적으로 Muzak 시스템으로 생각했습니다. Kleenex는 메이크업을보다 편리하게 제거하기 위해 발명되었습니다. IBM의 토마스 왓슨 (Thomas Watson) 회장은 "5 대의 컴퓨터를위한 세계 시장이 있다고 생각한다"고 말했다. 2001 년에 PATRIOT Act를 도입 한 Jim Sensenbrenner 대표 는 NSA가 "PATRIOT act authority"를 통해 자신이하고있는 일 을 하지 못하도록 특별히 고안된 것으로 기록되어 있습니다. 그리고 등등 ...
메이슨 휠러

59

취약한 기본 클래스 문제를 피합니다 . 모든 클래스에는 암시 적 또는 명시 적 보증 및 불변이 있습니다. Liskov 대체 원칙은 해당 클래스의 모든 하위 유형도 이러한 보증을 모두 제공해야합니다. 그러나를 사용하지 않으면이를 위반하는 것이 정말 쉽습니다 final. 예를 들어, 암호 검사기를 보자 :

public class PasswordChecker {
  public boolean passwordIsOk(String password) {
    return password == "s3cret";
  }
}

해당 클래스를 재정의하는 경우 한 구현은 모든 사람을 잠글 수 있고 다른 구현은 모든 사람에게 액세스 권한을 부여 할 수 있습니다.

public class OpenDoor extends PasswordChecker {
  public boolean passwordIsOk(String password) {
    return true;
  }
}

서브 클래스는 이제 원본과 매우 호환되지 않는 동작을 갖기 때문에 일반적으로 OK가 아닙니다. 만약 우리가 수업을 다른 행동으로 확장하고자한다면, 책임의 사슬이 더 나을 것입니다 :

PasswordChecker passwordChecker =
  new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
  new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
 new DefaultPasswordChecker(
   new OpenDoor(null)
 );

public interface PasswordChecker {
  boolean passwordIsOk(String password);
}

public final class DefaultPasswordChecker implements PasswordChecker {
  private PasswordChecker next;

  public DefaultPasswordChecker(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    if ("s3cret".equals(password)) return true;
    if (next != null) return next.passwordIsOk(password);
    return false;
  }
}

public final class OpenDoor implements PasswordChecker {
  private PasswordChecker next;

  public OpenDoor(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    return true;
  }
}

더 복잡한 클래스가 자체 메서드를 호출하면 해당 메서드가 재정의 될 수 있습니다. 데이터 구조를 예쁘게 인쇄하거나 HTML을 작성할 때 때때로이 문제가 발생합니다. 각 메소드는 일부 위젯을 담당합니다.

public class Page {
  ...;

  @Override
  public String toString() {
    PrintWriter out = ...;
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    writeHeader(out);
    writeMainContent(out);
    writeMainFooter(out);
    out.print("</body>");

    out.print("</html>");
    ...
  }

  void writeMainContent(PrintWriter out) {
    out.print("<div class='article'>");
    out.print(htmlEscapedContent);
    out.print("</div>");
  }

  ...
}

이제 좀 더 스타일을 추가하는 하위 클래스를 만듭니다.

class SpiffyPage extends Page {
  ...;


  @Override
  void writeMainContent(PrintWriter out) {
    out.print("<div class='row'>");

    out.print("<div class='col-md-8'>");
    super.writeMainContent(out);
    out.print("</div>");

    out.print("<div class='col-md-4'>");
    out.print("<h4>About the Author</h4>");
    out.print(htmlEscapedAuthorInfo);
    out.print("</div>");

    out.print("</div>");
  }
}

이제 이것이 HTML 페이지를 생성하는 좋은 방법이 아니라는 것을 잠시 무시하고 레이아웃을 다시 변경하려면 어떻게됩니까? SpiffyPage어떻게 든 그 내용을 감싸는 하위 클래스 를 만들어야합니다 . 여기서 볼 수있는 것은 실수로 템플릿 메소드 패턴을 적용한 것입니다. 템플릿 메서드는 기본 클래스에서 재정의되도록 정의 된 확장 점입니다.

기본 수업이 바뀌면 어떻게 되나요? HTML 내용이 너무 많이 변경되면 서브 클래스가 제공하는 레이아웃이 깨질 수 있습니다. 따라서 나중에 기본 클래스를 변경하는 것은 실제로 안전하지 않습니다. 모든 클래스가 동일한 프로젝트에있는 경우에는 분명하지 않지만 기본 클래스가 다른 사람들이 개발 한 일부 게시 된 소프트웨어의 일부인 경우 매우 눈에.니다.

이 확장 전략을 의도 한 경우 사용자가 각 부품이 생성되는 방식을 바꿀 수있게되었습니다. 외부에서 제공 할 수있는 각 블록에 대한 전략이있을 수 있습니다. 또는 데코레이터를 중첩시킬 수 있습니다. 이것은 위의 코드와 동일하지만 훨씬 더 명확하고 훨씬 유연합니다.

Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());

public interface PageLayout {
  void writePage(PrintWriter out, PageLayout top);
  void writeMainContent(PrintWriter out, PageLayout top);
  ...
}

public final class Page {
  private PageLayout layout = new DefaultPageLayout();

  public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
    layout = wrapper.apply(layout);
  }

  ...
  @Override public String toString() {
    PrintWriter out = ...;
    layout.writePage(out, layout);
    ...
  }
}

public final class DefaultPageLayout implements PageLayout {
  @Override public void writeLayout(PrintWriter out, PageLayout top) {
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    top.writeHeader(out, top);
    top.writeMainContent(out, top);
    top.writeMainFooter(out, top);
    out.print("</body>");

    out.print("</html>");
  }

  @Override public void writeMainContent(PrintWriter out, PageLayout top) {
    ... /* as above*/
  }
}

public final class SpiffyPageDecorator implements PageLayout {
  private PageLayout inner;

  public SpiffyPageDecorator(PageLayout inner) {
    this.inner = inner;
  }

  @Override
  void writePage(PrintWriter out, PageLayout top) {
    inner.writePage(out, top);
  }

  @Override
  void writeMainContent(PrintWriter out, PageLayout top) {
    ...
    inner.writeMainContent(out, top);
    ...
  }
}

top호출 writeMainContent이 데코레이터 체인의 상단을 통과하도록하려면 추가 매개 변수가 필요합니다 . 이는 개방형 재귀 라는 서브 클래 싱 기능을 에뮬레이트합니다 .

데코레이터가 여러 개인 경우 이제 더 자유롭게 믹싱 할 수 있습니다.

기존 기능을 약간 조정하려는 욕구보다 훨씬 더 자주 기존 클래스의 일부를 재사용하려는 욕구입니다. 나는 누군가가 당신이 항목을 추가하고 모든 항목을 반복 할 수있는 수업을 원했던 경우를 보았습니다. 올바른 해결책은 다음과 같습니다.

final class Thingies implements Iterable<Thing> {
  private ArrayList<Thing> thingList = new ArrayList<>();

  @Override public Iterator<Thing> iterator() {
    return thingList.iterator();
  }

  public void add(Thing thing) {
    thingList.add(thing);
  }

  ... // custom methods
}

대신에 그들은 서브 클래스를 만들었습니다 :

class Thingies extends ArrayList<Thing> {
  ... // custom methods
}

이 갑자기 전체 인터페이스는 것을 의미 ArrayList의 일부가되었습니다 우리의 인터페이스를 제공합니다. 사용자는 remove()물건이나 get()특정 지수에 물건을 넣을 수 있습니다 . 이것은 그런 식으로 의도 된 것입니까? 승인. 그러나 종종 모든 결과를 신중하게 생각하지 않습니다.

따라서 다음을 권장합니다

  • extend신중한 생각 없이는 수업을하지 마십시오 .
  • final메서드를 재정의하려는 경우를 제외하고는 항상 클래스를 표시하십시오 .
  • 예를 들어 단위 테스트와 같이 구현을 교체하려는 인터페이스를 만듭니다.

이 "규칙"이 깨져야 할 많은 예가 있지만, 일반적으로 유연하고 좋은 디자인으로 안내하고 기본 클래스의 의도하지 않은 변경 (또는 기본 클래스의 인스턴스로서 서브 클래스의 의도하지 않은 사용)으로 인한 버그를 피합니다. ).

일부 언어에는보다 엄격한 시행 메커니즘이 있습니다.

  • 모든 방법은 기본적으로 최종적인 것으로 명시 적으로 표시되어야합니다. virtual
  • 인터페이스를 상속하지 않고 구현 만하는 개인 상속을 제공합니다.
  • 기본 클래스 메소드를 가상으로 표시해야하고 모든 대체를 모두 표시해야합니다. 이것은 서브 클래스가 새로운 메소드를 정의했지만 나중에 동일한 서명을 가진 메소드가 기본 클래스에 추가되었지만 가상으로 의도되지 않은 문제점을 피합니다.

3
"깨지기 쉬운 기본 클래스 문제"를 언급 한 경우 +100 이상이어야합니다. :)
David Arno

6
나는 여기에서 만든 요점에 확신하지 않습니다. 예, 취약한 기본 클래스는 문제이지만 final은 구현 변경과 관련된 모든 문제를 해결하지 못합니다. 첫 번째 예는 PasswordChecker의 가능한 모든 사용 사례를 알고 있다고 가정하기 때문에 좋지 않습니다 ( "모두를 잠 그거나 모든 사람이 액세스하도록 허용하는 것은 좋지 않습니다"-누가 말했습니까?). 마지막 "따라서 권장되는 ..."목록은 실제로 좋지 않습니다. 기본적으로 아무것도 확장하지 않고 모든 것을 최종으로 표시하는 것을 옹호하고 있습니다. OOP, 상속 및 코드 재사용의 유용성을 완전히 없애줍니다.
adelphus

4
첫 번째 예는 취약한 기본 클래스 문제의 예가 아닙니다. 취약한 기본 클래스 문제에서 기본 클래스로 변경하면 서브 클래스가 중단됩니다. 그러나이 예에서 하위 클래스는 하위 클래스의 계약을 따르지 않습니다. 이들은 두 가지 다른 문제입니다. (또한, 특정 상황에서, 당신은 암호 검사기를 해제 할 수 그것은 실제로 적절한 것을 () 개발을위한 말)
윈스턴 Ewert에게

5
"그것은 깨지기 쉬운 기본 계급 문제를 피한다"-자신을 죽이는 것은 배고프지 않는 것과 같은 방식이다.
user253751 2016 년

9
@immibis는 음식을 먹는 것을 피하기 위해 먹는 것을 피하는 것과 같습니다. 물론, 절대 먹지 않는 것이 문제가 될 것입니다. 그러나 당신이 믿는 곳에서만 먹는 것이 합리적입니다.
Winston Ewert

33

Joshua Bloch의 Effective Java, 2nd Edition 을 언급 한 사람이 아무도 없습니다 (적어도 모든 Java 개발자에게 읽어야 함). 이 책의 항목 17은 이에 대해 자세히 설명하며 제목은 " 상속을위한 디자인 및 문서화 또는 금지 "입니다.

나는이 책에서 좋은 충고를 모두 반복하지는 않겠지 만,이 특정한 단락들은 관련이있는 것 같습니다.

그러나 일반적인 구체적인 수업은 어떻습니까? 전통적으로 그것들은 서브 클래 싱을 위해 최종화되거나 디자인 및 문서화되지 않았지만,이 상황은 위험합니다. 그러한 클래스가 변경 될 때마다 클래스를 확장하는 클라이언트 클래스가 중단 될 수 있습니다. 이것은 단지 이론적 인 문제가 아닙니다. 상속을 위해 설계되고 문서화되지 않은 비 최종 콘크리트 클래스의 내부를 수정 한 후 서브 클래 싱 관련 버그 보고서를받는 것은 드문 일이 아닙니다.

이 문제에 대한 최선의 해결책은 안전하게 서브 클래 싱되도록 설계 및 문서화되지 않은 클래스에서 서브 클래 싱을 금지하는 것입니다. 서브 클래 싱을 금지하는 두 가지 방법이 있습니다. 둘 중 더 쉬운 것은 클래스 final을 선언하는 것입니다. 대안은 모든 생성자를 개인용 또는 패키지 전용으로 만들고 생성자 대신 공용 정적 팩토리 를 추가 하는 것입니다. 서브 클래스를 내부적으로 사용할 수있는 유연성을 제공하는이 대안은 항목 15에서 설명합니다. 두 가지 방법 중 하나를 사용할 수 있습니다.


21

final유용한 이유 중 하나는 부모 클래스의 계약을 위반하는 방식으로 클래스를 서브 클래 싱 할 수 없기 때문입니다. 이러한 서브 클래 싱은 SOLID (대부분 "L")를 위반하는 것이며 클래스를 만들면이를 final방지 할 수 있습니다.

한 가지 전형적인 예는 서브 클래스를 변경 가능하게하는 방식으로 불변 클래스를 서브 클래스 화하는 것을 불가능하게 만드는 것입니다. 어떤 경우에는 그러한 행동의 변화는 매우 놀라운 영향을 줄 수 있습니다.

Java에서는 String이러한 객체가 전달 될 때 서브 클래스 를 만들고이를 변경 가능하게 만들거나 (또는 ​​누군가 메소드를 호출 할 때 홈으로 콜백하도록하여 시스템에서 중요한 데이터를 가져올 수있는 경우) 많은 흥미로운 보안 문제가 발생할 수 있습니다. 클래스 로딩 및 보안과 관련된 일부 내부 코드 주변.

Final은 또한 메소드 내에서 두 가지에 대해 동일한 변수를 재사용하는 것과 같은 간단한 실수를 방지하는 데 도움이됩니다. Scala에서는 valJava의 최종 변수와 대략적으로 일치하는 것만 사용 하고 실제로는 var또는 최종 변수가 아닌 변수는 의심으로 보입니다.

마지막으로 컴파일러는 이론적으로 적어도 클래스 또는 메소드가 최종임을 알 때 추가 최적화를 수행 할 수 있습니다. 가상 메소드 테이블을 통해 상속을 확인하십시오.


6
마지막으로 컴파일러는 적어도 이론적으로 => 나는 Clang의 가상화 단계를 개인적으로 검토했으며 실제로 사용되는지 확인했습니다.
Matthieu M.

그러나 컴파일러는 표시된 최종 여부에 관계없이 아무도 클래스 또는 메소드를 대체하지 않는다고 미리 말할 수 있습니까?
JesseTG

3
@JesseTG 한 번에 모든 코드에 액세스 할 수 있다면 아마도. 그래도 별도의 파일 컴파일은 어떻습니까?
Monica Monica 복원

3
@JesseTG 가상화 또는 (단일 / 다형성) 인라인 캐싱은 시스템이 현재로드 된 클래스를 알고 있기 때문에 재정의 메소드가 없다는 가정이 잘못된 것으로 판명되면 코드를 최적화 할 수 없기 때문에 JIT 컴파일러의 일반적인 기술입니다. 그러나 사전 컴파일러는 불가능합니다. 하나의 Java 클래스와 해당 클래스를 사용하는 코드를 컴파일 할 때 나중에 서브 클래스를 컴파일하고 인스턴스를 소비 코드로 전달할 수 있습니다. 가장 간단한 방법은 클래스 경로 앞에 다른 항아리를 추가하는 것입니다. 컴파일러는 런타임에 발생하므로 모든 것을 알 수 없습니다.
amon

5
@MTilsted 리플렉션을 통한 문자열 변경은을 통해 금지 될 수 있으므로 문자열을 변경할 수 없다고 가정 할 수 있습니다 SecurityManager. 대부분의 프로그램은 사용하지 않지만 신뢰할 수없는 코드는 실행하지 않습니다. +++ 당신 그렇지 않으면 당신은 보안으로 제로와 임의의 버그를 보너스로 얻는 것처럼 문자열을 변경할 수 없다고 가정 해야합니다. Java 문자열이 생산 코드에서 변경 될 수 있다고 가정하는 모든 프로그래머는 반드시 미쳤습니다.
maaartinus

7

두 번째 이유는 성능 입니다. 첫 번째 이유는 일부 클래스에는 시스템 작동을 위해 변경되지 않아야하는 중요한 동작이나 상태가 있기 때문입니다. 예를 들어, "PasswordCheck"클래스가 있고 해당 클래스를 빌드하기 위해 보안 전문가 팀을 고용했으며이 클래스는 잘 연구되고 정의 된 프로 콜로 수백 개의 ATM과 통신합니다. 대학에서 새로 온 신입 사원이 위의 클래스를 확장하는 "TrustMePasswordCheck"클래스를 만들면 내 시스템에 매우 해로울 수 있습니다. 이러한 메소드는 무시되지 않아야합니다.



JVM은 클래스가 final로 선언되지 않은 경우에도 서브 클래스가없는 경우 클래스를 final로 처리 할 수있을 정도로 똑똑합니다.
user253751

'성능'주장이 여러 번 반증 되었기 때문에 하향 조정되었습니다.
Paul Smith

7

수업이 필요할 때 수업을 씁니다. 서브 클래스가 필요하지 않으면 서브 클래스에 신경 쓰지 않습니다. 수업이 의도 한대로 작동하는지 확인하고 수업을 사용하는 장소에서 수업이 의도 한대로 작동한다고 가정합니다.

누구든지 내 클래스를 서브 클래스로 만들고 싶다면 어떤 일이 발생했는지에 대한 책임을 완전히 거부하고 싶습니다. 수업을 "최종"으로 만들어서 달성했습니다. 하위 클래스를 만들고 싶다면 클래스를 작성하는 동안 하위 클래스를 고려하지 않았다는 것을 기억하십시오. 따라서 클래스 소스 코드를 가져 와서 "최종"을 제거해야하며 그때부터 발생하는 모든 일은 전적으로 귀하의 책임 입니다.

"객체 지향적이지 않다"고 생각하십니까? 해야 할 일을하는 수업을하도록 돈을 받았습니다. 아무도 서브 클래 싱 할 수있는 수업을 만든 것에 대해 지불하지 않았습니다. 수업을 재사용 할 수 있도록 돈을받는다면 언제든지 환영합니다. "final"키워드를 제거하여 시작하십시오.

(그 외에 "최종"은 종종 상당한 최적화를 허용합니다. 예를 들어 공개 클래스 또는 공개 클래스의 Swift "최종"에서 컴파일러는 메소드 호출이 어떤 코드를 실행할지 완전히 알 수 있습니다. 동적 디스패치를 ​​정적 디스패치 (대부분의 이점)로 대체하고 정적 디스패치를 ​​인라인으로 대체 할 수 있습니다 (아마도 큰 이점).

adelphus : "서브 클래스를 만들고 싶다면 소스 코드를 가져 와서"최종 "을 제거하면 그것이 당신의 책임입니다"에 대해 이해하기 어려운 것이 무엇입니까? "최종"은 "공정한 경고"와 같습니다.

그리고 재사용 가능한 코드를 만드는 데 비용이 들지 않습니다 . 해야 할 일을하는 코드를 작성해야합니다. 비슷한 두 비트의 코드를 만들기 위해 돈을 지불하면 공통 부분을 추출하므로 비용이 저렴하고 시간을 낭비하지 않습니다. 재사용되지 않는 코드를 재사용 할 수있게 만드는 것은 시간 낭비입니다.

M4ks : 외부에서 접근해서는 안되는 모든 것을 항상 비공개로 만듭니다. 다시 서브 클래스로 만들려면 소스 코드를 가져 와서 필요한 경우 항목을 "보호"로 변경하고 수행 한 작업에 대한 책임을 져야합니다. 내가 비공개로 표시 한 항목에 액세스해야한다고 생각하면 자신이하는 일을 더 잘 알 수 있습니다.

둘 다 : 서브 클래 싱은 재사용 코드의 아주 작은 부분입니다. 서브 클래 싱없이 적응할 수있는 빌딩 블록을 생성하는 것은 블록 사용자가 얻는 것에 의존 할 수 있기 때문에 훨씬 강력하고 "최종"의 이점이 있습니다.


4
-1이 답변은 소프트웨어 개발자에게 잘못된 모든 것을 설명합니다. 누군가가 서브 클래 싱을 통해 수업을 재사용하고 싶다면 그들이 어떻게 그것을 사용 (또는 남용)하는 것이 당신의 책임이됩니까? 기본적으로, 당신은 당신 과 같이 final 을 사용하고 있습니다, 당신은 내 수업을 사용하지 않습니다. * "아무도 서브 클래 싱 할 수있는 클래스를 만드는 것에 대해 아무도 지불하지 않았습니다" . 진심 이세요? 이것이 바로 재사용 가능한 견고한 코드를 만들기 위해 소프트웨어 엔지니어가 고용 된 이유입니다.
adelphus

4
-1 모든 것을 비공개로 만들면 아무도 서브 클래 싱을 생각하지 않을 것입니다.
M4ks 2016 년

3
@adelphus이 답변의 문구는 가혹하고 경계가 무딘 반면 "잘못된"관점은 아닙니다. 사실 그것은이 질문에 대한 대부분의 답변이 임상 적 톤이 적은 경우와 동일한 관점입니다.
NemesisX00

'최종'을 제거 할 수 있다고 언급하면 ​​+1입니다. 코드의 가능한 모든 사용에 대한 사전 지식을 주장하는 것은 거만합니다. 그러나 일부 가능한 용도를 유지할 수없고 포크를 유지해야하는 경우를 분명히하는 것은 겸손합니다.
gmatht

4

플랫폼 용 SDK가 다음 클래스를 제공한다고 가정 해 봅시다.

class HTTPRequest {
   void get(String url, String method = "GET");
   void post(String url) {
       get(url, "POST");
   }
}

응용 프로그램은이 클래스를 서브 클래 싱합니다.

class MyHTTPRequest extends HTTPRequest {
    void get(String url, String method = "GET") {
        requestCounter++;
        super.get(url, method);
    }
}

모두 훌륭하지만 SDK를 사용하는 누군가는 메소드를 전달하는 get것이 어리석은 것으로 결정하고 이전 버전과의 호환성을 강화하기 위해 인터페이스를 더 잘 만듭니다.

class HTTPRequest {
   @Deprecated
   void get(String url, String method) {
        request(url, method);
   }

   void get(String url) {
       request(url, "GET");
   }
   void post(String url) {
       request(url, "POST");
   }

   void request(String url, String method);
}

위의 응용 프로그램이 새로운 SDK로 다시 컴파일 될 때까지 모든 것이 잘 보입니다. 갑자기 재정의 된 get 메소드가 더 이상 호출되지 않고 요청이 계산되지 않습니다.

겉보기에 무해한 변경으로 인해 하위 클래스가 깨지기 때문에이를 취약한 기본 클래스 문제라고합니다. 클래스 내에서 호출되는 메소드를 변경할 때마다 서브 클래스가 중단 될 수 있습니다. 이는 거의 모든 변경으로 인해 서브 클래스가 중단 될 수 있음을 의미합니다.

Final은 사용자가 클래스를 서브 클래 싱하지 못하게합니다. 그렇게하면 누군가가 정확히 어떤 메소드 호출이 의존하는지 걱정하지 않고 클래스 내부의 메소드를 변경할 수 있습니다.


1

Final은 효율적으로 클래스가 다운 스트림 상속 기반 클래스에 영향을 미치지 않고 클래스가 변경되지 않아도 안전하다는 것을 의미합니다 (아무 것도 없기 때문에). 일부 스레드 기반 하이 징크스).

Final은 의도하지 않은 행동 변화를 기반으로하는 다른 사람들의 코드에 영향을주지 않으면 서 수업의 작동 방식을 자유롭게 변경할 수 있음을 의미합니다.

예를 들어, HobbitKiller라는 클래스를 작성합니다. 이는 모든 호빗이 까다롭기 때문에 아마도 죽어야하기 때문입니다. 흠집, 그들은 모두 죽어야합니다.

이 클래스를 기본 클래스로 사용하고 화염 방사기를 사용하는 멋진 새 방법을 추가하지만 호빗을 대상으로하는 훌륭한 방법이 있기 때문에 내 클래스를 기본으로 사용하십시오. 화염 방사기를 조준하는 데 사용합니다.

3 개월 후 타겟팅 방법의 구현을 변경합니다. 이제 여러분에게 알려지지 않은 라이브러리를 업그레이드 할 때 언젠가는 클래스의 실제 런타임 구현이 의존하는 수퍼 클래스 메소드의 변경으로 인해 근본적으로 변경되었으며 일반적으로 제어하지 않습니다.

그래서 나는 양심적 인 개발자가되고 수업을 통해 미래로의 호빗 죽음을 보장하기 위해 확장 할 수있는 수업에 대한 변경 사항에 매우 신중해야합니다.

특별히 수업을 연장하려는 경우를 제외하고는 연장 능력을 제거함으로써 많은 두통을 덜 수있었습니다.


2
타겟팅 방법이 변경되는 이유는 무엇입니까? 그리고 수업의 행동을 크게 final
바꾸면

왜 이것이 바뀌는 지 모르겠습니다. 호빗은 까다 롭고 변경이 필요했습니다. 요점은 최종적으로 빌드하면 다른 사람들이 내 변경 사항으로 인해 코드가 감염되는 것을 방지하는 상속을 방지한다는 것입니다.
Scott Taylor

0

의 사용은 final어떤 식 으로든 SOLID 원칙의 위반이 아니다. 불행하게도, 오픈 / 클로즈 원칙 ( "소프트웨어 엔터티는 확장을 위해 열려야하지만 수정을 위해 닫혀 야합니다")을 "클래스를 수정하고 서브 클래스로 만들고 새로운 기능을 추가하는 것"을 의미하는 것으로 해석하는 것이 매우 일반적입니다. 이것은 원래 의도 한 것이 아니며 일반적으로 목표를 달성하기위한 최선의 접근 방식이 아닙니다.

OCP를 준수하는 가장 좋은 방법은 개체에 대한 종속성을 주입하여 (예 : 전략 디자인 패턴 사용) 매개 변수화 된 추상 동작을 제공하여 확장 점을 클래스에 디자인하는 것입니다. 이러한 동작은 새로운 구현이 상속에 의존하지 않도록 인터페이스를 사용하도록 설계되어야합니다.

또 다른 방법은 공개 API를 사용하여 클래스를 추상 클래스 또는 인터페이스로 구현하는 것입니다. 그런 다음 동일한 클라이언트에 플러그인 할 수있는 완전히 새로운 구현을 생성 할 수 있습니다. 새 인터페이스가 원본과 매우 유사한 동작을 요구하는 경우 다음 중 하나를 수행 할 수 있습니다.

  • Decorator 디자인 패턴을 사용하여 원본의 기존 동작을 재사용하거나
  • 헬퍼 객체에 유지하려는 동작 부분을 리팩터링하고 새 구현에서 동일한 헬퍼를 사용하십시오 (리팩토링은 수정되지 않음).

0

나에게 그것은 디자인의 문제입니다.

직원의 급여를 계산하는 프로그램이 있다고 가정 해 봅시다. 국가를 기준으로 2 날짜 사이에 근무일 수를 반환하는 수업이있는 경우 (각 국가마다 한 수업) 최종 일정을 정하고 모든 기업이 캘린더에만 무료 하루를 제공 할 수있는 방법을 제공 할 것입니다.

왜? 단순한. 개발자가 WorkingDaysUSAmyCompany 클래스에서 기본 클래스 WorkingDaysUSA를 상속하고 해당 비즈니스가 파업 / 유지 보수 / 화성의 2 차 이유에 따라 폐쇄 될 것임을 반영하도록 수정한다고 가정 해 보겠습니다.

클라이언트 주문 및 배송에 대한 계산은 런타임에 WorkingDaysUSAmyCompany.getWorkingDays ()를 호출 할 때 지연과 작업을 반영하지만 휴가 시간을 계산하면 어떻게됩니까? 모든 사람을위한 휴일으로 2 번째 화성을 추가해야합니까? 그러나 프로그래머가 상속을 사용했기 때문에 클래스를 보호하지 않았으므로 혼란을 초래할 수 있습니다.

또는 토요일에 반 시간 근무하는 국가에서 토요일에 근무하지 않는 회사를 반영하여 클래스를 상속하고 수정한다고 가정 해 보겠습니다. 그리고 지진, 전기 위기 또는 어떤 상황으로 인해 대통령은 최근 베네수엘라에서 일어난 3 일의 휴무일을 선언합니다. 상속 된 클래스의 메소드가 이미 매주 토요일에 빼면, 원래 클래스에 대한 나의 수정은 같은 날 두 번 빼는 결과를 초래할 수 있습니다. 각 클라이언트의 각 하위 클래스로 이동하여 모든 변경 사항이 호환되는지 확인해야합니다.

해결책? 클래스를 final로 만들고 addFreeDay (companyID mycompany, Date freeDay) 메소드를 제공하십시오. 그렇게하면 WorkingDaysCountry 클래스를 호출 할 때 이것이 서브 클래스가 아닌 기본 클래스임을 확신 할 수 있습니다.

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