코드를 확장하기 어려운 추상화가 너무 많음


9

코드베이스에서 추상화가 너무 많거나 적어도 처리하는 데 문제가 있습니다. 코드베이스의 대부분의 메소드는 코드베이스의 최상위 A를 취하도록 추상화되었지만이 상위의 하위 B에는 이러한 메소드 중 일부의 논리에 영향을주는 새로운 속성이 있습니다. 문제는 입력이 A로 추상화되고 A 에이 속성이 없기 때문에 해당 속성에서 해당 속성을 확인할 수 없다는 것입니다. B를 다르게 처리하는 새로운 방법을 만들려고하면 코드 복제가 필요합니다. 내 기술 책임자의 제안은 부울 매개 변수를 사용하는 공유 방법을 만드는 것이지만 이것의 문제점은 일부 사람들이 이것을 "숨겨진 제어 흐름"으로보고 있다는 것입니다. , 또한이 공유 메소드는 더 작은 공유 메소드로 분류 되더라도 향후 속성을 추가해야하는 경우 한 번에 지나치게 복잡하거나 복잡해집니다. 이것은 또한 커플 링을 증가시키고, 응집력을 감소 시키며, 팀의 누군가가 지적한 단일 책임 원칙을 위반합니다.

기본적으로이 코드베이스의 많은 추상화는 코드 중복을 줄이는 데 도움이되지만, 가장 높은 추상화를 취하기 위해 확장 / 변경 방법을 더 어렵게 만듭니다. 이런 상황에서 어떻게해야합니까? 다른 사람들이 자신이 좋아하는 것에 동의 할 수는 없지만 나는 비난의 중심에 있습니다. 그래서 결국 나를 아프게합니다.


10
"문제"를 문맹 화하는 코드 샘플을 추가하면 상황을 훨씬 더 이해하는 데 도움이 될 것입니다.
Seabizkit

여기에는 두 가지 SOLID 원칙이 있다고 생각합니다. 단일 책임-동작을 제어하는 ​​함수에 부울을 전달하면 함수에 더 이상 단일 책임이 없습니다. 다른 하나는 Liskov 대체 원칙입니다. 클래스 A를 매개 변수로 사용하는 함수가 있다고 상상해보십시오. A 대신 클래스 B를 전달하면 해당 기능의 기능이 손상됩니까?
bobek

나는 방법 A가 꽤 길고 하나 이상을 수행한다고 생각합니다. 그 경우입니까?
Rad80

답변:


27

B를 다르게 처리하는 새로운 방법을 만들려고하면 코드 복제가 필요합니다.

모든 코드 복제가 동일한 것은 아닙니다.

두 개의 매개 변수를 가져 와서 함께 호출하는 메서드가 있다고 가정 해보십시오 total(). 라는 다른 전화가 있다고 가정 해 보겠습니다 add(). 그들의 구현은 완전히 동일하게 보입니다. 하나의 방법으로 병합해야합니까? 아니!!!

안되 반복 - 자신 또는 DRY 원칙은 코드를 반복에 대해 없습니다. 결정, 아이디어를 퍼뜨리는 것에 관한 것이므로 아이디어를 변경하면 아이디어를 퍼뜨린 모든 곳에서 다시 작성해야합니다. 블리. 끔찍 해요 하지마 대신 DRY를 사용하면 한 곳에서 의사 결정을 내릴 수 있습니다 .

DRY (자신을 반복하지 마십시오) 원리 상태 :

모든 지식은 시스템 내에서 하나의 명백하고 권위있는 표현을 가져야합니다.

wiki.c2.com-스스로 반복하지 마십시오

그러나 DRY는 다른 곳에서 복사하여 붙여 넣는 것처럼 보이는 유사한 구현을 찾는 코드를 스캔하는 습관으로 손상 될 수 있습니다. 이것은 DRY의 두뇌 죽은 형태입니다. 정적 분석 도구를 사용하여이 작업을 수행 할 수 있습니다. 코드를 유연하게 유지하는 DRY 의 요점 을 무시하기 때문에 도움이되지 않습니다 .

총 요구 사항이 변경되면 total구현 을 변경해야 할 수도 있습니다 . 그렇다고 add구현 을 변경해야한다는 의미는 아닙니다 . 일부 쿠버가 그들을 하나의 방법으로 모으면 나는 약간의 불필요한 고통을 겪고 있습니다.

얼마나 많은 고통? 분명히 코드를 복사하고 필요할 때 새로운 방법을 만들 수 있습니다. 별거 아니야? Malarky! 다른 이름이 없으면 좋은 이름이 듭니다! 좋은 이름은 따르기가 어렵고 그 의미로 바이올린을 연주 할 때 제대로 응답하지 않습니다. 의도가 분명한 좋은 이름은 방법의 이름이 올 바르면 수정하기 쉬운 버그를 복사 한 위험보다 중요합니다.

그래서 내 충고는 비슷한 코드에 대한 무릎 저크 반응이 코드베이스를 매듭으로 묶는 것을 멈추는 것입니다. 나는 메소드가 존재한다는 사실을 무시하고 대신 빌리 닐리를 복사하여 붙여 넣을 수 있다고 말하는 것이 아닙니다. 아니요, 각 방법에는 그 아이디어에 대한 좋은 이름이 있어야합니다. 구현이 다른 좋은 아이디어의 구현과 일치한다면, 오늘날 누가 대체 누가 신경 쓰 겠는가?

반면에와 sum()동일하거나 심지어 다른 구현 방법을 가지고 total()있지만 총 요구 사항이 변경 될 때마다 변경 해야하는 sum()경우 두 가지 다른 이름으로 동일한 아이디어가 될 가능성이 큽니다 . 코드가 병합 된 경우 코드가 더 유연 할뿐만 아니라 사용하기가 덜 복잡합니다.

부울 매개 변수의 경우 예, 코드 냄새가 심합니다. 제어 흐름에 문제가있을뿐만 아니라 나쁜 점에서 추상화를 잘 랐음을 보여줍니다. 추상화는 사물을 사용하기가 더 단순하고 복잡하지 않게 만들어야합니다. 부울을 동작을 제어하는 ​​메소드에 전달하는 것은 실제로 호출하는 메소드를 결정하는 비밀 언어를 작성하는 것과 같습니다. 아야! 나 한테 그렇게 하지마 솔직하게 다형성일어나지 않는 한 각 방법마다 고유 한 이름을 지정하십시오 .

이제 추상화에 타 버린 것 같습니다. 추상화가 잘되면 훌륭한 일이기 때문에 너무 나쁩니다. 당신은 그것에 대해 생각하지 않고 많이 사용합니다. 랙과 피니언 시스템을 이해할 필요없이 자동차를 운전할 때마다 OS 인터럽트에 대해 생각하지 않고 인쇄 명령을 사용할 때마다, 그리고 각각의 강모에 대해 생각하지 않고 양치질을 할 때마다.

아니요, 직면 한 것처럼 보이는 문제는 잘못된 추상화입니다. 추상화는 필요와 다른 목적을 위해 만들어졌습니다. 복잡한 객체에 대한 간단한 인터페이스가 필요하므로 해당 객체를 이해하지 않고도 요구를 충족시킬 수 있습니다.

다른 개체를 사용하는 클라이언트 코드를 작성할 때 요구 사항과 해당 개체에서 필요한 것을 알 수 있습니다. 그렇지 않습니다. 이것이 클라이언트 코드가 인터페이스를 소유하는 이유입니다. 당신이 클라이언트 일 때 당신의 필요가 무엇인지 당신에게 말해주는 것은 없습니다. 당신은 당신의 요구가 무엇인지 보여주는 인터페이스를 내놓고 당신에게 건네진 모든 것이 그 요구를 충족시킬 것을 요구합니다.

그것은 추상화입니다. 클라이언트로서 나는 내가 무엇을 말하고 있는지 조차 모른다 . 나는 단지 내가 필요한 것을 알고 있습니다. 그렇다면 인터페이스를 변경하기 위해 무언가를 마무리해야합니다. 상관 없어요 내가해야 할 일을하세요 복잡하게하지 마십시오.

추상화를 사용하는 방법을 이해하기 위해 추상화 내부를 살펴보면 추상화가 실패했습니다. 그것이 어떻게 작동하는지 알 필요는 없습니다. 그냥 작동합니다. 좋은 이름을 지어 라. 만약 내가 내부를 보더라도 내가 찾은 것에 놀라지 말아야한다. 그것을 사용하는 방법을 기억하기 위해 계속 내부를 들여다 보지 마십시오.

추상화가 이런 식으로 작동한다고 주장하면 그 뒤에있는 레벨의 수는 중요하지 않습니다. 추상화 뒤를 보지 않는 한. 당신은 추상화가 당신의 요구에 적합하지 않다고 주장합니다. 이것이 작동하려면 사용하기 쉽고 이름이 좋으며 누출 되지 않아야 합니다.

그것은 Dependency Injection을 생성 한 태도입니다 (또는 당신이 나와 같은 오래된 학교 인 경우 참조를 전달하십시오). 상속보다 구성과 위임선호하는 것이 좋습니다. 태도는 많은 이름으로 간다. 내가 가장 좋아하는 것은 말하지 말라 .

하루 종일 원칙적으로 익사 할 수있었습니다. 동료가 이미있는 것처럼 들립니다. 그러나 다른 것은 엔지니어링 분야와 달리이 소프트웨어는 100 년도되지 않았습니다. 우리는 여전히 그것을 파악하고 있습니다. 따라서 협박하는 소리 나는 책을 많이 가진 사람이 읽기 어려운 코드를 작성하도록 괴롭히지 마십시오. 그 말을 잘 들으면서 이해한다고 주장하십시오. 믿음으로 아무것도하지 마십시오. 왜 모든 사람의 가장 큰 혼란을 일으키는 지 알지 못하고 이런 방식으로 들었 기 때문에 어떤 식 으로든 코딩하는 사람들.


나는 전적으로 동의합니다. DRY는 3 단어 캐치 프레이즈를 반복하지 않음의 3 글자 약어로 , 위키 의 14 페이지 기사 입니다 . 당신이 모두 맹목적으로 읽고 14 페이지 문서를 이해하지 않고 그 세 글자를 중얼 경우, 당신은 문제로 실행합니다. 또한 OAOO (Once And Only Once) 와 밀접한 관련이 있으며 SPOT (Single Point Of Truth) / SSOT (Single Source Of Truth ) 와 더 느슨하게 관련되어 있습니다.
Jörg W Mittag

"그들의 구현은 완전히 동일 해 보인다. 하나의 방법으로 병합되어야 하는가?" – 반대의 경우도 마찬가지입니다. 두 개의 코드가 다르다고해서 복제되지 않았다는 의미는 아닙니다. OAOO wiki 페이지 에서 Ron Jeffries의 큰 인용문이 있습니다 . "저는 Beck이 거의 완전히 다른 코드의 두 패치를"중복 "으로 선언하고 중복 된 것으로 변경 한 다음 새로 삽입 된 중복을 제거하는 것을 보았습니다. 더 나은 무언가로 "
Jörg W Mittag

물론 @ JörgWMittag. 중요한 것은 아이디어입니다. 다른 모양의 코드로 아이디어를 복제하는 경우 여전히 건조 위반입니다.
candied_orange

나는 자신을 반복하지 않는 14 페이지의 기사가 스스로 반복되는 경향이 있다고 상상해야합니다.
척 아담스

7

우리가 여기와 거기에서 읽는 일반적인 말은 다음과 같습니다.

다른 추상화 계층을 추가하여 모든 문제를 해결할 수 있습니다.

글쎄, 이것은 사실이 아니다! 당신의 예는 그것을 보여줍니다. 따라서 약간 수정 된 문장을 제안합니다 (;-) 자유롭게 재사용하십시오).

올바른 수준의 추상화를 사용하면 모든 문제를 해결할 수 있습니다.

귀하의 경우 두 가지 다른 문제가 있습니다.

  • 추상 수준에서 모든 방법을 추가하여 발생 하는 초과 생성 ;
  • 큰 그림을 얻지 못하고 상실감을 느끼는 구체적인 행동 의 단편화 . Windows 이벤트 루프와 약간 비슷합니다.

둘 다 서로 관련되어 있습니다.

  • 모든 전문화가 다르게하는 방법을 추상화하면 모든 것이 좋습니다. 아무도 특수한 방식으로 Shape계산할 수 있다고 생각하는 데 문제가 없습니다 surface().
  • 일반적인 일반적인 행동 패턴이있는 작업을 추상화하면 다음 두 가지 중에서 선택할 수 있습니다.

    • 모든 전문 분야에서 일반적인 행동을 반복 할 것입니다. 이것은 매우 중복 적입니다. 유지 보수가 어렵고, 특히 공통 부분이 전문화 과정에서 일관되게 유지되도록하기 위해 :
    • 템플릿 메소드 패턴 의 변형을 사용합니다. 이렇게하면 쉽게 특수화 할 수있는 추가 추상 메소드를 사용하여 일반적인 동작을 고려할 수 있습니다. 덜 중복되지만 추가 동작은 크게 분리되는 경향이 있습니다. 너무 많으면 아마도 너무 추상적 인 것입니다.

또한이 방법을 사용하면 디자인 수준에서 추상적 인 커플 링 효과를 얻을 수 있습니다. 새로운 종류의 새로운 특수 동작을 추가 할 때마다이를 추상화하고 추상 부모를 변경하며 다른 모든 클래스를 업데이트해야합니다. 그것은 일종의 변화 전파 가 아닙니다 . 그리고 그것은 전문화에 의존하지 않고 (적어도 디자인에서는) 추상화의 정신에 실제로 있지 않습니다.

나는 당신의 디자인을 모르고 더 많은 것을 도울 수 없습니다. 아마도 그것은 실제로 매우 복잡하고 추상적 인 문제이며 더 좋은 방법은 없습니다. 그러나 확률은 무엇입니까? overgeneralisation의 증상이 여기에 있습니다. 다시 살펴보고 일반화보다 구성을 고려할 때가 될 수 있습니까?


5

동작이 매개 변수 유형을 전환하는 방법을 볼 때마다 해당 방법이 실제로 방법 매개 변수에 속하는지 즉시 고려합니다. 예를 들어 다음과 같은 방법 대신

public void sort(List values) {
    if (values instanceof LinkedList) {
        // do efficient linked list sort
    } else { // ArrayList
        // do efficient array list sort
    }
}

나는 이것을 할 것이다 :

values.sort();

// ...

class ArrayList {
    public void sort() {
        // do efficient array list sort
    }
}

class LinkedList {
    public void sort() {
        // do efficient linked list sort
    }
}

우리는 행동을 언제 사용해야하는지 알고있는 곳으로 옮깁니다. 우리 는 구현의 유형이나 세부 사항을 알 필요가없는 진정한 추상화를 만듭니다 . 상황에 따라이 메소드를 원래 클래스 (내가 호출 할 O)에서 type으로 입력 A하고 재정의하는 것이 더 합리적 일 수 있습니다 B. 메소드가 doIt일부 오브젝트에서 호출 된 경우의 다른 동작으로 이동 doIt하여 A대체하십시오 B. doIt원래 호출 된 위치의 데이터 비트 가 있거나 메소드가 충분한 위치에서 사용되는 경우 원래 메소드를 그대로두고 위임 할 수 있습니다.

class O {
    int x;
    int y;

    public void doIt(A a) {
        a.doIt(this.x, this.y);
    }
}

그래도 조금 더 깊이 뛰어들 수 있습니다. 부울 매개 변수를 대신 사용하라는 제안을 살펴보고 동료가 생각하는 방식에 대해 배울 수있는 내용을 살펴 보겠습니다. 그의 제안은 다음과 같습니다.

public void doIt(A a, boolean isTypeB) {
    if (isTypeB) {
        // do B stuff
    } else { 
        // do A stuff
    }
}

이것은 instanceof우리가 그 검사를 외부화한다는 점을 제외하고는 첫 번째 예에서 사용한 것과 매우 끔찍 합니다. 즉, 다음 두 가지 방법 중 하나로 호출해야합니다.

o.doIt(a, a instanceof B);

또는:

o.doIt(a, true); //or false

첫 번째 방법으로, 콜 포인트는 그 유형이 무엇인지 전혀 모릅니다 A. 따라서 부울을 완전히 아래로 전달해야합니까? 이것이 실제로 코드 기반에서 원하는 패턴입니까? 세 번째로 고려해야 할 유형이 있으면 어떻게됩니까? 이것이 메소드가 호출되는 방식이라면, 우리는 그 메소드를 타입으로 옮기고 시스템이 다형성으로 구현을 선택하도록해야합니다.

두 번째 방법으로, 콜 포인트 의 유형을 이미 알고 있어야합니다a . 일반적으로 인스턴스를 작성하거나 해당 유형의 인스턴스를 매개 변수로 사용함을 의미합니다. 의 방법 작성 O하는 것은 소요 B일하는 것이 여기. 컴파일러는 어떤 방법을 선택할지 알 것입니다. 우리가 이런 변화를 겪고있을 때, 우리가 실제로 어디로 가고 있는지 알아낼 때까지 중복은 잘못된 추상화를 만드는 것보다 낫습니다 . 물론, 우리가이 시점으로 무엇을 변경했는지에 상관없이 실제로 수행되지 않았 음을 제안합니다.

우리 사이의 관계를 좀 더 자세히 살펴볼 필요 A하고 B. 일반적으로 상속보다는 구성을 선호 해야한다는 말이 있습니다. 이 모든 경우에 사실이 아니다, 그러나 우리가 파고 일단은 케이스의 놀라운 숫자에 해당됩니다. B에서 상속 A, 우리가 믿는 것을 의미 B입니다 A. 약간 다르게 작동한다는 점을 제외하고는 그대로 B사용해야합니다 A. 그러나 그 차이점은 무엇입니까? 차이점을 좀 더 구체적으로 지정할 수 있습니까? 그렇지 않은 B입니다 A,하지만 정말 AX될 수있는 A'또는 B'? 그렇게하면 코드는 어떻게 생겼습니까?

우리가 위에 방법 이동 한 경우 A이전 제안을, 우리의 인스턴스 주입 수 X로를 A, 그리고 해당 방법을 위임 X:

class A {
    X x;
    A(X x) {
        this.x = x;
    }

    public void doIt(int x, int y) {
        x.doIt(x, y);
    }
}

구현 A'하고 B'제거 할 수 있습니다 B. 보다 암묵적인 개념에 이름을 부여하여 코드를 개선했으며 런타임에 컴파일 시간 대신 해당 동작을 설정할 수있었습니다. A실제로 덜 추상적이되었습니다. 확장 된 상속 관계 대신 위임 된 개체에서 메서드를 호출합니다. 그 객체는 추상적이지만 구현상의 차이점에만 더 중점을 둡니다.

그래도 마지막으로 볼 것이 있습니다. 동료의 제안으로 롤백합시다. 모든 콜 사이트에서 우리 A가 보유 하고있는 유형을 명시 적으로 알고 있다면 다음과 같이 호출해야합니다.

B b = new B();
o.doIt(b, true);

즉 작성할 때 우리는 이전에 가정 AX중 하나입니다 A'또는 B'. 그러나 아마도이 가정조차 올바르지 않습니다. 이 유일한 장소 사이의 차이 AB문제? 그렇다면 약간 다른 접근법을 취할 수 있습니다. 우리는 여전히이 X중 하나입니다을 A'하거나 B',하지만 속하지 않는 A. O.doIt그것에 대해서만 관심이 있으므로 전달하십시오 O.doIt.

class O {
    int x;
    int y;

    public void doIt(A a, X x) {
        x.doIt(a, x, y);
    }
}

이제 우리의 콜 사이트는 다음과 같습니다 :

A a = new A();
o.doIt(a, new B'());

다시 한 번 B사라지고 추상화가 더 집중적으로 이동합니다 X. 그러나 이번에 A는 덜 알면 훨씬 간단합니다. 훨씬 덜 추상적입니다.

코드베이스에서 중복을 줄이는 것이 중요하지만, 우선 중복이 발생하는 이유를 고려해야합니다. 복제는 나 가려고하는 더 깊은 추상화의 표시 일 수 있습니다.


1
여기서 제공하는 "나쁜"코드 예제가 비 OO 언어로 수행하려는 경향과 비슷하다는 사실이 나에게 놀랍습니다. 그들이 잘못된 교훈을 배우고 코딩 방식으로 OO 세계로 가져 왔는지 궁금합니다.
Baldrickk

1
@Baldrickk 각 패러다임에는 고유 한 장단점이있는 고유 한 사고 방식이 있습니다. 기능적 Haskell에서는 패턴 일치가 더 나은 방법입니다. 그런 언어로되어 있지만 원래 문제의 일부 측면도 가능하지 않습니다.
cbojar

1
이것이 정답입니다. 작동하는 유형에 따라 구현을 변경하는 방법은 해당 유형의 방법이어야합니다.
Roman Reiner

0

상속에 의한 추상화 는 매우 추악해질 수 있습니다. 일반적인 팩토리가있는 병렬 클래스 계층 구조 리팩토링은 두통이 될 수 있습니다. 그리고 나중에 개발, 당신이있는 곳.

확장 점 , 엄격한 추상화 및 계층화 된 사용자 정의 등의 대안이 있습니다 . 특정 도시에 대한 사용자 정의를 기반으로 한 정부 고객의 사용자 정의를 말하십시오.

경고 : 불행히도 이것은 모든 (또는 대부분의) 클래스를 확장 할 때 가장 잘 작동합니다. 작은 옵션이 없습니다.

이 확장 성은 확장 가능한 객체 기본 클래스가 확장을 보유하도록하여 작동합니다.

void f(CreditorBO creditor) {
    creditor.as(AllowedCreditorBO.class).ifPresent(allowedCreditor -> ...);
}

내부적으로 확장 클래스별로 확장 된 개체에 대한 개체의 지연 매핑이 있습니다.

GUI 클래스 및 구성 요소의 경우 부분적으로 상속과 동일한 확장 성입니다. 버튼 추가 등.

귀하의 경우 유효성 검사는 확장인지 확인하고 확장에 대해 자체 검증해야합니다. 한 가지 경우에만 확장 점을 도입하면 이해할 수없는 코드가 추가됩니다.

따라서 현재 컨텍스트에서 작동하려고하는 해결책은 없습니다.


0

'숨겨진 흐름 제어'가 너무 손으로 들립니다.
문맥에서 벗어난 구성이나 요소는 그 특성을 가질 수 있습니다.

추상화가 좋습니다. 나는 두 가지 지침으로 그들을 강화합니다.

  • 너무 빨리 추상화하지 않는 것이 좋습니다. 결석하기 전에 더 많은 패턴의 예를 기다리십시오. '더보기'는 물론 주관적이고 어려운 상황에 따라 다릅니다.

  • 추상화가 좋기 때문에 너무 많은 추상화 수준을 피하십시오. 프로그래머는 코드베이스를 연결하고 12 레벨까지 갈 때 새로운 또는 변경된 코드를 위해 해당 레벨을 유지해야합니다. 잘 짜여진 코드에 대한 욕구는 많은 사람들이 따르기 어려운 수준으로 이어질 수 있습니다. 이것은 또한 '닌자 유지 관리 전용'코드베이스로 이어집니다.

두 경우 모두 '더 많은'과 '너무 많은'은 고정 된 숫자가 아닙니다. 때에 따라 다르지. 그것이 어려운 이유입니다.

나는 또한 Sandi Metz의이 글을 좋아한다

https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction

복제가 잘못된 추상화보다 훨씬 저렴
하고
잘못된 추상화를 통해 중복을 선호

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