단일 책임 원칙-코드 조각화를 피하려면 어떻게해야합니까?


56

팀 리더가 SOLID 개발 원칙을 강력하게 옹호하는 팀에서 일하고 있습니다. 그러나 그는 복잡한 소프트웨어를 집 밖으로 가져 오는 데 많은 경험이 부족합니다.

우리는 이미 매우 복잡한 코드 기반에 SRP를 적용한 상황에 처해 있으며, 이제는 매우 조각화되어 이해하고 디버깅하기가 어렵습니다.

개인 또는 보호 된 클래스 내의 메소드가 '변경 이유'를 나타내는 것으로 판단되어 공개 또는 내부 클래스 및 인터페이스로 추출되었으므로 이제 코드 단편화뿐만 아니라 캡슐화에도 문제가 있습니다. 응용 프로그램의 캡슐화 목표와 일치하지 않습니다.

우리는 20 개 이상의 인터페이스 매개 변수를받는 클래스 생성자를 가지고 있으므로 IoC 등록 및 해결은 그 자체로 괴물이되고 있습니다.

이러한 문제 중 일부를 해결하는 데 사용할 수있는 'SRP에서 리팩터링'접근법이 있는지 알고 싶습니다. 나는 밀접하게 관련된 클래스를 '포장'하여 기능의 합계에 대한 단일 지점의 액세스를 제공하는 빈 거친 클래스를 만들면 SOLID를 위반하지 않는다는 것을 읽었습니다. 과도하게 SRP의 클래스 구현).

그 외에도, 우리 모두를 행복하게 유지하면서 실질적으로 개발 노력을 계속할 수있는 솔루션을 생각할 수 없습니다.

어떤 제안?


18
그건 내 의견 일뿐입니다. 규칙의 하나가 더 있다고 생각합니다.이 규칙은 다양한 약어 (공통 감지 원리)의 더미에서 매우 쉽게 잊혀집니다. '솔루션'이 실제로 해결되는 더 많은 문제를 만들면 무언가 잘못되었습니다. 문제는 복잡하지만 복잡성을 처리하고 여전히 디버깅하기 쉬운 클래스에 포함되어 있다면 혼자 남겨 두는 것입니다. 일반적으로 귀하의 '래퍼 (wrapper)'아이디어는 나에게 들리지만, 나는 더 지식이 많은 사람에게 답을 남길 것입니다.
Patryk Ćwiek

6
'변경 이유'는 모든 이유를 조기에 추측 할 필요가 없습니다. 실제로 변경해야 할 때까지 기다렸다가 나중에 이러한 종류의 변경을 더 쉽게하기 위해 수행 할 수있는 작업을 확인하십시오.

62
20 개의 생성자 매개 변수가있는 클래스는 나에게 SRP처럼 들리지 않습니다!
MattDavey

1
"... IoC 등록 및 해결 ..."; 이것은 마치 (또는 팀장이) "IoC"와 "종속성 주입"(DI)이 같은 것으로 생각하는 것처럼 들립니다. DI는 IoC를 달성하기위한 수단이지만 확실히 유일한 것은 아닙니다. IoC를 수행하려는 이유 를 신중하게 분석해야합니다 . 단위 테스트를 작성하려고한다면 서비스 로케이터 패턴을 사용하거나 단순히 인터페이스 클래스 ( ISomething)를 사용해보십시오 . IMHO, 이러한 접근 방식은 종속성 주입보다 처리하기가 훨씬 쉽고 코드를 더 읽기 쉽게 만듭니다.

2
여기에 주어진 대답은 진공 상태 일 것입니다. 특정 응답을 제공하기 위해 코드를 확인해야합니다. 생성자에 20 개의 매개 변수가 있습니까? 글쎄, 당신은 객체가 없거나 ... 모두 유효 할 수도 있습니다. 또는 구성 파일에 속하거나 DI 클래스에 속할 수 있습니다. 또는 증상이 의심스러운 것처럼 들리지만 CS의 대부분의 항목과 마찬가지로 "의존합니다"...
Steven A. Lowe

답변:


84

클래스에 생성자에 20 개의 매개 변수가있는 경우 팀이 SRP가 무엇인지 잘 알고있는 것처럼 들리지 않습니다. 한 가지만 수행하는 클래스가있는 경우 종속성이 어떻게 20입니까? 낚시 여행을하면서 낚싯대, 태클 박스, 퀼팅 용품, 볼링 공, nunchucks, 화염 방사기 등을 가져 오는 것과 같습니다.

즉, SRP는 대부분의 원칙과 마찬가지로 과도하게 적용될 수 있습니다. 정수를 증가시키기 위해 새 클래스를 만들면 단일 책임이 될 수 있지만 계속됩니다. 말도 안돼. 우리는 SOLID 원칙과 같은 것이 목적을 위해 존재한다는 것을 잊어 버리는 경향이 있습니다. SOLID는 목적 자체가 아니라 목적을위한 수단입니다. 끝은 유지 보수성 입니다. Single Responsibility Principle을 통해 세분화 된 정보를 얻으려면 SOLID에 대한 열정이 팀을 SOLID의 목표로 눈을 멀게 한 지표입니다.

그래서, 내가 말하는 것은 ... SRP는 당신의 문제가 아니라는 것입니다. 그것은 SRP에 대한 오해 또는 엄청나게 세분화 된 적용입니다. 팀이 주요한 것을 주요하게 유지하도록하십시오. 그리고 가장 중요한 것은 유지 보수성입니다.

편집하다

사람들이 사용 편의성을 장려하는 방식으로 모듈을 설계하도록하십시오. 각 클래스를 미니 API로 생각하십시오. 먼저 "이 클래스를 어떻게 사용하고 싶습니까?"라고 생각한 다음 구현하십시오. "이 수업에서해야 할 일"만 생각하지 마십시오. SRP에는 사용하기 어려워 클래스를 만들 수있는 좋은 경향을 가지고 있다면 당신은 유용성에 많은 생각을 넣지 마십시오을.

편집 2

리팩토링에 대한 팁을 찾고 있다면 제안한 작업을 시작할 수 있습니다. 더 세분화 된 클래스를 만들어 여러 다른 클래스를 래핑하십시오. 성긴 클래스는 여전히 SRP를 준수 하지만 더 높은 수준 이어야 합니다. 그런 다음 두 가지 대안이 있습니다.

  1. 더 세분화 된 클래스가 더 이상 시스템의 다른 곳에서 사용되지 않는 경우 점차적으로 구현을 더 세분화 된 클래스로 끌어 와서 삭제할 수 있습니다.
  2. 세밀한 수업은 그대로 두십시오. 아마도 그것들은 잘 설계되었으며 사용하기 쉽도록 래퍼가 필요했습니다. 나는 이것이 많은 프로젝트의 경우라고 생각합니다.

리팩토링이 끝나면 (리포지토리에 커밋하기 전에) 작업을 검토하고 리팩토링이 실제로 유지 관리 성과 사용 편의성을 개선 한 것인지 자문 해보십시오.


2
사람들이 클래스 디자인에 대해 생각하게하는 다른 방법 : CRC 카드 (클래스 이름, 책임, 공동 작업자)를 작성하게하십시오 . 수업에 공동 작업자 또는 책임이 너무 많은 경우 SRP가 충분하지 않을 수 있습니다. 즉, 모든 텍스트가 색인 카드에 맞아야하거나 그렇지 않은 경우 너무 많이합니다.
Spoike

18
화염 방사기의 용도는 무엇인지 알지만 어떻게 도대체 어떻게 낚시를합니까?
R. Martinho Fernandes

13
+1 SOLID는 목적 자체가 아니라 목적을위한 수단입니다.
B 세븐

1
+1 : "Demeter의 법칙"과 같은 것들의 이름이 잘못되었다고 주장했습니다. "Demeter의 안내선"이어야합니다. 이런 것들이 당신을 위해 일해야하고, 당신을 위해 일해서는 안됩니다.
이진 걱정

2
@EmmadKareem : DAO 개체에는 여러 속성이 있다고 가정합니다. 그러나 Customer클래스 처럼 단순한 것으로 그룹화 하고 유지 관리하기 쉬운 코드를 가질 수있는 몇 가지 사항이 있습니다 . 여기 예를보십시오 : codemonkeyism.com/…
Spoike

32

Martin Fowler의 Refactoring에서 한 번 SRP에 대한 반대 규칙을 읽고 너무 멀리 가고있는 곳을 정의한다고 생각합니다. "모든 수업에 한 가지 이유 만 바꾸어야합니까?"라는 중요한 두 번째 질문이 있습니다. 그리고 "모든 변경은 하나의 클래스에만 영향을 줍니까?"

첫 번째 질문에 대한 대답이 모든 경우에 "예"이지만 두 번째 질문이 "가까운 것도 아닙니다"이면 SRP를 구현하는 방법을 다시 살펴 봐야합니다.

예를 들어, 테이블에 하나의 필드를 추가하면 DTO 및 유효성 검사기 클래스와 지속성 클래스 및 뷰 모델 개체 등을 변경해야한다는 문제가 발생합니다. 아마도 SRP를 어떻게 구현했는지 다시 생각해야 할 것입니다.

아마도 필드를 추가하는 것이 Customer 객체를 변경하는 이유라고 말했지만 XML 파일에서 데이터베이스로 지속성 계층을 변경하는 것이 Customer 객체를 변경하는 또 다른 이유입니다. 따라서 CustomerPersistence 오브젝트도 작성하기로 결정했습니다. 그러나 필드를 여전히 추가하려면 CustomerPersisitence 오브젝트를 변경해야합니다. 그렇다면 요점은 무엇입니까? 변경해야 할 두 가지 이유가있는 개체가 여전히 있습니다. 더 이상 고객이 아닙니다.

그러나 ORM을 도입하면 클래스를 작동시켜 DTO에 필드를 추가하면 해당 데이터를 읽는 데 사용되는 SQL이 자동으로 변경되도록 클래스를 작동시킬 수 있습니다. 그런 다음 두 가지 문제를 분리해야 할 충분한 이유가 있습니다.

요약하면 다음과 같습니다. "아니요,이 개체를 변경해야하는 여러 가지 이유가 있습니다"라는 횟수와 "아니요,이 변경은 "여러 개체에 영향을 미칩니다."라고 생각하면 SRP와 조각화간에 균형이 맞다고 생각합니다. 그러나 둘 다 여전히 높으면 관심을 분리 할 수있는 다른 방법이 있는지 궁금해하기 시작합니다.


"모든 변경 사항이 한 클래스에만 영향을 줍니까?"+1
dj18

내가 보지 못한 관련 문제는 하나의 논리적 엔터티에 연결된 작업이 다른 클래스로 조각난 경우 코드가 모두 동일한 엔터티에 연결된 여러 개의 개별 객체에 대한 참조를 보유해야 할 수도 있다는 것입니다. 예를 들어 "SetHeaterOutput"및 "MeasureTemperature"기능이있는 가마를 고려하십시오. 킬른이 독립적 인 HeaterControl 및 TemperatureSensor, 객체로 표현 된 경우 TemperatureFeedbackSystem 객체가 하나의 킬른 히터와 다른 킬른 온도 센서에 대한 참조를 보유하는 것을 막을 수있는 것은 없습니다.
supercat

1
대신 이러한 함수가 Kiln 객체에 의해 구현 된 IKiln 인터페이스로 결합 된 경우 TemperatureFeedbackSystem은 단일 IKiln 참조 만 보유하면됩니다. 독립적 인 애프터 마켓 온도 센서와 함께 가마를 사용해야한다면, 생성자가 IHeaterControl 및 ITemperatureSensor를 허용하고이를 사용하여 IKiln을 구현 한 CompositeKiln 객체를 사용할 수 있지만, 이러한 의도적으로 느슨한 구성은 코드에서 쉽게 인식 할 수 있습니다.
supercat

23

시스템이 복잡 하다고해서 시스템을 복잡하게 만들어야한다는 의미는 아닙니다 . 다음과 같이 종속성 (또는 공동 작업자)이 너무 많은 클래스가있는 경우 :

public class MyAwesomeClass {
    public class MyAwesomeClass(IDependency1 _d1, IDependency2 _d2, ... , IDependency20 _d20) {
      // Assign it all
    }
}

... 너무 복잡 해져서 실제로 SRP를 따르지 않고 있습니까? 당신이 무엇을 기록한 경우 나는 셨을 텐데요 MyAwesomeClassA의 수행 CRC 카드 는 인덱스 카드에 맞지 않는 것 또는 당신이 정말로 작은 읽기 어려운 문자에 작성해야합니다.

당신이 여기에있는 것은 당신의 사람들이 대신 인터페이스 분리 원칙을 따랐으며 극단적으로 가져 갔을 수도 있지만 그것은 완전히 다른 이야기입니다. 의존성이 도메인 객체 (일어남)라고 주장 할 수는 있지만 동시에 20 개의 도메인 객체를 처리하는 클래스를 사용하면 너무 멀리 확장됩니다.

TDD는 수업이 얼마나 많은지를 보여주는 좋은 지표입니다. 둔하게 넣어; 테스트 방법에 작성하는 데 시간 MyAwesomeClass이 오래 걸리는 설정 코드가있는 경우 (테스트 리팩터링하더라도) 할 일이 너무 많습니다.

이 수수께끼를 어떻게 해결합니까? 책임을 다른 수업으로 옮깁니다. 이 문제가있는 수업에서 취할 수있는 몇 가지 단계가 있습니다.

  1. 여러분의 학급이 그들과 함께하는 모든 행동 (또는 책임)을 파악하십시오.
  2. 밀접하게 관련된 종속성에 따라 작업을 그룹화하십시오.
  3. 재 선교! 즉, 식별 된 각 조치를 새 클래스 또는 다른 클래스로 리팩토링합니다.

리팩토링 책임에 대한 추상적 인 예

하자 C여러 종속성이 클래스가 될 D1, D2, D3, D4적은 사용 리팩토링 할 필요가있다. C의존성 을 호출 하는 메소드를 식별 하면 간단한 목록을 작성할 수 있습니다.

  • D1- performA(D2),performB()
  • D2 - performD(D1)
  • D3 - performE()
  • D4 - performF(D3)

목록을 보면 우리가 볼 수 D1D2클래스가 어떻게 든 함께 필요에 따라 서로 관련이 있습니다. 우리는 또한 그 D4요구를 볼 수 있습니다 D3. 그래서 우리는 두 그룹화가 있습니다 :

  • Group 1- D1<->D2
  • Group 2-- D4>D3

그룹화는 이제 클래스에 두 가지 책임이 있음을 나타냅니다.

  1. Group 1-서로 필요한 두 객체 호출을 처리하기위한 것입니다. 어쩌면 클래스 C가 두 종속성을 모두 처리해야 할 필요성을 없애고 그 중 하나를 대신 호출 하도록 할 수 있습니다 . 이 그룹에서는에 D1대한 참조 가 있을 수 있습니다 D2.
  2. Group 2-다른 책임은 한 개체가 다른 개체를 호출해야합니다. 수업 대신 D4처리 할 수 없습니까 D3? 그런 다음 우리는 아마 제거 할 수있는 D3클래스에서 C시켜서 D4대신 호출을한다.

예제가 매우 추상적이며 많은 가정을하므로 돌로 설정된 대답을 취하지 마십시오. 나는 이것을 리팩토링하는 더 많은 방법이 있다고 확신하지만, 적어도 단계는 클래스를 나누는 대신 책임을 옮기는 일종의 프로세스를 얻는 데 도움이 될 수 있습니다.


편집하다:

댓글 중 @Emmad Karem의 말 :

"클래스에 생성자에 20 개의 매개 변수가있는 경우 팀에서 SRP가 무엇인지 잘 알고있는 것처럼 들리지 않습니다. 한 가지만 수행하는 클래스가있는 경우 20 개의 종속성이있는 방법은 무엇입니까?"- Customer 클래스가 있으면 생성자에 20 개의 매개 변수가있는 것이 이상하지 않습니다.

DAO 객체에는 생성자에서 설정해야하는 많은 매개 변수가있는 경향이 있으며 매개 변수는 일반적으로 문자열과 같은 간단한 유형입니다. 그러나 Customer클래스 의 예에서 다른 클래스 내에 속성을 그룹화하여 작업을 단순화 할 수 있습니다. Address거리 가있는 클래스와 Zipcode우편 번호가 포함 된 클래스가 있고 데이터 유효성 검사와 같은 비즈니스 로직도 처리합니다.

public class Address {
    private String street1;
    //...

    private Zipcode zipcode;

    // easy to extend
    public bool isValid() {
        return zipcode.isValid();
    }
}

public class Zipcode {
    private string zipcode;
    public bool isValid() {
        // return regex match that zipcode contains numbers
    }
}

이 내용은 블로그 게시물 "Java에서 String을 사용하지 마십시오 (또는 자주 사용하지 마십시오)" 에서 자세히 설명 합니다. 하위 객체를보다 쉽게 ​​만들 수 있도록 생성자 또는 정적 메서드를 사용하는 대신 유체 빌더 패턴을 사용할 수 있습니다 .


+1 : 정답입니다! 그룹화는 재귀 적으로 그룹화를 적용 할 수 있기 때문에 IMO는 매우 강력한 메커니즘입니다. 매우 대략적으로 n 개의 추상화 레이어를 사용하면 2 ^ n 개의 항목을 구성 할 수 있습니다.
조르지오

+1 : 처음 몇 단락은 팀이 직면 한 것과 정확히 일치합니다. 실제로는 서비스 객체 인 "Business Objects"및 작성해야 할 단위 테스트 설정 코드입니다. 서비스 계층 호출에 한 줄의 코드가 포함될 때 문제가 있음을 알고있었습니다. 비즈니스 계층 메소드에 대한 호출.
Man

3

나는 SRP에 대한 모든 답변과 그것을 너무 멀리 가져갈 수있는 방법에 동의합니다. 귀하의 게시물에서 SRP를 준수하는 "과도 리팩토링"으로 인해 캡슐화가 깨지거나 수정 된 것으로 나타났습니다. 나를 위해 일한 한 가지는 항상 기본을 고수하고 목표를 달성하는 데 필요한 것을 정확하게 수행하는 것입니다.

레거시 시스템으로 작업 할 때 모든 것을 개선하기위한 "열정"은 일반적으로 팀 리더, 특히 해당 역할에 새로운 팀 리더에서 상당히 높습니다. SOLID에는 SRP가 없습니다. S 만 있습니다. SOLID를 따르는 경우 OLID도 잊지 마십시오.

나는 지금 레거시 시스템에서 작업하고 있으며 우리는 처음에 비슷한 길을 가기 시작했습니다. 우리에게 도움이 된 것은 SOLID와 KISS (Keep It Simple Stupid)라는 두 가지 이점을 모두 활용하려는 단체 팀의 결정 이었습니다 . 우리는 코드 구조에 대한 주요 변경 사항을 종합적으로 논의하고 다양한 개발 원칙을 적용 할 때 상식을 적용했습니다. "S / W 개발법"이 아닌 지침으로 훌륭합니다. 팀은 단순한 팀 리더가 아니라 팀의 모든 개발자에 관한 것입니다. 항상 나를 위해 일한 것은 방에있는 모든 사람을 데리고 팀 전체가 동의하는 공유 지침 세트를 만들어내는 것입니다.

현재 상황을 수정하는 방법과 관련하여 VCS를 사용하고 응용 프로그램에 너무 많은 새로운 기능을 추가하지 않은 경우 항상 팀 전체가 이해 가능하고 읽기 쉽고 유지 관리 가능하다고 생각하는 코드 버전으로 돌아갈 수 있습니다. 예! 나는 일을 버리고 처음부터 시작하도록 요구하고 있습니다. 고장난 것을 "고정"하여 기존의 것으로 다시 옮기는 것보다 낫습니다.


3

그 해답은 무엇보다도 코드의 유지 관리 성과 명확성입니다. 그것은 저것 보다 적은 코드를 쓰는 것을 의미 합니다. 적은 추상화, 적은 인터페이스, 적은 옵션, 적은 매개 변수.

코드 구조를 평가하거나 새로운 기능을 추가 할 때마다 실제 로직에 비해 보일러 플레이트가 얼마나 필요한지 생각합니다. 답이 50 %를 초과하면 아마도 내가 생각을하고있는 것입니다.

SRP 외에도 많은 개발 스타일이 있습니다. 귀하의 경우 YAGNI와 같은 소리는 분명히 부족합니다.


3

여기서 많은 답변이 실제로 좋지만이 문제의 기술적 측면에 중점을 둡니다. 개발자가 실제로 SRP를 위반하는 것처럼 SRP 사운드를 따르려고 시도하는 것처럼 들립니다.

당신은 밥의 블로그를 볼 수 있습니다 여기에 이 상황에 있지만, 그는 책임은 다음 여러 클래스에 걸쳐 지저분하면 해당 클래스가 병렬로 변경하기 때문에 책임 SRP을 위반한다고 주장한다. 나는 당신의 개발자가 Bob의 블로그 맨 위에있는 디자인을 정말로 좋아할 것이라고 생각합니다. 특히 "공통 폐쇄 원칙"을 위반하기 때문에 함께 변하는 것들이 함께 유지됩니다.

SRP는 "한 가지 일"이 아니라 "변경 사유"를 나타내며, 실제로 변경이 발생할 때까지 변경 사유에 대해 걱정할 필요가 없습니다. 두 번째 사람은 추상화 비용을 지불합니다.

이제 두 번째 문제인 "SOLID 개발의 악의적 인 옹호자"가 있습니다. 이 개발자와 큰 관계가있는 것 같지는 않으므로 코드베이스의 문제를 납득시키려는 시도가 엉망입니다. 문제를 실제로 논의 할 수 있도록 관계를 복구해야합니다. 내가 추천하는 것은 맥주입니다.

커피 숍으로 향하지 않으면 심각하지 않습니다. 비공식적으로 이야기 할 수있는 사무실 밖에서 휴식을 취하십시오. 당신이하지 않을 회의에서 논쟁에서 이기기 위해 노력하는 것보다는 재미있는 곳에서 토론을하십시오. 당신을 미치게 만드는이 개발자는 소프트웨어를 "집 밖으로"가져 오려고 노력하고 쓰레기를 내보내고 싶지 않은 실제 기능을하는 사람이라는 것을 인식하십시오. 공통점을 공유 할 가능성이 있으므로 SRP를 준수하면서 디자인을 개선하는 방법에 대해 논의 할 수 있습니다.

SRP가 좋은 것임을 인식하고 측면을 다르게 해석한다는 점을 인정할 수 있다면 생산적인 대화를 시작할 수 있습니다.


-1

SRP가 일반적으로 좋은 생각이라는 팀 리더의 결정 [update = 2012.05.31]에 동의합니다. 그러나 나는 20 개의 인터페이스 인자를 가진 생성자가 훨씬 멀다는 @ Spoike -s 의견에 전적으로 동의한다. [/ update] :

IoC와 함께 SRP를 도입하면 하나의 "다중 응답 클래스"에서 많은 SRP 클래스로 복잡성이 이동하고 다음과 같은 이점이 있습니다.

  • 보다 쉬운 단위 테스트 / tdd (한 번에 하나의 srp 클래스 테스트)
  • 하지만 ~의 비용으로
    • 훨씬 더 어려운 코드 초기화 및 통합
    • 더 어려운 디버깅
    • 조각화 (= 여러 파일 / 디렉토리에 코드 배포)

srp를 희생시키지 않으면 코드 조각화를 줄일 수 없습니다.

그러나 생성자의 초기화 복잡성을 숨기는 구문 설탕 클래스를 구현하여 코드 초기화의 "고통을 완화"할 수 있습니다.

   class MySrpClass {
      MySrpClass(Interface1 parm1, Interface2 param2, .... Interface20 param2) {
      }
   } 

   class MySyntaxSugarClass : MySrpClass {
      MySyntaxSugarClass() {
         super(new MyInterface1Implementation(), new MyImpl2(), ....)
      }
   }

2
나는 20 개의 인터페이스가 클래스가 너무 많은 것을 나타내는 지표라고 생각합니다. 즉, 변경해야 할 20 가지 이유가 있으며 이는 SRP를 위반하는 것입니다. 시스템이 복잡하다고해서 시스템이 복잡해야한다는 의미는 아닙니다.
Spoike
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.