SRP를 구현하는 실용적인 방법은 무엇입니까?


11

클래스가 단일 책임 원칙을 위반하는지 확인하기 위해 사람들이 실제로 사용하는 기술은 무엇입니까?

나는 수업이 바뀌어야 할 이유가 하나 밖에 없다는 것을 알고 있지만, 그 문장에는 실제로 그것을 구현할 실질적인 방법이 부족하다.

내가 찾은 유일한 방법은 ".........해야합니다 ......... 자체" 문장을 사용하는 것입니다. 여기서 첫 번째 공백은 클래스 이름이고 나중에 공백은 메서드 (책임) 이름입니다.

그러나 때로는 책임이 실제로 SRP를 위반하는지 파악하기가 어렵습니다.

SRP를 확인하는 방법이 더 있습니까?

노트 :

문제는 SRP의 의미에 대한 것이 아니라 SRP를 확인하고 구현하기위한 실용적인 방법론 또는 일련의 단계입니다.

최신 정보

수업

SRP를 분명히 위반하는 샘플 클래스를 추가했습니다. 사람들이 단일 책임 원칙에 접근하는 방법을 설명하기 위해 예제로 사용할 수 있다면 좋을 것입니다.

예는 여기에서 입니다.


이것은 흥미로운 규칙이지만 "사람 클래스는 스스로 렌더링 할 수 있습니다"라고 여전히 쓸 수 있습니다. 비즈니스 규칙과 데이터 지속성을 포함하는 동일한 클래스에 GUI를 포함시키는 것은 좋지 않기 때문에 이는 SRP 위반으로 간주 될 수 있습니다. 따라서 아키텍처 도메인 (계층 및 계층)의 개념을 추가하고이 명령문이 해당 도메인 중 하나 (예 : GUI, 데이터 액세스 등)에서만 유효해야합니다.
NoChance

@EmmadKareem이 규칙은 Head First Object-Oriented Analysis and Design 에서 언급되었으며 이것이 바로 제가 생각한 것입니다. 그것을 구현하는 실용적인 방법이 다소 부족합니다. 그들은 때때로 책임이 디자이너에게는 명백하지 않을 것이라고 언급했으며, 그 방법이 실제로이 클래스에 있어야하는지 판단하기 위해 많은 상식을 사용해야했습니다.
Songo

SRP를 정말로 이해하려면 Bob Martin 아저씨의 글을 읽어보십시오. 그의 코드는 내가 본 것 중 가장 예쁘고, SRP에 대해 말하는 것은 건전한 조언 일뿐만 아니라 손을 내밀는 것 이상이라고 믿습니다.
Robert Harvey

그리고 다운 투표자는 게시물을 개선해야하는 이유를 설명해 주시겠습니까?!
Songo

답변:


7

SRP는 확실하지 않은 용어로 클래스가 변경해야 할 이유가 하나만 있어야한다고 언급합니다.

질문에서 "report"클래스를 해체 할 때 세 가지 방법이 있습니다.

  • printReport
  • getReportData
  • formatReport

Report모든 방법에서 중복 이 사용되는 것을 무시하면 이것이 왜 SRP를 위반하는지 쉽게 알 수 있습니다.

  • "인쇄"라는 용어는 일종의 UI 또는 실제 프린터를 의미합니다. 따라서이 클래스에는 일정량의 UI 또는 프리젠 테이션 로직이 포함됩니다. UI 요구 사항을 변경하려면 Report클래스를 변경해야합니다 .

  • "데이터"라는 용어는 어떤 종류의 데이터 구조를 의미하지만 실제로 무엇을 지정하지는 않습니다 (XML? JSON? CSV?). 그럼에도 불구하고 보고서의 "내용"이 변경되면이 방법도 변경됩니다. 데이터베이스 또는 도메인에 연결되어 있습니다.

  • formatReport는 일반적으로 메서드의 끔찍한 이름이지만 UI와 관련이 있으며 UI와는 다른 측면이 있다고 생각 printReport합니다. 따라서 관련이없는 또 다른 이유가 있습니다.

따라서이 클래스는 데이터베이스, 스크린 / 프린터 장치 로그 나 파일 출력 등을위한 내부 형식 지정 논리 결합 될 수 있습니다 . 한 클래스에 세 가지 함수를 모두 사용하면 종속성 수를 곱하고 종속성 또는 요구 사항 변경으로 인해이 클래스 (또는 그에 의존하는 다른 클래스)가 중단 될 확률이 3 배가됩니다.

여기서 문제의 일부는 특히 가시적 인 예를 골랐다는 것입니다. 한 가지만Report 수행하더라도. 라는 클래스가 없어야합니다 . 왜냐하면 ... 어떤 보고서입니까? 다른 데이터와 다른 요구 사항에 따라 모든 "보고"가 완전히 다른 짐승이 아닌가? 그리고 스크린이나 인쇄용으로 이미 포맷 된 보고서가 아닌가 ?

그러나 과거를 살펴보고 가상의 구체적인 이름을 만들어 봅시다. IncomeStatement하나는 매우 일반적인 보고서입니다. 적절한 "SRP"아키텍처는 세 가지 유형이 있습니다.

  • IncomeStatement- 형식이 지정된 보고서에 나타나는 정보 를 포함 및 / 또는 계산 하는 도메인 및 / 또는 모델 클래스 .

  • IncomeStatementPrinter, 아마도 같은 표준 인터페이스를 구현할 것입니다 IPrintable<T>. 하나의 주요 방법 Print(IncomeStatement)과 인쇄 별 설정 구성을위한 다른 방법 또는 속성이 있습니다.

  • IncomeStatementRenderer화면 렌더링을 처리하며 프린터 클래스와 매우 유사합니다.

  • 또한 IncomeStatementExporter/ 와 같은 기능별 클래스를 추가 할 수도 있습니다 IExportable<TReport, TFormat>.

이것은 제네릭 및 IoC 컨테이너를 도입하여 현대 언어에서 훨씬 더 쉬워졌습니다. 대부분의 응용 프로그램 코드는 특정 IncomeStatementPrinter클래스 에 의존 할 필요가 없으며 모든 종류의 인쇄 가능한 보고서를 사용 IPrintable<T>하여 작동 할 수 있습니다.이 방법을 사용 하면 기본 클래스 의 모든 이점을 방법과 함께 제공하며 일반적인 SRP 위반은 없습니다 . 실제 구현은 IoC 컨테이너 등록에서 한 번만 선언하면됩니다.Reportprint

일부 사람들은 위의 디자인에 직면했을 때 다음과 같이 응답합니다. "이것은 절차 코드처럼 보이며 OOP의 요점은 데이터와 동작의 분리로부터 우리를 떠나는 것입니다!" 내가 말하는 것 : wrong .

IncomeStatement것입니다 하지 그냥 "데이터", 및 상기 실수는 그들이에 관련이없는 기능을 모든 종류의 방해 시작 이후 이러한 "투명"클래스를 생성하여 뭔가 잘못을하고있다 느낌 OOP의 사람들의 많은 원인이 무엇 인 IncomeStatement것을, (물론 게으름). 이 클래스는 데이터로 시작될 수 있지만 시간이 지남에 따라 더 많은 모델이 될 것 입니다.

예를 들어 실제 손익 계산서에는 총 수익 , 총 비용순이익 라인이 있습니다. 적절하게 설계된 금융 시스템은 거래 데이터가 아니기 때문에 이러한 데이터를 저장 하지 않을 가능성이 높습니다 . 실제로 새로운 거래 데이터의 추가에 따라 변경됩니다. 그러나 이러한 선의 계산 은 보고서 인쇄, 렌더링 또는 내보내기에 관계없이 항상 동일합니다. 당신의 그래서 IncomeStatement클래스의 형태로 그것에 행동의 공정한 금액을해야 할 것입니다 getTotalRevenues(), getTotalExpenses()getNetIncome()방법, 그리고 아마도 몇 가지 다른 사람. 실제로 "하지"않는 것처럼 보이지만 자체 동작이있는 진정한 OOP 스타일의 객체입니다.

그러나 formatprint방법은 정보 자체와 관련이 없습니다. 실제로, 이러한 방법의 여러 가지 구현 , 예를 들어 경영진에 대한 자세한 설명 및 주주에 대한 자세한 설명이 필요하지는 않습니다. 이러한 독립 함수를 다른 클래스로 분리하면 모든 크기의 단일 print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)메소드에 대한 부담없이 런타임에 다른 구현을 선택할 수 있습니다 . 왝!

위의 대규모 매개 변수화 된 메소드가 잘못되는 부분과 별도의 구현이 올바른 곳을 알 수 있기를 바랍니다. 단일 개체의 경우 인쇄 논리에 새 주름을 추가 할 때마다 도메인 모델을 변경해야합니다 ( 재무 팀은 페이지 번호를 원하지만 내부 보고서에서만이를 추가 할 수 있습니까? ). 하나 또는 두 개의 위성 클래스에 구성 속성을 추가하기 만하면됩니다.

SRP를 올바르게 구현하는 것은 종속성 관리에 관한 것입니다 . 요컨대, 클래스가 이미 유용한 것을 수행하고 UI, 프린터, 네트워크, 파일 등과 같은 새로운 종속성을 도입하는 다른 메소드를 추가하는 것을 고려하고 있다면 안됩니다 . 대신 이 기능을 클래스 에 추가 할 수있는 방법과이 새 클래스를 전체 아키텍처에 적합하게 만드는 방법을 고려하십시오 (종속성 주입을 중심으로 디자인 할 때 매우 쉽습니다). 이것이 일반적인 원칙 / 과정입니다.


참고 사항 : Robert와 마찬가지로 SRP 호환 클래스에는 하나 또는 두 개의 상태 변수 만 있어야한다는 개념을 특허 적으로 거부합니다. 이러한 얇은 래퍼는 실제로 유용한 기능을 거의 기대하지 않습니다. 따라서 이것으로 배 밖으로 가지 마십시오.


실제로 +1 큰 답변. 그러나 클래스에 대해 혼란 스럽습니다 IncomeStatement. 제안 된 설계가는 것을 의미 하는가 IncomeStatement의 인스턴스를해야합니다 IncomeStatementPrinterIncomeStatementRenderer그래서 전화 때 print()IncomeStatement그것을 호출 위임 것 IncomeStatementPrinter대신을?
Songo

@ 송고 : 물론 아니에요! SOLID 님을 따르는 경우 주기적 종속성이 없어야합니다. 분명히 내 대답은 것을 분명히 충분하지 않았다 IncomeStatement클래스가 없는print 방법, 또는 format방법, 또는 직접 검사 또는 보고서 데이터 자체를 조작 처리하지 않는 다른 방법을. 그것이 다른 수업의 목적입니다. 인쇄 IPrintable<IncomeStatement>하려면 컨테이너에 등록 된 인터페이스에 종속됩니다 .
Aaronaught

아 당신의 요점을 참조하십시오. 그러나 클래스에 인스턴스를 주입 하면 주기적 종속성 은 어디에 있습니까? 내가 그것을 호출 할 때 그것을 상상하는 방법은 그것을 위임 할 것이다 . 이 방법 뭐가 문제? ... 또 다른 질문은, 당신은 언급 , 형식의 보고서에 나타납니다 내가 그것을 데이터베이스 또는 XML 파일에서 읽을 수 있도록하려면하는 정보를 포함해야 내가 방법을 추출해야하는 부하 데이터 별도의 클래스에 넣고 호출을 위임 합니까? PrinterIncomeStatementIncomeStatement.print()IncomeStatementPrinter.print(this, format)IncomeStatementIncomeStatement
Songo

@Songo : 당신이 IncomeStatementPrinter에 따라 IncomeStatementIncomeStatement에 따라 IncomeStatementPrinter. 그것은 순환적인 의존성입니다. 그리고 그것은 단지 나쁜 디자인입니다. 에 대한 전혀 이유가 없습니다 IncomeStatement대해 아무것도 알고는 PrinterIncomeStatementPrinter- 그것은 인쇄에 관심을 아니에요, 도메인 모델, 그리고 다른 클래스 만들거나를 얻을 수 있기 때문에 대표단은 무의미하다 IncomeStatementPrinter. 도메인 모델에서 인쇄에 대한 개념을 가질 이유가 없습니다.
Aaronaught

IncomeStatement데이터베이스 (또는 XML 파일)에서 로드하는 방법에 관해서는 일반적으로 도메인이 아닌 저장소 및 / 또는 매퍼가 처리하며 도메인에서는이 위임하지 않습니다 . 다른 클래스가 이러한 모델 중 하나 를 읽어야 하는 경우 해당 저장소를 명시 적으로 요청 합니다 . Active Record 패턴을 구현하지 않는 한, 나는 팬이 아닙니다.
Aaronaught

2

SRP를 확인하는 방법은 클래스의 모든 방법 (책임)을 확인하고 다음 질문을하는 것입니다.

"이 기능을 구현하는 방식을 바꿔야합니까?"

어떤 종류의 구성이나 조건에 따라 다른 방법으로 구현 해야하는 기능을 찾으면이 책임을 처리하기 위해 추가 클래스가 필요하다는 것을 알고 있습니다.


1

Object Calisthenics의 규칙 8에서 인용 한 내용은 다음과 같습니다 .

대부분의 클래스는 단순히 단일 상태 변수를 처리해야하지만 두 개가 필요한 클래스도 있습니다. 클래스에 새 인스턴스 변수를 추가하면 해당 클래스의 응집력이 즉시 감소합니다. 일반적으로 이러한 규칙에 따라 프로그래밍하는 동안 단일 인스턴스 변수의 상태를 유지하는 클래스와 별도의 두 변수를 조정하는 클래스의 두 종류가 있습니다. 일반적으로 두 가지 책임을 혼합하지 마십시오

이 (어떤 이상 주의적) 견해를 감안할 때, 하나 또는 두 개의 상태 변수를 포함하는 클래스는 SRP를 위반하지 않을 것이라고 말할 수 있습니다. 또한 두 개 이상의 상태 변수를 포함하는 클래스 SRP를 위반할 수 있습니다 .


2
이 견해는 절망적으로 단순합니다. 아인슈타인의 유명하지만 간단한 방정식조차도 두 가지 변수가 필요합니다.
Robert Harvey

OP의 질문은 "SRP를 확인하는 다른 방법이 있습니까?"였습니다. -이것은 하나의 가능한 지표입니다. 예, 단순하지만 모든 경우에 적용되지는 않지만 SRP 위반 여부를 확인하는 한 가지 방법입니다.
MattDavey

1
나는 변하기 쉬운 대 불변의 상태 또한 중요한 고려 사항이라고 생각한다
jk.

규칙 8은 시스템을 절망적으로 복잡하고 이해할 수없고 유지 보수 할 수 없게 만드는 수천, 수천 클래스의 설계를 작성하기위한 완벽한 프로세스를 설명합니다. 그러나 장점은 SRP를 따르는 것입니다.
Dunk

@ 덩크 나는 당신의 의견에 동의하지 않지만, 그 논의는 전적으로 주제에 관한 주제가 아닙니다.
MattDavey

1

하나의 가능한 구현 (Java). 나는 반환 유형으로 자유를 얻었지만 무엇보다도 질문에 대한 답변이라고 생각합니다. TBH 나는 더 나은 이름이 순서가 있지만 보고서 클래스의 인터페이스가 그렇게 나쁘지 않다고 생각합니다. 나는 간결한 진술과 진술을 생략했다.

편집 : 또한 클래스는 불변입니다. 일단 생성되면 아무것도 변경할 수 없습니다. setFormatter () 및 setPrinter ()를 추가하면 문제가 발생하지 않습니다. IMHO의 핵심은 인스턴스화 후 원시 데이터를 변경하지 않는 것입니다.

public class Report
{
    private ReportData data;
    private ReportDataDao dao;
    private ReportFormatter formatter;
    private ReportPrinter printer;


    /*
     *  Parameterized constructor for depndency injection, 
     *  there are better ways but this is explicit.
     */
    public Report(ReportDataDao dao, 
        ReportFormatter formatter, ReportPrinter printer)
    {
        super();
        this.dao = dao;
        this.formatter = formatter;
        this.printer = printer;
    }

    /*
     * Delegates to the injected printer.
     */
    public void printReport()
    {
        printer.print(formatReport());
    }


    /*
     * Lazy loading of data, delegates to the dao 
     * for the meat of the call.
     */
    public ReportData getReportData()
    {
        if (reportData == null)
        {
            reportData = dao.loadData();
        }
        return reportData;
    }

    /*
     * Delegate to the formatter for formatting 
     * (notice a pattern here).
     */
    public ReportData formatReport()
    {
        formatter.format(getReportData());
    }
}

구현해 주셔서 감사합니다. 나는 두 가지를 가지고 있는데, if (reportData == null)나는 당신이 data대신 의미한다고 가정합니다 . 둘째, 나는이 구현에 어떻게 도착했는지 알고 싶었습니다. 마찬가지로 모든 호출을 다른 개체에 위임하기로 결정한 이유도 마찬가지입니다. 내가 항상 궁금한 점이 또 하나 있습니다. 보고서를 인쇄하는 것이 실제로 책임입니까?! 왜 생성자 를 사용하는 별도의 printer클래스 를 만들지 않았 report습니까?
Songo

예, reportData = 데이터입니다. 죄송합니다. 위임을 통해 종속성을 세부적으로 제어 할 수 있습니다. 런타임시 각 구성 요소에 대한 대체 구현을 제공 할 수 있습니다. 이제 HtmlPrinter, PdfPrinter, JsonPrinter 등을 사용할 수 있습니다. 위의 객체에 통합 된 것뿐만 아니라 위임 된 구성 요소를 독립적으로 테스트 할 수 있으므로 테스트에도 편리합니다. 프린터와 보고서의 관계를 확실히 바꿀 수 있습니다. 제공된 클래스 인터페이스로 솔루션을 제공 할 수 있음을 보여 드리고 싶었습니다. 그것은 레거시 시스템 작업에서 습관 :).
히스 릴리

흠 ... 그래서 시스템을 처음부터 구축했다면 어떤 옵션을 선택하겠습니까? Printer보고서 또는 소요 클래스 Report프린터를 취 클래스를? 보고서를 구문 분석 해야하는 위치에서 비슷한 문제가 발생했으며 보고서를 작성하는 파서를 작성해야하거나 보고서에 파서가 있어야하며 parse()호출이 위임 되는지 여부에 대해 TL과 논쟁 했습니다.
Songo

나중에 필요한 경우 ... printer.print (report)를 시작하고 report.print ()를 수행합니다. printer.print (report) 접근 방식의 가장 큰 장점은 재사용 성이 높다는 것입니다. 책임을 분리하고 필요한 곳에 편리한 방법을 제공 할 수 있습니다. 시스템의 다른 개체가 ReportPrinter에 대해 알 필요가 없도록하려면 클래스에 print () 메서드를 사용하면 보고서 인쇄 논리를 외부 세계와 격리시키는 수준의 Abstaction을 달성 할 수 있습니다. 이것은 여전히 ​​좁은 변화의 벡터를 가지고 있으며 사용하기 쉽습니다.
Heath Lilley

0

귀하의 예에서 SRP가 위반되고 있음이 확실하지 않습니다. 보고서가 비교적 단순하다면 보고서 자체를 형식화하고 인쇄 할 수 있어야합니다.

class Report {
  void format() {
     text = text.trim();
  }

  void print() {
     new Printer().write(text);
  }
}

메소드는 너무 단순하여 클래스 ReportFormatterReportPrinter클래스 를 갖는 것이 이치에 맞지 않습니다 . 인터페이스의 유일한 눈부신 문제는 getReportData값이 아닌 객체에 대해 묻지 말고 위반하기 때문입니다.

반면에 방법이 매우 복잡하거나 형식을 지정하거나 인쇄하는 방법이 여러 가지 인 Report경우 책임을 위임하는 것이 좋습니다 (더 테스트 가능).

class Report {
  void format(ReportFormatter formatter) {
     text = formatter.format(text);
  }

  void print(ReportPrinter printer) {
     printer.write(text);
  }
}

SRP는 철학적 개념이 아닌 설계 원칙이므로 실제 작업중인 코드를 기반으로합니다. 의미 적으로 클래스를 원하는만큼의 책임으로 나누거나 그룹화 할 수 있습니다. 그러나 실용적인 원칙으로 SRP는 수정해야 할 코드를 찾는 데 도움이 됩니다. SRP를 위반하는 징후는 다음과 같습니다.

  • 수업은 너무 커서 스크롤하거나 올바른 방법을 찾는 데 시간을 낭비합니다.
  • 수업은 매우 작으며 수많은 수업 사이에서 뛰어 내리거나 올바른 수업을 찾는 데 시간을 낭비합니다.
  • 변경해야 할 때 많은 클래스에 영향을 미치므로 추적하기가 어렵습니다.
  • 변경해야 할 때 어떤 클래스를 변경해야하는지 명확하지 않습니다.

이름을 개선하고, 유사한 코드를 그룹화하고, 중복을 제거하고, 계층화 된 디자인을 사용하고, 필요에 따라 클래스를 분할 / 결합함으로써 리팩토링을 통해 이러한 문제를 해결할 수 있습니다. SRP를 배우는 가장 좋은 방법은 코드베이스에 들어가 고통을 리팩터링하는 것입니다.


내가 게시물에 첨부 한 예를 확인하고 그것을 기반으로 답변을 정교하게 할 수 있습니까?
Songo

업데이트되었습니다. SRP는 상황에 따라 다르며 전체 수업을 (별도의 질문으로) 게시 한 경우 설명하기가 더 쉽습니다.
개렛 홀

업데이트 해 주셔서 감사합니다. 질문은, 실제로 인쇄하는 것이 보고서의 책임입니까?! 생성자에서 보고서를 가져 오는 별도의 프린터 클래스를 작성하지 않은 이유는 무엇입니까?
Songo

SRP는 코드 자체에 의존한다고 말하고 있습니다. 독립적으로 적용해서는 안됩니다.
개렛 홀

네, 요점을 알았습니다. 그러나 처음부터 시스템을 구축했다면 어떤 옵션을 선택하겠습니까? Printer보고서 또는 소요 클래스 Report프린터를 취 클래스를? 코드가 복잡한 지 여부를 파악하기 전에 이러한 디자인 문제에 직면하는 경우가 많습니다.
Songo

0

단일 책임 원칙응집 개념과 밀접한 관련이 있습니다. 매우 응집력있는 클래스를 가지려면 클래스의 인스턴스 변수와 해당 메소드 사이에 상호 의존성이 있어야합니다. 즉, 각 메소드는 가능한 한 많은 인스턴스 변수를 조작해야합니다. 메소드가 사용하는 변수가 많을수록 클래스에 대한 응집력이 커집니다. 최대 응집력은 일반적으로 달성 할 수 없습니다.

또한 SRP를 잘 적용하기 위해서는 비즈니스 논리 영역을 잘 이해해야합니다. 각 추상화가 무엇을해야하는지 알기 위해. 계층 구조는 각 계층이 특정 작업을 수행하도록하여 SRP 와도 관련이 있습니다 (데이터 소스 계층은 데이터 등을 제공해야 함).

메소드가 모든 변수를 사용하지 않더라도 응집력으로 돌아와서 결합해야합니다.

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public Type1 method3() {
        //use var2 and var3
    }
}

코드 아래와 같은 것은 없어야합니다. 여기서 인스턴스 변수의 일부는 메소드의 일부에 사용되고 변수의 다른 일부는 메소드의 다른 부분에 사용됩니다 (여기서는 두 개의 클래스가 있어야합니다) 변수의 각 부분).

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;
    private TypeA varA;
    private TypeB varB;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public TypeA methodA() {
        //use varA and varB
    }

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