추상 클래스에는 어떤 코드가 포함되어야합니까?


10

나는 최근에 추상 클래스 사용에 대해 고민하고 있습니다.

때로는 추상 클래스가 미리 만들어져 파생 클래스가 작동하는 방식의 템플릿으로 작동합니다. 이는 다소 높은 수준의 기능을 제공하지만 파생 클래스에서 구현할 특정 세부 정보를 생략한다는 것을 의미합니다. 추상 클래스는 몇 가지 추상 메소드를 배치하여 이러한 세부 사항의 필요성을 정의합니다. 이러한 경우 추상 클래스는 청사진, 기능에 대한 높은 수준의 설명 또는 호출하려는 모든 것과 같이 작동합니다. 자체적으로 사용할 수는 없지만 고급 구현에서 제외 된 세부 사항을 정의하도록 전문화되어야합니다.

다른 경우에는 일부 파생 된 클래스를 생성 한 후 추상 클래스가 생성됩니다 (부모 / 추상 클래스가 아직 파생되지 않았기 때문에 아직 의미하는 바는 아님). 이 경우 추상 클래스는 일반적으로 현재 파생 클래스에 포함 된 모든 종류의 공통 코드를 넣을 수있는 장소로 사용됩니다.

위의 관찰을 통해이 두 경우 중 어느 것이 규칙인지 궁금합니다. 모든 파생 클래스에서 일반적으로 발생하기 때문에 추상 클래스에 세부 정보가 표시되어야합니까? 고급 기능의 일부가 아닌 공통 코드가 있어야합니까?

파생 클래스에 공통으로 발생하기 때문에 추상 클래스 자체에 의미가없는 코드가 있어야합니까?

추상 클래스 A에는 a () 메소드와 aq () 메소드가 있습니다. 파생 클래스 AB와 AC 모두에서 aq () 메서드는 b () 메서드를 사용합니다. b ()를 A로 옮겨야합니까? 그렇다면 A 만 봅니다 (AB와 AC는 없다고 가정). b ()의 존재는 의미가 없습니다! 이것은 나쁜 것입니까? 누군가가 추상 클래스를 살펴보고 파생 클래스를 방문하지 않고 진행되는 상황을 이해할 수 있어야합니까?

솔직히 말해서, 이것을 요구하는 순간, 파생 클래스를 보지 않고도 이해가되는 추상 클래스를 작성하는 것은 깨끗한 코드와 깨끗한 아키텍처의 문제라고 생각하는 경향이 있습니다. 나는 모든 종류의 코드에 대한 덤프처럼 작동하는 추상 클래스의 아이디어가 실제로 모든 파생 클래스에서 공통적으로 발생하는 것을 좋아하지 않습니다.

당신은 어떻게 생각 / 연습합니까?


7
왜 "규칙"이 있어야합니까?
Robert Harvey

1
Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture.-- 왜? 응용 프로그램을 디자인하고 개발하는 과정에서 여러 클래스가 자연스럽게 추상 클래스로 리팩토링 될 수있는 공통 기능을 가지고 있음을 알 수 있습니까? 파생 클래스에 대한 코드를 작성하기 전에 항상 이것을 예상 할 수있는 천성적인 사람이어야합니까? 내가 이처럼 성실하지 않으면, 리팩토링을 수행하는 것이 금지됩니까? 코드를 버리고 다시 시작해야합니까?
Robert Harvey

내가 오해했다면 죄송합니다! 나는 내가 지금 느끼는 것을 더 나은 관행으로 말하려고 노력했지만 절대적인 규칙이 있어야 함을 암시하지는 않았다. 또한 추상 클래스에 속하는 코드가 미리 작성되어야 함을 암시하지 않습니다. 실제로 추상 클래스가 하이 레벨 코드 (파생 클래스의 템플릿 역할을 함)와 로우 레벨 코드 (사용하지 않는 유용성에 대해 이해할 수 없음)로 끝나는 방법을 설명했습니다. 파생 클래스).
Alexandros Gougousis

공용 기본 클래스의 경우 @RobertHarvey를 사용하면 파생 클래스를 볼 수 없습니다. 내부 수업의 경우 아무런 차이가 없습니다.
Frank Hileman

답변:


4

추상 클래스 A에는 a () 메소드와 aq () 메소드가 있습니다. 파생 클래스 AB와 AC 모두에서 aq () 메서드는 b () 메서드를 사용합니다. b ()를 A로 옮겨야합니까? 그렇다면 A 만 봅니다 (AB와 AC는 없다고 가정). b ()의 존재는 의미가 없습니다! 이것은 나쁜 것입니까? 누군가가 추상 클래스를 살펴보고 파생 클래스를 방문하지 않고 진행되는 상황을 이해할 수 있어야합니까?

무엇 당신의 묻는 것은 어디 장소입니다 b()다른 의미에서 질문 여부, 그리고 A즉각적인 슈퍼에 대한 클래스와 최선의 선택을 AB하고 AC.

세 가지 선택이있는 것 같습니다.

  1. 떠나 b()모두 ABAC
  2. 중간 클래스를 작성 ABAC-Parent에서 상속 A하고 소개하고는 b()다음에 대한 즉각적인 슈퍼 클래스로 사용 AB하고AC
  3. 넣어 b()에서 A(또 다른 미래의 클래스가 있는지 모르고 AD원할 것입니다 b()여부)

  1. 건조 하지 않아 고통 .
  2. YAGNI로 고통받습니다 .
  3. 그래서, 이것도 남습니다.

AD원하지 않는 다른 수업 b()자체가 제시 될 때까지 (3) 올바른 선택 인 것 같습니다.

AD선물 과 같은 시점에서 우리는 (2)의 접근법을 리팩토링 할 수 있습니다. 결국 소프트웨어입니다!


7
네 번째 옵션이 있습니다. b()수업에 넣지 마십시오 . 이 인수로 모든 데이터를 받아 모두가 무료 기능 확인 ABAC호출. 따라서 추가 할 때 이동하거나 더 이상 클래스를 만들 필요가 없습니다 AD.
user1118321

1
@ user1118321, 상자 밖에서 훌륭하고 좋은 생각. b()인스턴스 수명이 긴 상태 가 필요하지 않은 경우 특히 적합합니다 .
Erik Eidt

(3)에서 문제가되는 것은 추상 클래스가 더 이상 독립적 인 행동을 설명하지 않는다는 것입니다. 그것은 코드 조각 이며이 클래스와 모든 파생 클래스 사이에서 앞뒤로 이동하지 않고 추상 클래스 코드를 이해할 수 없습니다.
Alexandros Gougousis

추상적이 아닌 ac호출 하는 기본 / 기본 을 만들 수 있습니까 ? bac
Erik Eidt

3
나에게 가장 중요한 질문은 AB와 AC가 모두 동일한 방법 b ()를 사용하는 이유입니다. 우연의 일치라면 AB와 AC에서 두 가지 유사한 구현을 남겨 두겠습니다. 그러나 아마도 AB와 AC에 대한 일반적인 추상화가 있기 때문일 것입니다. 나에게 DRY 자체는 그다지 가치가 없지만 유용한 추상화를 놓쳤다는 힌트를줍니다.
Ralf Kleberhoff

2

추상 클래스는 편리하기 때문에 추상 클래스에 던져지는 다양한 기능이나 데이터에 대한 덤핑 그라운드가 아닙니다.

가장 안정적이고 확장 가능한 객체 지향 접근 방식을 제공하는 것으로 보이는 규칙 중 하나는 " 상속보다 컴포지션 선호 "입니다. 추상 클래스는 코드를 포함하지 않는 인터페이스 사양으로 생각하는 것이 가장 좋습니다.

추상 클래스와 함께 사용되는 일종의 라이브러리 메소드 인 메소드가있는 경우 추상적 클래스에서 파생 된 클래스가 일반적으로 필요로하는 기능이나 동작을 표현하는 가장 가능성이 높은 메소드 인 경우 이 메소드를 사용할 수있는 추상 클래스와 다른 클래스 사이의 클래스. 이 새로운 클래스는이 메소드를 제공함으로써 특정 구현 대안 또는 경로를 정의하는 추상 클래스의 특정 인스턴스를 제공합니다.

추상 클래스의 아이디어는 실제로 추상 클래스를 구현하는 파생 클래스가 서비스 또는 동작까지 제공하는 것에 대한 추상 모델을 갖는 것입니다. 상속은 사용하기 쉬우 며, 가장 유용한 클래스는 mixin 패턴을 사용하는 다양한 클래스로 구성되어 있습니다.

그러나 항상 변경 질문, 변경 사항 및 변경 방법 및 변경 이유는 항상 있습니다.

상속은 소스의 부서지기 쉽고 부서지기 쉬운 원인이 될 수 있습니다 ( 상속 : 이미 사용을 중지 하십시오 ! ).

취약한 기본 클래스 문제는 기본 클래스 (수퍼 클래스)가 "취약한"것으로 간주되는 객체 지향 프로그래밍 시스템의 근본적인 아키텍처 문제입니다. 파생 클래스가 상속 할 때 기본 클래스를 안전하게 수정하면 파생 클래스가 오작동 할 수 있습니다. . 프로그래머는 단순히 기본 클래스의 메소드를 검사하여 기본 클래스 변경이 안전한지 여부를 판별 할 수 없습니다.


OOP 개념이 잘못 사용되거나 남용되거나 남용되면 문제가 발생할 수 있다고 확신합니다! 또한 b ()가 라이브러리 메서드 (예 : 다른 종속성이없는 순수한 함수)라는 가정을하면 내 질문의 범위가 좁아집니다. 이것을 피하자.
Alexandros Gougousis

@ AlexandrosGougousis 나는 그것이 b()순수한 기능 이라고 가정하지 않습니다 . 펑 토이 드 또는 템플릿 또는 다른 것이 될 수 있습니다. 순수한 함수, COM 객체 또는 솔루션에 제공하는 기능에 사용되는 모든 구성 요소를 의미하는 "라이브러리 방법"이라는 구를 사용하고 있습니다.
Richard Chambers

내가 확실하지 않으면 죄송합니다! 나는 많은 예제들 중 하나로서 순수한 기능을 언급했습니다 (더 많이 주셨습니다).
Alexandros Gougousis

1

귀하의 질문은 추상적 클래스에 대한 하나 또는 접근 방식을 제안합니다. 그러나 도구 상자의 다른 도구로 생각해야한다고 생각합니다. 그리고 문제는 추상 직업이 어떤 직업 / 문제에 적합한 도구인가?

훌륭한 사용 사례 중 하나는 템플릿 메소드 패턴 을 구현하는 것입니다 . 모든 불변 로직을 추상 클래스에, 변형 로직을 서브 클래스에 넣습니다. 공유 논리 자체는 불완전하며 작동하지 않습니다. 대부분의 경우 여러 단계가 항상 동일하지만 적어도 하나의 단계가 다른 알고리즘 구현에 관한 것입니다. 이 단계를 추상 클래스 내의 함수 중 하나에서 호출되는 추상 메소드로 사용하십시오.

때로는 추상 클래스가 미리 만들어져 파생 클래스가 작동하는 방식의 템플릿으로 작동합니다. 이는 다소 높은 수준의 기능을 제공하지만 파생 클래스에서 구현할 특정 세부 정보를 생략한다는 것을 의미합니다. 추상 클래스는 몇 가지 추상 메소드를 배치하여 이러한 세부 사항의 필요성을 정의합니다. 이러한 경우 추상 클래스는 청사진, 기능에 대한 높은 수준의 설명 또는 호출하려는 모든 것과 같이 작동합니다. 자체적으로 사용할 수는 없지만 고급 구현에서 제외 된 세부 사항을 정의하도록 전문화되어야합니다.

첫 번째 예제는 본질적으로 템플릿 메소드 패턴에 대한 설명이라고 생각합니다 (잘못된 경우 수정).이를 추상 클래스의 완벽하게 유효한 사용 사례로 생각합니다.

다른 경우에는 일부 파생 된 클래스를 생성 한 후 추상 클래스가 생성됩니다 (부모 / 추상 클래스가 아직 파생되지 않았기 때문에 아직 의미하는 바는 아님). 이 경우 추상 클래스는 일반적으로 현재 파생 클래스에 포함 된 모든 종류의 공통 코드를 넣을 수있는 장소로 사용됩니다.

두 번째 예에서는 공유 논리 및 중복 코드를 처리하기위한 우수한 방법이 있기 때문에 추상 클래스를 사용하는 것이 최선의 선택이 아니라고 생각합니다. 하자 당신이 추상 클래스가 있다고 가정 A, 파생 클래스 B등을 C모두 파생 클래스가 메소드의 형태로 몇 가지 논리를 공유합니다 s(). 중복을 제거하기위한 올바른 접근 방식을 결정하려면 메소드 s()가 공용 인터페이스의 일부 인지 여부를 알아야합니다 .

그렇지 않은 경우 (method를 사용한 구체적인 예제와 b()같이) 경우는 매우 간단합니다. 별도의 클래스를 작성하고 조작을 수행하는 데 필요한 컨텍스트를 추출하십시오. 이것은 상속에 대한 구성 의 전형적인 예입니다 . 문맥이 거의 없거나 전혀 없다면 일부 의견에서 제안한 것처럼 간단한 도우미 기능으로 충분할 수 있습니다.

s()공용 인터페이스의 일부인 경우 좀 더 까다로워집니다. 와 관련 s()이없고 관련 이 없다고 가정 할 때 안에 넣지 말아야합니다 . 그럼 어디에 넣을까요? 나는 별도의 인터페이스 선언에 대한 주장 을 정의를 . 그런 다음 다시 구현하기위한 논리 와 둘 다를 포함 하고 의존 하는 별도의 클래스를 작성해야 합니다.BCAs()AIs()s()BC

마지막으로, 여기 당신이 추상 클래스 인터페이스에 갈 때시기를 결정하는 데 도움이 될 SO에 대한 흥미로운 질문의 훌륭한 답변에 대한 링크입니다 :

좋은 추상 클래스는 기능이나 상태를 공유 할 수 있기 때문에 다시 작성해야하는 코드의 양을 줄입니다.


공개 인터페이스의 일부인 s ()가 훨씬 까다로워진다는 데 동의합니다. 나는 일반적으로 같은 클래스에서 다른 공용 메서드를 호출하는 공용 메서드를 정의하지 않습니다.
Alexandros Gougousis 0

1

논리적으로나 기술적으로 객체 지향 지점이 누락 된 것 같습니다. 기본 클래스에서 유형의 공통 동작 그룹화와 다형성이라는 두 가지 시나리오를 설명합니다. 이들은 모두 추상 클래스의 합법적 인 응용 프로그램입니다. 그러나 클래스를 요약해야하는지 여부는 기술적 가능성이 아니라 분석 모델에 따라 다릅니다.

실제 세계에는 화신이없는 유형을 인식해야하지만 존재하는 특정 유형의 토대를 마련해야합니다. 예 : 동물. 동물과 같은 것은 없습니다. 그것은 항상 개 또는 고양이 또는 실제 동물이 없지만 무엇이든입니다. 그러나 동물은 그들 모두를 구성합니다.

그런 다음 레벨에 대해 이야기합니다. 상속과 관련하여 레벨은 거래의 일부가 아닙니다. 일반적인 데이터 나 일반적인 행동을 인식하지도 않는데 이는 도움이되지 않는 기술적 접근입니다. 스테레오 타입을 인식하고 기본 클래스를 삽입해야합니다. 그러한 고정 관념이 없으면 여러 클래스로 구현되는 몇 가지 인터페이스를 사용하는 것이 좋습니다.

멤버와 함께 추상 클래스의 이름이 의미가 있어야합니다. 기술적으로나 논리적으로 파생 클래스와 독립적이어야합니다. 동물처럼 추상적 인 방법 인 Eat (다형성 일 수 있음)와 부 울린 추상 속성 인 IsPet 및 IsLifestock을 가질 수 있습니다. 모든 의존성 (기술적 또는 논리적)은 자손에서 기본으로 한 방향으로 만 가야합니다. 기본 클래스 자체는 하위 클래스에 대한 지식이 없어야합니다.


언급 한 고정 관념은 상위 수준 기능에 대해 이야기 할 때 또는 다른 사람이 계층 구조에서 상위 클래스로 설명되는 일반 개념에 대해 이야기 할 때와 거의 같은 의미입니다. 계층).
Alexandros Gougousis

"기본 클래스 자체는 하위 클래스에 대한 지식이 없어야합니다." 나는 이것에 전적으로 동의합니다. 부모 클래스 (개요 여부)에는 이해하기 위해 자식 클래스 구현을 검사 할 필요가없는 독립적 인 비즈니스 로직이 포함되어야합니다.
Alexandros Gougousis

하위 클래스를 아는 기본 클래스에는 또 다른 것이 있습니다. 새로운 하위 유형이 개발 될 때마다 기본 클래스를 수정해야합니다. 최근에 기존 코드 에서이 문제가 발생했습니다. 기본 클래스에는 응용 프로그램 구성에 따라 하위 유형의 인스턴스를 생성하는 정적 메서드가 있습니다. 의도는 공장 패턴이었습니다. 이 방법으로도 SRP를 위반합니다. "솔직히 말해서"또는 "언어가 자동으로 처리 할 것"이라고 생각했던 일부 SOLID 포인트로 사람들이 상상할 수있는 것보다 더 창의적이라는 것을 알았습니다.
Martin Maat

0

첫 번째 경우 , 귀하의 질문에서 :

이러한 경우 추상 클래스는 청사진, 기능에 대한 높은 수준의 설명 또는 호출하려는 모든 것과 같이 작동합니다.

두 번째 경우 :

다른 경우에는 ... 추상 클래스는 일반적으로 현재 파생 클래스에 포함 된 모든 종류의 공통 코드를 넣을 수있는 장소로 사용됩니다.

그런 다음 질문 :

이 두 경우 중 어느 것이 규칙이되어야하는지 궁금합니다. ... 추상 클래스 자체에는 의미가 없을 수있는 코드가 파생 클래스에 공통적이기 때문에 존재해야합니까?

IMO에는 두 가지 시나리오가 있으므로 적용 할 디자인을 결정하기위한 단일 규칙을 그릴 수 없습니다.

첫 번째 경우에는 이미 다른 답변에서 언급 한 Template Method 패턴 과 같은 것들이 있습니다.

두 번째 경우에는 ab () 메서드를받는 추상 클래스 A의 예제를 제공했습니다. IMO b () 메소드는 다른 모든 파생 클래스에 적합한 경우에만 이동해야합니다. 즉, 두 곳에서 사용되기 때문에 이동하는 경우 내일 b ()가 전혀 이해되지 않는 새로운 구체적인 파생 클래스가있을 수 있기 때문에 이것은 좋은 선택이 아닙니다. 또한, 당신이 말했듯이, A 클래스를 별도로보고 b ()가 그 맥락에서 의미가 없다면, 이것이 좋은 디자인 선택이 아니라는 힌트 일 것입니다.


-1

몇 시간 전에 똑같은 질문이있었습니다!

내가 찾은 것은 내가 발견하지 못한 숨겨진 개념이 있다는 것을 알게 된 것입니다. 그리고이 개념은 아마도 value-object 로 표현해서는 안됩니다 . 매우 추상적 인 예가 있으므로 코드 사용의 의미를 설명 할 수 없습니다. 그러나 여기 내 자신의 사례가 있습니다. 외부 리소스에 요청을 보내고 응답을 구문 분석하는 방법을 나타내는 두 개의 클래스가 있습니다. 요청이 형성되는 방식과 응답이 구문 분석되는 방식에는 유사점이있었습니다. 그래서 내 계층 구조는 다음과 같습니다.

abstract class AbstractProtocol
{
    /**
     * @return array Registration params to send
     */
    abstract protected function assembleRegistrationPart();

    /**
     * @return array Payment params to send
     */
    abstract protected function assemblePaymentPart();

    protected function doSend(array $data)
    {
        return
            (new HttpClient(
                [
                    'timeout' => 60,
                    'encoding' => 'utf-8',
                    'language' => 'en',
                ]
            ))
                ->send($data);
    }

    protected function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}

class ClassicProtocol extends AbstractProtocol
{
    public function send()
    {
        $registration = $this->assembleRegistrationPart();
        $payment = $this->assemblePaymentPart();
        $specificParams = $this->assembleClassicSpecificPart();

        $dataToSend =
            array_merge(
                $registration, $payment, $specificParams
            );

        $this->log($dataToSend);

        $this->doSend($dataToSend);
    }

    protected function assembleRegistrationPart()
    {
        return ['hello' => 'there'];
    }

    protected function assemblePaymentPart()
    {
        return ['pay' => 'yes'];
    }
}

이러한 코드는 단순히 상속을 잘못 사용함을 나타냅니다. 그것이 리팩토링 될 수있는 방법입니다.

class ClassicProtocol
{
    private $request;
    private $logger;

    public function __construct(Request $request, Logger $logger, Client $client)
    {
        $this->request = $request;
        $this->client = $client;
        $this->logger = $logger;
    }

    public function send()
    {
        $this->logger->log($this->request->getData());
        $this->client->send($this->request->getData());
    }
}

$protocol =
    new ClassicProtocol(
        new PaymentRequest(
            new RegistrationData(),
            new PaymentData(),
            new ClassicSpecificData()
        ),
        new ClassicLogger(),
        new ClassicClient()
    );

class RegistrationData
{
    public function getData()
    {
        return ['hello' => 'there'];
    }
}

class PaymentData
{
    public function getData()
    {
        return ['pay' => 'yes'];
    }
}

class ClassicLogger
{
    public function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}
class ClassicClient
{
    private $properties;

    public function __construct()
    {
        $this->properties =
            [
                'timeout' => 60,
                'encoding' => 'utf-8',
                'language' => 'en',
            ];
    }
}

그 이후로 나는 상속을 매우 신중하게 대하며 여러 번 다쳤습니다.

그 이후로 나는 상속에 관한 또 다른 결론에 도달했다. 나는 내부 구조에 기반한 상속을 강력히 반대한다 . 그것은 캡슐화를 깨뜨리고, 깨지기 쉽고, 결국 절차 적입니다. 그리고 내 도메인을 올바른 방법으로 분해 하면 상속이 자주 발생하지 않습니다.


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