특정 인스턴스에 대한 서브 클래스 작성이 나쁜 습관입니까?


37

다음 디자인을 고려하십시오

public class Person
{
    public virtual string Name { get; }

    public Person (string name)
    {
        this.Name = name;
    }
}

public class Karl : Person
{
    public override string Name
    {
        get
        {
            return "Karl";
        }
    }
}

public class John : Person
{
    public override string Name
    {
        get
        {
            return "John";
        }
    }
}

여기에 문제가 있다고 생각하십니까? 나에게 Karl과 John 클래스는 클래스와 정확히 동일하기 때문에 클래스 대신 인스턴스이어야합니다.

Person karl = new Person("Karl");
Person john = new Person("John");

인스턴스가 충분할 때 왜 새 클래스를 작성합니까? 클래스는 인스턴스에 아무것도 추가하지 않습니다.


17
불행히도 많은 프로덕션 코드에서이를 찾을 수 있습니다. 가능하면 청소하십시오.
Adam Zuckerman

14
이것은 훌륭한 디자인입니다. 개발자가 비즈니스 데이터의 모든 변경에 빠뜨릴 수 없도록하고 24/7 사용할 수있는 문제가 없다면 ;-)
Doc Brown

1
OP가 기존의 지혜와 반대되는 것으로 보이는 일종의 관련 질문이 있습니다. programmers.stackexchange.com/questions/253612/…
덩크

11
데이터가 아닌 행동에 따라 수업을 디자인하십시오. 나에게 하위 클래스는 계층 구조에 새로운 동작을 추가하지 않습니다.
Songo

2
이것은 람다가 Java 8 (다른 구문과 동일한 것)에 오기 전에 Java의 익명 서브 클래스 (예 : 이벤트 핸들러)와 함께 널리 사용됩니다.
marczellm

답변:


77

모든 사람에 대해 특정 서브 클래스를 가질 필요는 없습니다.

맞습니다. 대신 인스턴스 여야합니다.

서브 클래스의 목표 : 부모 클래스 확장

서브 클래스는 부모 클래스가 제공 하는 기능확장하는 데 사용됩니다 . 예를 들어 다음이있을 수 있습니다.

  • 무언가를 할 수 있고 속성을 가질 Battery수 있는 부모 클래스Power()Voltage

  • 그리고 서브 클래스 RechargeableBattery상속 할 수있는, Power()그리고 Voltage하지만하는 것도 할 수있다 Recharge()라.

RechargeableBatterya Battery를 인수로 허용하는 모든 메소드 에 클래스 인스턴스를 매개 변수로 전달할 수 있습니다 . 이것은라고 Liskov 대체 원칙 , 다섯 개 SOLID 원칙 중 하나. 마찬가지로, 실제로 MP3 플레이어가 AA 배터리 2 개를 수용하는 경우 충전식 AA 배터리 2 개로 교체 할 수 있습니다.

필드 간의 차이를 나타 내기 위해 필드 또는 서브 클래스를 사용해야하는지 여부를 결정하기가 어려운 경우가 있습니다. 예를 들어 AA, AAA 및 9- 볼트 배터리를 처리해야하는 경우 세 개의 서브 클래스를 만들거나 열거 형을 사용 하시겠습니까? 232 페이지 Martin Fowler의 리팩토링 에서“서브 클래스를 필드로 대체” 에서는 아이디어를주고받는 방법에 대해 설명합니다.

당신의 예에서 KarlJohn아무것도 확장 있지 않으며 추가적인 가치를 제공하지 않는다 : 당신은 정확히 동일한 기능을 사용 할 수 있습니다 Person직접 클래스를. 추가 값없이 더 많은 코드 줄을 갖는 것은 결코 좋지 않습니다.

비즈니스 사례의 예

특정 사람을위한 서브 클래스를 만드는 것이 실제로 의미가있는 비즈니스 사례 일 수 있습니까?

회사에서 일하는 사람을 관리하는 응용 프로그램을 구축한다고 가정 해 봅시다. 응용 프로그램은 권한도 관리하므로 회계 담당자 인 Helen은 SVN 저장소에 액세스 할 수 없지만 두 프로그래머 인 Thomas와 Mary는 회계 관련 문서에 액세스 할 수 없습니다.

회사의 창립자이자 CEO 인 Jimmy는 누구에게도 특권이 없습니다. 예를 들어, 전체 시스템을 종료하거나 사람을 해고 할 수 있습니다. 당신은 아이디어를 얻습니다.

이러한 응용 프로그램의 가장 나쁜 모델은 다음과 같은 클래스를 갖는 것입니다.

          클래스 다이어그램은 Helen, Thomas, Mary 및 Jimmy 클래스와 공통 상위 클래스 인 추상 Person 클래스를 보여줍니다.

코드 중복이 매우 빨리 발생하기 때문입니다. 4 명의 직원의 기본적인 예에서도 Thomas와 Mary 클래스간에 코드를 복제합니다. 이렇게하면 공통 부모 클래스를 만들게 Programmer됩니다. 회계사가 여러 명있을 수 있으므로 Accountant클래스를 만들 수도 있습니다 .

          클래스 다이어그램은 추상 회계사, 추상 프로그래머 및 지미의 세 자녀를 가진 추상 개인을 보여줍니다.  회계사에는 Helen이라는 하위 클래스가 있고 Programmer에는 Thomas와 Mary의 두 가지 하위 클래스가 있습니다.

지금, 당신은 클래스를 갖는 것을 알 Helen매우 유용뿐만 아니라 유지되지 않습니다 ThomasMary: 코드의 작품의 대부분을 상위 수준에서 어쨌든-에서 회계사, 프로그래머와 지미의 수준. SVN 서버는 로그에 액세스해야하는 Thomas 또는 Mary인지 상관하지 않습니다. 프로그래머인지 회계사인지 알아야합니다.

if (person is Programmer)
{
    this.AccessGranted = true;
}

따라서 사용하지 않는 클래스를 제거하십시오.

          클래스 다이어그램에는 공통 상위 클래스 인 abstract Person과 함께 회계사, 프로그래머 및 Jimmy 서브 클래스가 포함됩니다.

“하지만 지미는있는 그대로 유지할 수 있습니다. 항상 CEO는 1 명이고 대기업은 1 명뿐입니다. 또한 Jimmy는 코드에서 많이 사용되며 실제로 이전 예제와는 달리 다음과 같이 보입니다.

if (person is Jimmy)
{
    this.GiveUnrestrictedAccess(); // Because Jimmy should do whatever he wants.
}
else if (person is Programmer)
{
    this.AccessGranted = true;
}

이 접근 방식의 문제점은 여전히 ​​버스를 타더라도 Jimmy가 타격을받을 수 있고 새로운 CEO가 생길 수 있다는 것입니다. 또는 이사회는 Mary가 자신이 새로운 CEO가되어야한다고 결정할 수 있으며, Jimmy는 영업 사원의 위치로 강등 될 것이므로 이제 모든 코드를 살펴보고 모든 것을 변경해야합니다.


6
@DocBrown : 틀린 것은 아니지만 깊지 않은 짧은 답변은 주제를 깊이있게 설명하는 답변보다 훨씬 높은 점수를 받는다는 사실에 종종 놀라곤합니다. 아마 너무 많은 사람들이 많이 읽는 것을 좋아하지 않기 때문에 매우 짧은 대답은 그들에게 더 매력적으로 보입니다. 아직도, 나는 적어도 약간의 설명을 제공하기 위해 대답을 편집했습니다.
Arseni Mourzenko

3
짧은 답변이 먼저 게시되는 경향이 있습니다. 이전 게시물은 더 많은 투표를 얻습니다.
Tim B

1
@ TimB : 보통 중간 크기의 답변으로 시작한 다음 매우 완전하게 편집하십시오 (여기 에는 수정본이 15 개 정도 있고 4 개만 표시되어 있음을 감안할 때 예제 가 있습니다). 시간이 지남에 따라 답변이 길어질수록 더 많은지지를받습니다. 짧게 남아있는 비슷한 질문은 더 많은 찬성을 불러 일으킨다.
Arseni Mourzenko

@DocBrown에 동의하십시오. 예를 들어, 좋은 대답은 " 컨텐츠가 아닌 행동을 위한 서브 클래스"와 같은 말 을하고이 주석에서하지 않을 참조를 언급합니다.
user949300

2
마지막 단락에서 명확하지 않은 경우 : 무제한 액세스 권한을 가진 사람이 1 명인 경우에도 그 사람을 무제한 액세스를 구현하는 별도의 클래스의 인스턴스로 만들어야합니다. 사람 자체를 확인하면 코드 데이터 상호 작용이 발생하고 전체 웜 캔을 열 수 있습니다. 예를 들어, 이사회가 승리자로 CEO를 임명하기로 결정하면 어떻게됩니까? "제한 없음"클래스가없는 경우 기존 CEO를 업데이트 할뿐만 아니라 다른 구성원에 대한 수표를 2 개 더 추가해야합니다. 가지고 있다면 "제한 없음"으로 설정하면됩니다.
Nzall

15

인스턴스에서 설정 가능한 필드 여야하는 세부 사항을 변경하기 위해 이러한 종류의 클래스 구조를 사용하는 것은 바보입니다. 그러나 그것은 당신의 예에 달려 있습니다.

Person다른 클래스를 다른 클래스로 만드는 것은 거의 나쁜 생각입니다. 새로운 사람이 도메인에 들어갈 때마다 프로그램을 변경하고 다시 컴파일해야합니다. 그러나 기존 클래스에서 상속하여 다른 문자열이 어딘가에 반환되는 것이 때로는 유용하지 않다는 것을 의미하지는 않습니다.

차이점은 해당 클래스가 나타내는 엔터티가 어떻게 달라지는 지입니다. 사람들은 거의 항상왔다 갔다하고 사용자를 다루는 거의 모든 심각한 응용 프로그램은 런타임에 사용자를 추가하고 제거 할 수있을 것으로 예상됩니다. 그러나 다른 암호화 알고리즘과 같은 것을 모델링하는 경우 새로운 것을 지원하는 것이 어쨌든 큰 변화 일 것입니다. myName()메서드가 다른 문자열을 반환하고 메소드가 다른 것을 수행하는 새 클래스를 발명하는 것이 좋습니다 perform().


12

인스턴스가 충분할 때 왜 새 클래스를 작성합니까?

대부분의 경우 그렇지 않습니다. 귀하의 예는 실제로이 행동이 실제 가치를 추가하지 않는 좋은 경우입니다.

하위 클래스는 기본적으로 확장이 아니고 부모의 내부 작업을 수정하기 때문에 Open Closed 원칙을 위반합니다 . 더욱이, 부모의 공개 생성자는 이제 서브 클래스에서 짜증을 내고 API는 이해하기 어려워졌습니다.

그러나 코드 전체에서 자주 사용되는 하나 또는 두 개의 특수 구성 만있는 경우에는 복잡한 생성자가있는 부모를 하위 클래스로 만드는 것이 더 편리하고 시간이 덜 걸리는 경우도 있습니다. 그러한 특별한 경우에는 그러한 접근 방식으로 아무 문제가 없습니다. J / K를 생성 하는 생성자 라고합시다


OCP 단락에 동의하지 않습니다. 클래스가 속성 세터를 그 자손에게 노출시키는 경우, 좋은 디자인 원칙을 따르는 것으로 가정 할 때 – 속성은 내부 작업의 일부가 되어서는 안됩니다
Ben Aaronson

@BenAaronson 속성 자체의 동작이 변경되지 않는 한 OCP를 위반하지 않지만 여기서는 OCP를 위반하지 않습니다. 물론 내부 작업에 해당 속성을 사용할 수 있습니다. 그러나 기존 행동을 변경해서는 안됩니다! 상속으로 변경하지 말고 행동을 확장해야합니다. 물론 모든 규칙에는 예외가 있습니다.
팔콘

1
그렇다면 가상 멤버를 사용하는 것이 OCP 위반입니까?
Ben Aaronson

3
@Falcon 반복적 인 복잡한 생성자 호출을 피하기 위해 새 클래스를 파생시킬 필요가 없습니다. 일반적인 경우에 대한 팩토리를 작성하고 작은 변형을 매개 변수화하십시오.
Keen

@ BenAaronson 실제 구현을 가진 가상 구성원을 갖는 것은 그러한 위반이라고 말합니다. 추상 멤버 또는 아무 것도하지 않는 메소드는 유효한 확장 점입니다. 기본적으로 기본 클래스의 코드를 볼 때 모든 비 최종 메서드가 완전히 다른 것으로 대체 될 수있는 후보가 아니라 그대로 실행됩니다. 또는 다르게 표현하자면, 상속 클래스가 기본 클래스의 동작에 영향을 줄 수있는 방법은 명시 적이며 "추가적"으로 제한됩니다.
millimoose

8

이것이 연습의 범위라면, 이것이 나쁜 습관이라는 데 동의합니다.

John과 Karl의 행동이 다르면 상황이 약간 변경됩니다. 그것은 그 수 person하는 방법이 cleanRoom(Room room)요한 매우 효과적으로 좋은 룸메이트이며 청소 밝혀 곳,하지만 칼이 아닌 아주 많은 방을 청소하지 않습니다.

이 경우, 동작이 정의 된 자신의 서브 클래스가되도록하는 것이 합리적입니다. 같은 것을 성취하는 더 좋은 방법이 있지만 (예를 들어, CleaningBehavior수업) 적어도이 방법은 OO 원칙을 끔찍하게 위반하지 않습니다.


다른 행동, 다른 데이터. 감사.
Ignacio Soler Garcia

6

어떤 경우에는 정해진 수의 인스턴스 만있을 것이므로이 접근법을 사용하는 것은 괜찮습니다 (내 의견으로는 추악하지만). 언어에서 허용하는 경우 열거 형을 사용하는 것이 좋습니다.

자바 예제 :

public enum Person
{
    KARL("Karl"),
    JOHN("John")

    private final String name;

    private Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }
}

6

많은 사람들이 이미 대답했습니다. 나는 내 자신의 개인적인 관점을 줄 것이라고 생각했습니다.


옛날 옛적에 나는 음악을 만드는 응용 프로그램 (그리고 여전히)에서 일했습니다.

응용 프로그램은 추상적했다 Scale: 하위 클래스로 클래스를 CMajor, DMinor등등 Scale과 같이 뭔가를 보았다 :

public abstract class Scale {
    protected Note[] notes;
    public Scale() {
        loadNotes();
    }
    // .. some other stuff ommited
    protected abstract void loadNotes(); /* subclasses put notes in the array
                                          in this method. */
}

음악 생성기는 특정 Scale 인스턴스 . 사용자는 목록에서 음계를 선택하여 음악을 생성합니다.

어느 날 멋진 아이디어가 떠 올랐습니다. 사용자가 자신의 스케일을 만들도록 허용하지 않겠습니까? 사용자는 목록에서 메모를 선택하고 버튼을 누르면 사용 가능한 스케일에 새 스케일이 추가됩니다.

그러나 나는 이것을 할 수 없었다. 모든 스케일은 이미 컴파일 타임에 설정 되었기 때문입니다. 클래스로 표현되기 때문입니다. 그런 다음 저를 때렸습니다.

'슈퍼 클래스와 서브 클래스'의 관점에서 생각하는 것은 종종 직관적입니다. 이 시스템을 통해 거의 모든 것을 표현할 수 있습니다 : 수퍼 클래스 Person및 서브 클래스 JohnMary; 수퍼 클래스 Car및 서브 클래스 VolvoMazda; 슈퍼 클래스 Missile와 서브 클래스 SpeedRocked, LandMineTrippleExplodingThingy .

이런 방식으로 생각하는 것은 매우 자연 스럽습니다. 특히 OO를 비교적 처음 접하는 사람에게는 더욱 그렇습니다.

그러나 클래스는 템플릿 이고 객체는 이러한 템플릿에 내용이 담겨 있음을 항상 기억해야합니다 . 템플릿에 원하는 내용을 모두 부어 셀 수없이 많은 가능성을 창출 할 수 있습니다.

템플릿을 채우는 것은 서브 클래스의 일이 아닙니다. 그것은 객체의 일입니다. 서브 클래스의 역할은 실제 기능추가 하거나 템플릿을 확장하는 것입니다. 입니다.

그렇기 때문에 필드가 있는 구체적인 Scale클래스를 만들고 객체가이 템플릿을 채우Note[] 도록해야 합니다 . 아마도 생성자 또는 무언가를 통해. 결국에는 그렇게했습니다.

클래스에서 템플릿 을 디자인 할 때마다 (예 : Note[]채워 져야 하는 빈 멤버 또는 String name값을 할당해야하는 필드) 템플릿을 채우는 것이이 클래스의 객체의 역할 임을 기억하십시오 ( 또는 아마도 이러한 객체를 만드는 사람들). 서브 클래스는 템플릿을 채우지 않고 기능을 추가하기위한 것입니다.


"슈퍼 클래스 Person, 서브 클래스 JohnMary"와 같은 종류의 시스템 을 만들고 싶은 유혹이있을 수 있습니다.

이런 식으로, Person p = new Mary()대신 이라고 말할 수 있습니다 Person p = new Person("Mary", 57, Sex.FEMALE). 보다 체계적이고 구조적인 작업을 수행합니다. 그러나 우리가 말했듯이, 모든 데이터 조합에 대해 새로운 클래스를 만드는 것은 좋은 접근 방법이 아닙니다. 코드가 부풀어 오르고 런타임 능력 측면에서 당신을 제한하기 때문입니다.

여기 해결책이 있습니다 : 기본 팩토리를 사용하십시오. 심지어 정적 팩토리 일 수도 있습니다. 이렇게 :

public final class PersonFactory {
    private PersonFactory() { }

    public static Person createJohn(){
        return new Person("John", 40, Sex.MALE);
    }

    public static Person createMary(){
        return new Person("Mary", 57, Sex.FEMALE);
    }

    // ...
}

이런 식으로 다음과 같이 '프로그램에 온다'라는 '사전 설정'을 쉽게 사용할 수 Person mary = PersonFactory.createMary()있지만, 예를 들어 사용자가 그렇게하도록 허용하려는 경우와 같이 새로운 사람을 동적으로 디자인 할 권리도 보유합니다 . 예 :

// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);

또는 더 나은 방법은 다음과 같습니다.

public final class PersonFactory {
    private PersonFactory() { }

    private static Map<String, Person> persons = new HashMap<>();
    private static Map<String, PersonData> personBlueprints = new HashMap<>();

    public static void addPerson(Person person){
        persons.put(person.getName(), person);
    }

    public static Person getPerson(String name){
        return persons.get(name);
    }

    public static Person createPerson(String blueprintName){
        PersonData data = personBlueprints.get(blueprintName);
        return new Person(data.name, data.age, data.sex);
    }

    // .. or, alternative to the last method
    public static Person createPerson(String personName){
        Person blueprint = persons.get(personName);
        return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
    }

}

public class PersonData {
    public String name;
    public int age;
    public Sex sex;

    public PersonData(String name, int age, Sex sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

나는 쫓겨났다. 나는 당신이 아이디어를 얻는다고 생각합니다.


서브 클래스는 그들의 슈퍼 클래스에 의해 설정된 템플릿을 채우는 것을 의미하지 않습니다. 서브 클래스는 기능추가하기위한 것 입니다. 개체 는 템플릿을 작성하기위한 것입니다.

가능한 모든 데이터 조합에 대해 새 클래스를 작성해서는 안됩니다. ( Scale가능한 모든 조합에 대해 새로운 서브 클래스를 생성해서는 안되는 것처럼 Note).

그녀는 지침입니다. 새로운 서브 클래스를 만들 때마다 수퍼 클래스에 존재하지 않는 새로운 기능이 추가되는지 고려하십시오. 해당 질문에 대한 답변이 "아니오"인 경우 수퍼 클래스의 '템플릿 입력'을 시도하는 것보다 개체를 만드는 것입니다. (그리고 아마도 '사전 설정'이있는 공장은 삶을 더 쉽게 만듭니다).

희망이 도움이됩니다.


이것은 훌륭한 예입니다. 대단히 감사합니다.
Ignacio Soler Garcia

@SoMoS 다행입니다.
Aviv Cohn

2

여기에는 '인스턴스이어야 함'에 대한 예외가 있지만 약간 극단적입니다.

당신이 원하는 경우 컴파일러 가 (이 예에서) 칼이나 존인지에 따라 접근 방법에 대한 사용 권한을 적용하기보다는있는 인스턴스가 다음 다른 클래스를 사용할 수 있습니다 통과되었습니다 확인하는 방법의 구현을 요청할 수 있습니다. 인스턴스화가 아닌 다른 클래스를 적용하면 런타임보다는 컴파일 타임에 'Karl'과 'John'을 구별 검사 할 수 있으며 이는 특히 컴파일러가 런타임보다 신뢰할 수있는 보안 컨텍스트에서 유용 할 수 있습니다 외부 라이브러리의 코드 검사 (예 :).


2

코드를 복제 하는 것은 나쁜 습관으로 간주됩니다 .

이것은 코드의 라인과 코드베이스의 크기가 유지 보수 비용 및 결함과 관련 되기 때문에 적어도 부분적으로 발생 합니다.

이 특정 주제에 대한 관련 상식 논리는 다음과 같습니다.

1) 이것을 변경해야 할 경우 몇 개의 장소를 방문해야합니까? 덜 좋습니다.

2) 이런 식으로하는 이유가 있습니까? 귀하의 예에서-할 수 없었습니다-그러나 일부 예 에서이 작업을 수행하는 이유가있을 수 있습니다. 내 머리 꼭대기에서 생각할 수있는 것은 상속이 필연적으로 GORM 용 플러그인 중 일부와 잘 어울리지 않는 초기 Grails와 같은 일부 프레임 워크입니다. 거의 그리고 멀리.

3) 이런 식으로 더 깨끗하고 이해하기 쉬워 집니까? 다시 말하지만, 예제에는 없지만 분리 된 클래스가 실제로 유지 관리하기가 더 쉬운 확장 패턴이 있습니다 (명령 또는 전략 패턴의 특정 사용법을 염두에 두십시오).


1
나쁜지 아닌지 돌에 설정되어 있지 않습니다. 예를 들어 객체 생성 및 메서드 호출에 대한 추가 오버 헤드가 성능에 해를 끼칠 수있어 애플리케이션이 더 이상 사양을 충족시키지 못하는 시나리오가 있습니다.
jwenting

포인트 # 2를 참조하십시오. 물론 이유가 있습니다. 그렇기 때문에 누군가의 코드를 제거하기 전에 물어봐야합니다.
dcgregorya

0

유스 케이스가 있습니다 : 함수 또는 메소드에 대한 참조를 일반 객체로 형성하십시오.

Java GUI 코드에서 이것을 많이 볼 수 있습니다.

ActionListener a = new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
        /* ... */
    }
};

컴파일러는 새로운 익명 서브 클래스를 작성하여 여기서 도움을줍니다.

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