수업을 너무 세분화하고 있습니까? 단일 책임 원칙을 어떻게 적용해야합니까?


9

세 가지 기본 단계를 포함하는 많은 코드를 작성합니다.

  1. 어딘가에서 데이터를 가져옵니다.
  2. 그 데이터를 변환하십시오.
  3. 그 데이터를 어딘가에 두십시오.

나는 일반적으로 각각의 디자인 패턴에서 영감을 얻은 세 가지 유형의 클래스를 사용합니다.

  1. 공장-일부 리소스에서 개체를 작성합니다.
  2. 중재자-공장을 사용하고 변형을 수행 한 다음 지휘관을 사용하십시오.
  3. 지휘관-그 데이터를 다른 곳에 두십시오.

제 수업은 매우 작은 경향이 있습니다. 종종 데이터를 가져오고, 데이터를 변환하고, 일하고, 데이터를 저장하는 등 단일 (공용) 방법이 있습니다. 이것은 클래스의 확산으로 이어지지 만 일반적으로 잘 작동합니다.

테스트를 할 때 어려움을 겪는 곳은 결국 밀접하게 결합 된 테스트입니다. 예를 들어;

  • 공장-디스크에서 파일을 읽습니다.
  • 사령관-파일을 디스크에 씁니다.

다른 하나 없이는 테스트 할 수 없습니다. 디스크 읽기 / 쓰기를 수행하기 위해 추가 '테스트'코드를 작성할 수는 있지만 반복하고 있습니다.

.Net을 살펴보면 File 클래스는 다른 접근 방식을 취하며 공장과 사령관의 책임을 결합합니다. 한 곳에서 Create, Delete, Exists 및 Read를위한 기능이 있습니다.

.Net의 예를 따르고 특히 외부 리소스를 다룰 때 클래스를 결합해야합니까? 여전히 결합 된 코드이지만 더 의도적입니다. 테스트가 아닌 원래 구현에서 발생합니다.

여기에서 단일 책임 원칙을 다소 과도하게 적용한 문제가 있습니까? 읽고 쓰는 별도의 클래스가 있습니다. 시스템 디스크와 같은 특정 리소스를 처리하는 결합 된 클래스를 가질 수있을 때.



6
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.- "책임"을 "해야 할 일"과 혼동하고 있습니다. 책임은 "관심 영역"과 비슷합니다. File 클래스의 책임은 파일 작업을 수행하는 것입니다.
Robert Harvey

1
당신이 좋은 몸매 인 것 같습니다. 테스트 조정자 만 있으면됩니다 (또는 원하는 경우 모든 유형의 전환에 대해 하나씩). 테스트 중재자는 .net의 File 클래스를 사용하여 파일이 올바른지 확인하기 위해 파일을 읽을 수 있습니다. SOLID 관점에서는 문제가 없습니다.
Martin Maat

1
@Robert Harvey가 언급했듯이 SRP는 실제로 책임에 관한 것이 아니기 때문에 이름이 불분명합니다. "변경 될 수있는 하나의 까다 롭고 어려운 문제 영역을 캡슐화하고 추상화하는 것"입니다. STDACMC가 너무 길 었다고 생각합니다. :-) 즉, 세 부분으로 나누는 것이 합리적이라고 생각합니다.
user949300

1
FileC #에서 라이브러리 의 중요한 점 은 File클래스가 파사드 일 수 있다는 것입니다. 모든 파일 작업을 단일 위치-클래스에 넣지 만 내부에서 비슷한 읽기 / 쓰기 클래스를 사용할 수 있습니다. 실제로 파일 처리를위한 더 복잡한 논리가 포함되어 있습니다. 이러한 클래스 ( File)는 실제로 SRP를 준수합니다. 실제로 파일 시스템을 사용하는 프로세스는 다른 계층 뒤에서 추상화 될 것입니다. 그렇지 않다고 말할 수 있습니다. :)
Andy

답변:


5

단일 책임 원칙을 따르는 것이 여기에서 당신을 안내했지만 다른 이름을 가진 곳일 수 있습니다.

명령 쿼리 책임 분리

그 점을 연구 해보면 익숙한 패턴을 따라 찾을 수있을 것입니다. 그리고 얼마나 멀리 갈 것인지 궁금해하는 사람은 아닙니다. 산성 검사는 이것이 따르는 것이 당신에게 진정한 이익을 가져다 주거나 그것이 맹인 진언이라면 생각할 필요가 없습니다.

테스트에 대한 우려를 표명했습니다. CQRS를 따르는 것이 테스트 가능한 코드 작성을 금지한다고 생각하지 않습니다. 코드를 테스트 할 수없는 방식으로 CQRS를 따르고있을 수 있습니다.

제어 흐름을 변경하지 않고도 다형성을 사용하여 소스 코드 종속성을 반전시키는 방법을 아는 데 도움이됩니다. 나는 당신의 기술이 테스트를 작성하는 곳이 어디인지 확실하지 않습니다.

라이브러리에서 찾은 습관을 따르는주의 단어는 최적이 아닙니다. 라이브러리는 고유 한 요구 사항이 있으며 솔직히 오래되었습니다. 따라서 가장 좋은 예조차도 당시 최고의 예일뿐입니다.

이것은 CQRS를 따르지 않는 완벽하게 유효한 예제가 없다고 말하는 것은 아닙니다. 그것을 따르는 것은 항상 약간의 고통이 될 것입니다. 항상 지불 할 가치가있는 것은 아닙니다. 그러나 필요한 경우 사용하게되어 기쁩니다.

당신이 그것을 사용하는 경우 다음 경고 단어에주의하십시오 :

특히 CQRS는 시스템 전체가 아닌 시스템의 특정 부분 (DDD 용어의 BoundedContext)에만 사용해야합니다. 이러한 사고 방식에서 각 경계 컨텍스트는 모델링 방법에 대한 자체 결정이 필요합니다.

마틴 플로우 러 : CQRS


이전에는 CQRS를 보지 못했습니다. 코드는 테스트가 가능하며 더 나은 방법을 찾으려고합니다. 내가 할 수있을 때 모의와 의존성 주입을 사용합니다.
제임스 우드

이것에 대해 처음 읽을 때 내 응용 프로그램을 통해 비슷한 것을 식별했습니다. 유연한 검색 처리, 여러 필드 필터링 가능 / 정렬 가능 (Java / JPA)은 골치 아픈 문제이며 기본 검색 엔진을 만들지 않는 한 수많은 상용구 코드로 이어집니다 이 물건을 처리 할 것입니다 (rsql-jpa 사용). 비록 동일한 모델 (둘 다 동일한 JPA 엔티티)이 있지만 검색은 전용 일반 서비스에서 추출되며 모델 계층은 더 이상 처리 할 필요가 없습니다.
Walfrat

3

코드가 단일 책임 원칙을 따르는 지 확인하려면 더 넓은 관점이 필요합니다. 코드 자체를 분석하는 것만으로는 대답 할 수 없으며 향후 요구 사항이 변경 될 수있는 힘 또는 행위자를 고려해야합니다.

응용 프로그램 데이터를 XML 파일로 저장한다고 가정 해 보겠습니다. 읽기 또는 쓰기와 관련된 코드를 변경하는 요인은 무엇입니까? 몇 가지 가능성 :

  • 응용 프로그램에 새 기능이 추가되면 응용 프로그램 데이터 모델이 변경 될 수 있습니다.
  • 새로운 종류의 데이터 (예 : 이미지)를 모델에 추가 할 수 있습니다.
  • 저장소 형식은 응용 프로그램 논리와 관계없이 변경 될 수 있습니다. 상호 운용성 또는 성능 문제로 인해 XML에서 JSON 또는 이진 형식으로 말합니다.

이 모든 경우에 읽기와 쓰기 논리를 모두 변경해야합니다 . 다시 말해, 그들은 별도의 책임 이 아닙니다 .

그러나 다른 시나리오를 상상해보십시오. 응용 프로그램은 데이터 처리 파이프 라인의 일부입니다. 별도의 시스템에서 생성 된 일부 CSV 파일을 읽고 일부 분석 및 처리를 수행 한 다음 다른 파일을 출력하여 세 번째 시스템에서 처리합니다. 이 경우 읽기와 쓰기는 독립적 인 책임이며 분리되어야합니다.

결론 : 일반적으로 파일 읽기 및 쓰기가 별도의 책임인지는 말할 수 없으며 응용 프로그램의 역할에 따라 다릅니다. 그러나 테스트에 대한 힌트를 바탕으로 귀하의 경우 단일 책임이라고 생각합니다.


2

일반적으로 올바른 아이디어가 있습니다.

어딘가에서 데이터를 가져옵니다. 그 데이터를 변환하십시오. 그 데이터를 어딘가에 두십시오.

세 가지 책임이있는 것 같습니다. "중재자"인 IMO가 많은 일을하고있을 수 있습니다. 세 가지 책임을 모델링하여 시작해야한다고 생각합니다.

interface Reader[T] {
    def read(): T
}

interface Transformer[T, U] {
    def transform(t: T): U
}

interface Writer[T] {
    def write(t: T): void
}

그러면 프로그램은 다음과 같이 표현 될 수 있습니다.

def program[T, U](reader: Reader[T], 
                  transformer: Transformer[T, U], 
                  writer: Writer[U]): void =
    writer.write(transformer.transform(reader.read()))

이것은 클래스의 확산으로 이어집니다

나는 이것이 문제라고 생각하지 않습니다. 소규모의 응집력 있고 테스트 가능한 클래스가 많은 IMO는 응집력이 적은 대규모 클래스보다 낫습니다.

테스트를 할 때 어려움을 겪는 곳은 결국 밀접하게 결합 된 테스트입니다. 다른 하나 없이는 테스트 할 수 없습니다.

각 조각은 독립적으로 테스트 할 수 있어야합니다. 위에서 모델링 한 것처럼 파일에 대한 읽기 / 쓰기를 다음과 같이 나타낼 수 있습니다.

class FileReader(fileName: String) implements Reader[String] {
    override read(): String = // read file into string
}

class FileWriter(fileName: String) implements Writer[String] {
    override write(str: String) = // write str to file
}

이러한 클래스를 테스트하여 파일 시스템에서 읽고 쓰는지 확인하기 위해 통합 테스트를 작성할 수 있습니다. 나머지 논리는 변환으로 작성할 수 있습니다. 예를 들어 파일이 JSON 형식 인 경우 Strings를 변환 할 수 있습니다 .

class JsonParser implements Transformer[String, Json] {
    override transform(str: String): Json = // parse as json
}

그런 다음 적절한 객체로 변환 할 수 있습니다.

class FooParser implements Transformer[Json, Foo] {
    override transform(json: Json): Foo = // ...
}

이들 각각은 독립적으로 테스트 가능합니다. 당신은 단위 테스트도 할 수 program조롱 이상 reader, transformerwriter.


바로 지금 내가있는 곳입니다. 각 기능을 개별적으로 테스트 할 수 있지만 테스트하여 기능을 결합합니다. 예를 들어 FileWriter를 테스트 한 다음 작성된 내용을 읽어야하는 확실한 솔루션은 FileReader를 사용하는 것입니다. Fwiw, 중재자는 종종 비즈니스 로직 적용과 같은 다른 작업을 수행하거나 기본 응용 프로그램 기본 기능으로 표시 될 수 있습니다.
제임스 우드

1
@JamesWood 통합 테스트의 경우가 종종 있습니다. 당신은하지 않습니다 그러나 시험에서 부부로 클래스를. FileWriter를 사용하는 대신 파일 시스템에서 직접 읽어 테스트 할 수 있습니다 FileReader. 목표가 무엇인지 테스트하는 것은 정말 당신에게 달려 있습니다. 을 사용 FileReader하면 테스트가 중단 FileReader되거나 중단되면 테스트가 중단됩니다 FileWriter. 디버그하는 데 시간이 더 걸릴 수 있습니다.
사무엘

또한 stackoverflow.com/questions/1087351/… 을 참조 하십시오. 테스트를 더 좋게 만드는 데 도움이 될 수 있습니다
Samuel

그것은 바로 지금 내가있는 곳 입니다. 100 % 사실이 아닙니다. 당신은 중재자 패턴을 사용하고 있다고 말했습니다. 나는 이것이 여기서 유용하지 않다고 생각한다. 이 패턴은 매우 복잡한 흐름으로 서로 상호 작용하는 많은 다른 객체가있을 때 사용됩니다. 모든 관계를 촉진하고 한 곳에서 구현하기 위해 중재자를 거기에 두었습니다. 이것은 당신의 경우가 아닌 것 같습니다. 작은 단위가 잘 정의되어 있습니다. 또한 @Samuel의 위의 설명과 같이 한 단위를 테스트하고 다른 단위를 호출하지 않고 어설 션을 수행해야합니다
Emerson Cardoso

@EmersonCardoso; 내 질문에서 시나리오를 다소 단순화했습니다. 내 중재자 중 일부는 매우 간단하지만 다른 중재자는 더 복잡하며 여러 공장 / 사령관을 자주 사용합니다. 단일 시나리오의 세부 사항을 피하려고합니다. 여러 시나리오에 적용 할 수있는 고급 디자인 아키텍처에 더 관심이 있습니다.
제임스 우드

2

나는 밀접하게 테스트를 결합 할 것입니다. 예를 들어;

  • 공장-디스크에서 파일을 읽습니다.
  • 사령관-파일을 디스크에 씁니다.

그래서 여기서 초점 은 그들을 하나로 묶는 것에 있습니다. 둘 사이에 객체를 전달 File합니까 (예 : ?) 그러면 서로 연결된 파일이 아니라 서로 연결된 파일입니다.

당신이 말한 것으로부터 당신은 수업을 분리했습니다. 함정은 더 쉽게 또는 '이해하기'때문에 함께 테스트하고 있다는 것 입니다.

Commander디스크에서 입력해야하는 이유는 무엇 입니까? 관심있는 것은 특정 입력을 사용하여 작성하는 것입니다. 그런 다음 테스트의 내용을 사용하여 파일을 올바르게 작성했는지 확인할 수 있습니다 .

테스트 Factory하려는 실제 부분 은 '이 파일을 올바르게 읽고 올바른 것을 출력합니까?'입니다. 따라서 테스트에서 파일을 읽기 전에 파일을 조롱하십시오 .

또는 공장과 사령관이 함께 결합했을 때 작동하는지 테스트하는 것이 좋습니다. 통합 테스트와 매우 유사합니다. 여기서 질문은 단위 테스트를 별도로 할 수 있는지 여부에 관한 것입니다.


이 특정 예에서 이들을 함께 묶는 것은 리소스 (예 : 시스템 디스크)입니다. 그렇지 않으면 두 클래스 사이에 상호 작용이 없습니다.
제임스 우드

1

어딘가에서 데이터를 가져옵니다. 그 데이터를 변환하십시오. 그 데이터를 어딘가에 두십시오.

David Parnas 가 1972 년에 쓴 일반적인 절차 적 접근 방식 입니다. 상황이 어떻게 진행 되는지에 집중합니다 . 문제의 구체적인 해결책을 더 높은 수준의 패턴으로 생각하면 항상 잘못된 것입니다.

객체 지향 접근 방식을 추구하는 경우 도메인에 집중하고 싶습니다 . 무엇에 관한 것입니까? 시스템의 주요 책임은 무엇입니까? 도메인 전문가의 언어로 제공되는 주요 개념은 무엇입니까? 따라서 도메인을 이해하고, 분해하고, 상위 책임 영역을 모듈 로 취급하고, 명사로 표현 된 하위 레벨 개념을 오브젝트로 취급하십시오. 다음은 최근 질문에 제공된 입니다. 매우 관련성이 있습니다.

응집력에는 분명한 문제가 있습니다. 직접 언급했습니다. 일부 수정을 입력 로직으로 만들고 테스트를 작성하는 경우 해당 데이터를 다음 계층으로 전달하는 것을 잊을 수 있으므로 기능이 작동한다는 것을 결코 증명하지 못합니다. 이 레이어들은 본질적으로 결합되어 있습니다. 인공적인 디커플링은 상황을 더욱 악화시킵니다. 나는 나 자신을 알고있다 : 나는이 스타일로 완전히 쓰여진 내 어깨 뒤에 100 남자 년이있는 7 요 프로젝트. 가능하면 도망 치십시오.

그리고 전체 SRP 일에. 그것은 문제 영역, 즉 도메인에 적용되는 응집력 에 관한 것 입니다. 이것이 SRP의 기본 원칙입니다. 이로 인해 객체가 똑똑해지고 자신에 대한 책임이 구현됩니다. 아무도 그들을 통제하지 않으며 아무도 데이터를 제공하지 않습니다. 데이터와 행동을 결합하여 후자 만 노출시킵니다. 따라서 객체는 원시 데이터 유효성 검사, 데이터 변환 (즉, 동작) 및 지속성을 모두 결합합니다. 다음과 같이 보일 수 있습니다.

class FinanceTransaction
{
    private $id;
    private $storage;

    public function __construct(UUID $id, DataStorage $storage)
    {
        $this->id = $id;
        $this->storage = $storage;
    }

    public function perform(
        Order $order,
        Customer $customer,
        Merchant $merchant
    )
    {
        if ($order->isExpired()) {
            throw new Exception('Order expired');
        }

        if ($customer->canNotPurchase($order)) {
            throw new Exception('It is not legal to purchase this kind of stuff by this customer');
        }

        $this->storage->save($this->id, $order, $customer, $merchant);
    }
}

(new FinanceTransaction())
    ->perform(
        new Order(
            new Product(
                $_POST['product_id']
            ),
            new Card(
                new CardNumber(
                    $_POST['card_number'],
                    $_POST['cvv'],
                    $_POST['expires_at']
                )
            )
        ),
        new Customer(
            new Name(
                $_POST['customer_name']
            ),
            new Age(
                $_POST['age']
            )
        ),
        new Merchant(
            new MerchantId($_POST['merchant_id'])
        )
    )
;

결과적으로 일부 기능을 나타내는 꽤 많은 응집력 클래스가 있습니다. 유효성 검사는 일반적으로 적어도 DDD 방식 에서는 가치 개체에 적용 됩니다.


1

테스트를 할 때 어려움을 겪는 곳은 결국 밀접하게 결합 된 테스트입니다. 예를 들어;

  • 공장-디스크에서 파일을 읽습니다.
  • 사령관-파일을 디스크에 씁니다.

파일 시스템으로 작업 할 때 유출되는 추상화에주의하십시오-너무 자주 무시되는 것을 보았으며 설명한 증상이 있습니다.

클래스가 이러한 파일에서 오는 / 이러한 데이터로 작동하는 경우 파일 시스템은 구현 세부 사항 (I / O)이되며 분리해야합니다. 이러한 클래스 (공장 / 사령관 / 중재자)는 제공된 데이터를 저장 / 읽는 것이 유일한 작업이 아닌 한 파일 시스템을 인식해서는 안됩니다. 파일 시스템을 다루는 클래스는 경로 (생성자를 통해 전달 될 수 있음)와 같은 컨텍스트 특정 매개 변수를 캡슐화해야하므로 인터페이스가 그 특성을 밝히지 않아야합니다 (인터페이스 이름의 "File"단어는 대부분 냄새입니다).


"이 클래스 (공장 / 사령관 / 중재자)는 제공된 데이터를 저장 / 읽는 것이 유일한 작업이 아니라면 파일 시스템을 인식하지 않아야합니다." 이 특정한 예에서, 그것이 그들이하는 전부입니다.
제임스 우드

0

제 생각에는 올바른 길을 가고 시작한 것 같지만 충분히 멀지 않았습니다. 한 기능을 수행하고 잘하는 다른 클래스로 기능을 분리하는 것이 맞다고 생각합니다.

한 단계 더 나아가려면 Factory, Mediator 및 Commander 클래스에 대한 인터페이스를 작성해야합니다. 그런 다음 다른 클래스의 구체적인 구현에 대한 단위 테스트를 작성할 때 해당 클래스의 모형을 사용할 수 있습니다. 모의를 사용하면 메소드가 올바른 순서와 올바른 매개 변수로 호출되고 테스트중인 코드가 다른 리턴 값으로 올바르게 작동하는지 검증 할 수 있습니다.

데이터 읽기 / 쓰기를 추상화하는 것을 볼 수도 있습니다. 지금 파일 시스템을 사용하고 있지만 나중에 데이터베이스 나 소켓으로 이동하고 싶을 수도 있습니다. 데이터 소스 / 대상이 변경되는 경우 중재자 클래스를 변경할 필요가 없습니다.


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