Java는 오랫동안 클래스를 파생 할 수없는 클래스를 선언 할 수있는 힘을 가지고 있으며 C ++에도 클래스가 있습니다. 그러나 SOLID의 열기 / 닫기 원칙에 비추어 볼 때 이것이 왜 유용한가? 나에게 final
키워드는 똑같이 들리지만 friend
합법적이지만 키워드 를 사용하는 경우 디자인이 잘못되었을 수 있습니다. 파생 할 수없는 클래스가 훌륭한 아키텍처 또는 디자인 패턴의 일부가되는 몇 가지 예를 제공하십시오.
final
.
Java는 오랫동안 클래스를 파생 할 수없는 클래스를 선언 할 수있는 힘을 가지고 있으며 C ++에도 클래스가 있습니다. 그러나 SOLID의 열기 / 닫기 원칙에 비추어 볼 때 이것이 왜 유용한가? 나에게 final
키워드는 똑같이 들리지만 friend
합법적이지만 키워드 를 사용하는 경우 디자인이 잘못되었을 수 있습니다. 파생 할 수없는 클래스가 훌륭한 아키텍처 또는 디자인 패턴의 일부가되는 몇 가지 예를 제공하십시오.
final
.
답변:
final
의도를 표현 합니다. 이 클래스는 사용자에게 클래스, 메소드 또는 변수를 알려줍니다. "이 요소는 변경되어서는 안되며 변경하려는 경우 기존 디자인을 이해하지 못했습니다."
이것은 모든 클래스와 작성한 모든 메소드 가 서브 클래스에 의해 완전히 다른 것을 수행하도록 변경 될 것으로 예상해야한다면 프로그램 아키텍처가 실제로 매우 어려울 것이기 때문에 중요 합니다. 어떤 요소가 변경 가능하고 변경되지 않아야하는지 미리 결정하고를 통해 변경 불가능 성을 강화하는 것이 훨씬 좋습니다 final
.
주석과 아키텍처 문서를 통해이 작업을 수행 할 수도 있지만, 향후 사용자가 문서를 읽고 준수하기를 기대하는 것보다 컴파일러가 가능한 일을 강제로 수행하도록하는 것이 좋습니다.
취약한 기본 클래스 문제를 피합니다 . 모든 클래스에는 암시 적 또는 명시 적 보증 및 불변이 있습니다. 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
Joshua Bloch의 Effective Java, 2nd Edition 을 언급 한 사람이 아무도 없습니다 (적어도 모든 Java 개발자에게 읽어야 함). 이 책의 항목 17은 이에 대해 자세히 설명하며 제목은 " 상속을위한 디자인 및 문서화 또는 금지 "입니다.
나는이 책에서 좋은 충고를 모두 반복하지는 않겠지 만,이 특정한 단락들은 관련이있는 것 같습니다.
그러나 일반적인 구체적인 수업은 어떻습니까? 전통적으로 그것들은 서브 클래 싱을 위해 최종화되거나 디자인 및 문서화되지 않았지만,이 상황은 위험합니다. 그러한 클래스가 변경 될 때마다 클래스를 확장하는 클라이언트 클래스가 중단 될 수 있습니다. 이것은 단지 이론적 인 문제가 아닙니다. 상속을 위해 설계되고 문서화되지 않은 비 최종 콘크리트 클래스의 내부를 수정 한 후 서브 클래 싱 관련 버그 보고서를받는 것은 드문 일이 아닙니다.
이 문제에 대한 최선의 해결책은 안전하게 서브 클래 싱되도록 설계 및 문서화되지 않은 클래스에서 서브 클래 싱을 금지하는 것입니다. 서브 클래 싱을 금지하는 두 가지 방법이 있습니다. 둘 중 더 쉬운 것은 클래스 final을 선언하는 것입니다. 대안은 모든 생성자를 개인용 또는 패키지 전용으로 만들고 생성자 대신 공용 정적 팩토리 를 추가 하는 것입니다. 서브 클래스를 내부적으로 사용할 수있는 유연성을 제공하는이 대안은 항목 15에서 설명합니다. 두 가지 방법 중 하나를 사용할 수 있습니다.
final
유용한 이유 중 하나는 부모 클래스의 계약을 위반하는 방식으로 클래스를 서브 클래 싱 할 수 없기 때문입니다. 이러한 서브 클래 싱은 SOLID (대부분 "L")를 위반하는 것이며 클래스를 만들면이를 final
방지 할 수 있습니다.
한 가지 전형적인 예는 서브 클래스를 변경 가능하게하는 방식으로 불변 클래스를 서브 클래스 화하는 것을 불가능하게 만드는 것입니다. 어떤 경우에는 그러한 행동의 변화는 매우 놀라운 영향을 줄 수 있습니다.
Java에서는 String
이러한 객체가 전달 될 때 서브 클래스 를 만들고이를 변경 가능하게 만들거나 (또는 누군가 메소드를 호출 할 때 홈으로 콜백하도록하여 시스템에서 중요한 데이터를 가져올 수있는 경우) 많은 흥미로운 보안 문제가 발생할 수 있습니다. 클래스 로딩 및 보안과 관련된 일부 내부 코드 주변.
Final은 또한 메소드 내에서 두 가지에 대해 동일한 변수를 재사용하는 것과 같은 간단한 실수를 방지하는 데 도움이됩니다. Scala에서는 val
Java의 최종 변수와 대략적으로 일치하는 것만 사용 하고 실제로는 var
또는 최종 변수가 아닌 변수는 의심으로 보입니다.
마지막으로 컴파일러는 이론적으로 적어도 클래스 또는 메소드가 최종임을 알 때 추가 최적화를 수행 할 수 있습니다. 가상 메소드 테이블을 통해 상속을 확인하십시오.
SecurityManager
. 대부분의 프로그램은 사용하지 않지만 신뢰할 수없는 코드는 실행하지 않습니다. +++ 당신 은 그렇지 않으면 당신은 보안으로 제로와 임의의 버그를 보너스로 얻는 것처럼 문자열을 변경할 수 없다고 가정 해야합니다. Java 문자열이 생산 코드에서 변경 될 수 있다고 가정하는 모든 프로그래머는 반드시 미쳤습니다.
두 번째 이유는 성능 입니다. 첫 번째 이유는 일부 클래스에는 시스템 작동을 위해 변경되지 않아야하는 중요한 동작이나 상태가 있기 때문입니다. 예를 들어, "PasswordCheck"클래스가 있고 해당 클래스를 빌드하기 위해 보안 전문가 팀을 고용했으며이 클래스는 잘 연구되고 정의 된 프로 콜로 수백 개의 ATM과 통신합니다. 대학에서 새로 온 신입 사원이 위의 클래스를 확장하는 "TrustMePasswordCheck"클래스를 만들면 내 시스템에 매우 해로울 수 있습니다. 이러한 메소드는 무시되지 않아야합니다.
수업이 필요할 때 수업을 씁니다. 서브 클래스가 필요하지 않으면 서브 클래스에 신경 쓰지 않습니다. 수업이 의도 한대로 작동하는지 확인하고 수업을 사용하는 장소에서 수업이 의도 한대로 작동한다고 가정합니다.
누구든지 내 클래스를 서브 클래스로 만들고 싶다면 어떤 일이 발생했는지에 대한 책임을 완전히 거부하고 싶습니다. 수업을 "최종"으로 만들어서 달성했습니다. 하위 클래스를 만들고 싶다면 클래스를 작성하는 동안 하위 클래스를 고려하지 않았다는 것을 기억하십시오. 따라서 클래스 소스 코드를 가져 와서 "최종"을 제거해야하며 그때부터 발생하는 모든 일은 전적으로 귀하의 책임 입니다.
"객체 지향적이지 않다"고 생각하십니까? 해야 할 일을하는 수업을하도록 돈을 받았습니다. 아무도 서브 클래 싱 할 수있는 수업을 만든 것에 대해 지불하지 않았습니다. 수업을 재사용 할 수 있도록 돈을받는다면 언제든지 환영합니다. "final"키워드를 제거하여 시작하십시오.
(그 외에 "최종"은 종종 상당한 최적화를 허용합니다. 예를 들어 공개 클래스 또는 공개 클래스의 Swift "최종"에서 컴파일러는 메소드 호출이 어떤 코드를 실행할지 완전히 알 수 있습니다. 동적 디스패치를 정적 디스패치 (대부분의 이점)로 대체하고 정적 디스패치를 인라인으로 대체 할 수 있습니다 (아마도 큰 이점).
adelphus : "서브 클래스를 만들고 싶다면 소스 코드를 가져 와서"최종 "을 제거하면 그것이 당신의 책임입니다"에 대해 이해하기 어려운 것이 무엇입니까? "최종"은 "공정한 경고"와 같습니다.
그리고 재사용 가능한 코드를 만드는 데 비용이 들지 않습니다 . 해야 할 일을하는 코드를 작성해야합니다. 비슷한 두 비트의 코드를 만들기 위해 돈을 지불하면 공통 부분을 추출하므로 비용이 저렴하고 시간을 낭비하지 않습니다. 재사용되지 않는 코드를 재사용 할 수있게 만드는 것은 시간 낭비입니다.
M4ks : 외부에서 접근해서는 안되는 모든 것을 항상 비공개로 만듭니다. 다시 서브 클래스로 만들려면 소스 코드를 가져 와서 필요한 경우 항목을 "보호"로 변경하고 수행 한 작업에 대한 책임을 져야합니다. 내가 비공개로 표시 한 항목에 액세스해야한다고 생각하면 자신이하는 일을 더 잘 알 수 있습니다.
둘 다 : 서브 클래 싱은 재사용 코드의 아주 작은 부분입니다. 서브 클래 싱없이 적응할 수있는 빌딩 블록을 생성하는 것은 블록 사용자가 얻는 것에 의존 할 수 있기 때문에 훨씬 강력하고 "최종"의 이점이 있습니다.
플랫폼 용 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은 사용자가 클래스를 서브 클래 싱하지 못하게합니다. 그렇게하면 누군가가 정확히 어떤 메소드 호출이 의존하는지 걱정하지 않고 클래스 내부의 메소드를 변경할 수 있습니다.
Final은 효율적으로 클래스가 다운 스트림 상속 기반 클래스에 영향을 미치지 않고 클래스가 변경되지 않아도 안전하다는 것을 의미합니다 (아무 것도 없기 때문에). 일부 스레드 기반 하이 징크스).
Final은 의도하지 않은 행동 변화를 기반으로하는 다른 사람들의 코드에 영향을주지 않으면 서 수업의 작동 방식을 자유롭게 변경할 수 있음을 의미합니다.
예를 들어, HobbitKiller라는 클래스를 작성합니다. 이는 모든 호빗이 까다롭기 때문에 아마도 죽어야하기 때문입니다. 흠집, 그들은 모두 죽어야합니다.
이 클래스를 기본 클래스로 사용하고 화염 방사기를 사용하는 멋진 새 방법을 추가하지만 호빗을 대상으로하는 훌륭한 방법이 있기 때문에 내 클래스를 기본으로 사용하십시오. 화염 방사기를 조준하는 데 사용합니다.
3 개월 후 타겟팅 방법의 구현을 변경합니다. 이제 여러분에게 알려지지 않은 라이브러리를 업그레이드 할 때 언젠가는 클래스의 실제 런타임 구현이 의존하는 수퍼 클래스 메소드의 변경으로 인해 근본적으로 변경되었으며 일반적으로 제어하지 않습니다.
그래서 나는 양심적 인 개발자가되고 수업을 통해 미래로의 호빗 죽음을 보장하기 위해 확장 할 수있는 수업에 대한 변경 사항에 매우 신중해야합니다.
특별히 수업을 연장하려는 경우를 제외하고는 연장 능력을 제거함으로써 많은 두통을 덜 수있었습니다.
final
의 사용은 final
어떤 식 으로든 SOLID 원칙의 위반이 아니다. 불행하게도, 오픈 / 클로즈 원칙 ( "소프트웨어 엔터티는 확장을 위해 열려야하지만 수정을 위해 닫혀 야합니다")을 "클래스를 수정하고 서브 클래스로 만들고 새로운 기능을 추가하는 것"을 의미하는 것으로 해석하는 것이 매우 일반적입니다. 이것은 원래 의도 한 것이 아니며 일반적으로 목표를 달성하기위한 최선의 접근 방식이 아닙니다.
OCP를 준수하는 가장 좋은 방법은 개체에 대한 종속성을 주입하여 (예 : 전략 디자인 패턴 사용) 매개 변수화 된 추상 동작을 제공하여 확장 점을 클래스에 디자인하는 것입니다. 이러한 동작은 새로운 구현이 상속에 의존하지 않도록 인터페이스를 사용하도록 설계되어야합니다.
또 다른 방법은 공개 API를 사용하여 클래스를 추상 클래스 또는 인터페이스로 구현하는 것입니다. 그런 다음 동일한 클라이언트에 플러그인 할 수있는 완전히 새로운 구현을 생성 할 수 있습니다. 새 인터페이스가 원본과 매우 유사한 동작을 요구하는 경우 다음 중 하나를 수행 할 수 있습니다.
나에게 그것은 디자인의 문제입니다.
직원의 급여를 계산하는 프로그램이 있다고 가정 해 봅시다. 국가를 기준으로 2 날짜 사이에 근무일 수를 반환하는 수업이있는 경우 (각 국가마다 한 수업) 최종 일정을 정하고 모든 기업이 캘린더에만 무료 하루를 제공 할 수있는 방법을 제공 할 것입니다.
왜? 단순한. 개발자가 WorkingDaysUSAmyCompany 클래스에서 기본 클래스 WorkingDaysUSA를 상속하고 해당 비즈니스가 파업 / 유지 보수 / 화성의 2 차 이유에 따라 폐쇄 될 것임을 반영하도록 수정한다고 가정 해 보겠습니다.
클라이언트 주문 및 배송에 대한 계산은 런타임에 WorkingDaysUSAmyCompany.getWorkingDays ()를 호출 할 때 지연과 작업을 반영하지만 휴가 시간을 계산하면 어떻게됩니까? 모든 사람을위한 휴일으로 2 번째 화성을 추가해야합니까? 그러나 프로그래머가 상속을 사용했기 때문에 클래스를 보호하지 않았으므로 혼란을 초래할 수 있습니다.
또는 토요일에 반 시간 근무하는 국가에서 토요일에 근무하지 않는 회사를 반영하여 클래스를 상속하고 수정한다고 가정 해 보겠습니다. 그리고 지진, 전기 위기 또는 어떤 상황으로 인해 대통령은 최근 베네수엘라에서 일어난 3 일의 휴무일을 선언합니다. 상속 된 클래스의 메소드가 이미 매주 토요일에 빼면, 원래 클래스에 대한 나의 수정은 같은 날 두 번 빼는 결과를 초래할 수 있습니다. 각 클라이언트의 각 하위 클래스로 이동하여 모든 변경 사항이 호환되는지 확인해야합니다.
해결책? 클래스를 final로 만들고 addFreeDay (companyID mycompany, Date freeDay) 메소드를 제공하십시오. 그렇게하면 WorkingDaysCountry 클래스를 호출 할 때 이것이 서브 클래스가 아닌 기본 클래스임을 확신 할 수 있습니다.
final
입니까? 저를 포함한 많은 사람들이 모든 비 추상적 인 수업을 만드는 것이 좋은 디자인이라는 것을 알게되었습니다final
.