paint-brush
코드를 개선하고 검토 시 싸움을 피하는 방법~에 의해@zavodnoyapl
1,852 판독값
1,852 판독값

코드를 개선하고 검토 시 싸움을 피하는 방법

~에 의해 Aleksei Dovbenko23m2024/02/11
Read on Terminal Reader

너무 오래; 읽다

단위 테스트 및 게으름: 기술 팀 내에서 개별 기술을 향상시킬 수 있는 분쟁 없는 방법입니다.
featured image - 코드를 개선하고 검토 시 싸움을 피하는 방법
Aleksei Dovbenko HackerNoon profile picture
0-item
1-item


새로운 팀을 구성할 때 리더(팀 리더, 기술 리더)가 통일된 프로그래밍 스타일을 확립해야 하는 과제에 직면한다는 것은 비밀이 아닙니다. 왜냐하면 모든 팀원이 새롭고 코드 구성 및 선택에 대한 각자의 접근 방식이 있기 때문입니다. 관행. 일반적으로 이로 인해 코드 검토 중에 긴 논쟁이 발생하고 결국 SOLID, KISS, DRY 등과 같은 잘 알려진 방식에 대한 다양한 해석으로 확대됩니다. 이러한 방식의 이면에 있는 원칙은 매우 모호하며 충분한 지속성이 있으면 쉽게 찾을 수 있습니다. 하나가 다른 하나와 모순되는 역설. 예를 들어 단일 책임과 DRY를 고려해 보겠습니다.


단일 책임 원칙(SOLID의 "S") 정의에 대한 한 가지 변형에서는 각 개체가 하나의 책임을 갖고 이 책임이 클래스 내에 완전히 캡슐화되어야 한다고 명시합니다. DRY 원칙(Don't Repeat Yourself)은 코드 중복을 피할 것을 제안합니다. 그러나 코드에 다양한 레이어/서비스/모듈에서 사용할 수 있는 데이터 전송 개체(DTO)가 있는 경우 다음 원칙 중 어떤 원칙을 따라야 합니까? 의심할 여지 없이 많은 프로그래밍 서적에서는 유사한 상황을 다루고 있습니다. 일반적으로 동일한 속성 및 논리 세트를 사용하지만 다른 도메인에 속하는 다양한 개체/함수를 처리하는 경우 중복이 발생하지 않는다고 설명합니다. 그러나 이러한 개체가 서로 다른 도메인에 속해야 한다는 것을 어떻게 증명할 수 있으며, 가장 중요한 것은 리더가 이 진술을 주장하고 증명할 준비가 되어 있는지(그리고 자신감이 있는지)입니다.

 One frequently practiced approach is making categorical statements like "This is our way/It's the leader's word and we take it for granted" and similar authoritative declarations that emphasize the authority and expertise of the person who came up with these rules. This approach undoubtedly succeeds when dealing with an established team and a project with an existing codebase upon which development continues. But what should be done when the team is new, and the project has just begun? Appeals to authority may not work, as the Team/Tech Leader has not yet established their authority, and each team member believes that their knowledge and approach will be the optimal solution for the future project.


이 기사에서는 이러한 논쟁의 여지가 있는 대부분의 상황을 피할 수 있는 접근 방식을 제안합니다. 더욱이, 실제로 각 개발자는 (리더의 반대 없이) 자신이 잘못하고 있는 것과 이를 개선하는 방법을 이해할 것입니다.


우선 몇 가지 추가 조건과 정의를 소개하겠습니다.

  1. 심사를 위해 제출한 시점에서는 작업이 완료된 것으로 간주되며, 심사를 통과하면 아무런 변경 없이 출시될 수 있습니다. 즉, 코드에서 사전 계획된 변경/추가 가능성을 고려하지 않습니다.

  2. 팀은 업무 수행에 어려움을 겪지 않는 동등한 경험과 자격을 갖춘 전문가로 구성됩니다. 유일한 차이점은 접근 방식에 있습니다.

  3. 코드 스타일은 일관되며 코드 검사기로 확인됩니다.

  4. 개발 시간은 중요하지 않으며, 적어도 제품의 신뢰성보다는 덜 중요합니다.


    첫 번째 조건의 필요성에 대해서는 나중에 검토할 것입니다. 비록 그 자체로는 매우 명백하지만, 완료되지 않은 작업을 검토를 위해 제출하는 것은 비논리적이기 때문입니다. 두 번째 조건에서는 각 팀원이 알고리즘을 선택하고 할당된 작업을 구현하는 데 문제가 없는지 확인합니다. 세 번째 조건에서는 팀이 특정 스타일(PSR)을 고수한다고 가정하고 "CamelCase 또는 snake_case 중 무엇이 더 좋은가"와 같은 질문이 발생하지 않습니다. 그리고 마지막 조건은 본 작업에서 작업 완료를 위한 노력의 변화를 계산하지 않는다는 것입니다.

단위 테스트

많은 독자들은 단위 테스트가 코드 품질을 향상시킨다는 것을 알고 있습니다. 일반적으로 이를 언급한 후 테스트 중심 개발(TDD) 방법론을 예로 들어 언급하는데, 이는 실제로 코드 품질을 향상시키지만 구현 전에 테스트를 작성하려면 높은 수준의 프로그래밍 기술이 필요하기 때문에 실제로는 상대적으로 거의 적용되지 않습니다.


이전에 언급한 잘 알려진 방법에 의존하지 않고 단위 테스트가 코드를 개선하는 데 어떻게 도움이 될 수 있습니까? 먼저, 모의 개체/모듈을 종속성으로 사용하여 특정 메서드/모듈/클래스를 테스트하기 위해 단위 테스트가 적용된다는 점을 기억해 보겠습니다.


첫 번째 조건에 따라 작업은 검토를 위해 제출할 때 완료된 것으로 간주되어야 합니다. 그러므로 완료된 작업에 대한 정의를 소개하겠습니다. 작업은 아래 나열된 모든 조건을 충족하는 경우에만 완료된 것으로 간주됩니다.

  • 할당된 작업의 요구 사항이 충족되었습니다.

  • 모든 새로운 코드는 프로그램의 다양한 알고리즘 조건을 포함하여 단위 테스트를 통해 다루어져야 합니다.

  • 새 코드는 기존 테스트를 중단하지 않습니다.

    새로운 테스트를 작성하고 이전 테스트를 유지하는 데 무제한의 시간이 있고(조건 4) 각 개발자가 이러한 테스트를 작성하고 작업 요구 사항(조건 2)을 충족할 수 있으므로 모든 작업이 잠재적으로 완료될 수 있다고 생각할 수 있습니다. 이제 완료된 작업의 정의를 도입했으므로 조건 1을 정당화할 수 있습니다. 코드가 테스트에서 다루어지지 않으면 검토를 위해 제출할 수 없습니다. 그렇지 않으면 코드가 검토 없이 거부됩니다. 따라서 개발자는 피드백 후 코드 문제를 해결하는 데 테스트 수정이 필요하다는 것을 알고 있습니다. 사소해 보이는 이 점이 좋은 코드를 작성하는 근본적인 원동력이 됩니다.


    다음 코드 예제를 고려해 보겠습니다. 이 기사에서는 PHP 언어가 예제로 사용되지만 객체 지향 프로그래밍 패러다임을 지원하는 C와 유사한 언어라면 모두 가능합니다.


 class SomeFactory { public function __construct( private readonly ARepository $aRepository, ) { } /** * @throws ErrorException */ public function createByParameters(ObjectType $type, array $parameters): ObjectE|ObjectD|ObjectA|ObjectB|ObjectC { switch ($type) { case ObjectType::A: if (!array_key_exists('id', $parameters) || !is_int($parameters['id'])) { throw new ErrorException('Some message'); } $aEntity = $this->aRepository->findById($parameters['id']); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { $data = $parameters['default']; } else { throw new ErrorException('Some message'); } } return new ObjectA($data); case ObjectType::B: // some code return new ObjectB($parameters); case ObjectType::C: // some code return new ObjectC($parameters); case ObjectType::D: // some code return new ObjectD($parameters); case ObjectType::E: // some code return new ObjectE($parameters); } throw new RuntimeException('some message'); } }


여기서 우리는 제안된 접근 방식의 효율성을 입증하기 위해 모든 관행을 의도적으로 위반했습니다. 그러나 제시된 알고리즘은 기능적입니다. 유형에 따라 특정 매개변수를 가진 엔터티가 생성됩니다. 그럼에도 불구하고 우리의 주요 임무는 이 코드가 검토 단계에 도달하지 않도록 하고 개발자가 독립적으로 코드를 개선하도록 유도하는 것입니다. 조건 1에 따라 검토를 위해 코드를 제출하려면 테스트를 작성해야 합니다. 다음과 같은 테스트를 작성해 보겠습니다.


 class SomeFactoryTest extends TestCase { public function testCreateByParametersReturnsObjectAWithDefaultMethods(): void { $someFactory = new SomeFactory( $aRepository = $this->createMock(ARepository::class), ); $parameters = [ 'id' => $id = 5, 'default' => ['someData'], ]; $aRepository->expects($this->once()) ->method('findById') ->with($id) ->willReturn(null); $actualResult = $someFactory->createByParameters(ObjectType::A, $parameters); $this->assertInstanceOf(ObjectA::class, $actualResult); // additional checkers for $actualResult } }


꽤 간단한 것으로 밝혀졌지만 이는 5가지 유형 중 하나에 대해 필요한 8가지 테스트 중 하나일 뿐입니다. 모든 테스트가 작성된 후 검토 중에 변경이 필요한 피드백이 있으면 이러한 테스트가 중단될 수 있으며 개발자는 이를 다시 작성하거나 조정해야 합니다. 예를 들어, 새로운 종속성(가령 로거)을 추가하면 모든 테스트에서 공장 초기화가 변경됩니다.


 $someFactory = new SomeFactory( $aRepository = $this->createMock(ARepository::class), $this->createMock(LoggerInterface::class) );


주석 비용이 어떻게 증가했는지 확인하십시오. 이전에 종속성을 추가/변경하려면 SomeFactory 클래스에 대한 수정만 필요했지만 이제 모든 테스트(40개 이상일 수 있음)도 변경해야 합니다. 당연히 이러한 변경 사항을 여러 번 반복한 후 개발자는 피드백을 처리하는 데 필요한 노력을 최소화하기를 원할 것입니다. 어떻게 할 수 있습니까? 대답은 분명합니다. 각 유형에 대한 엔터티 생성 논리를 별도의 클래스로 분리합니다. 우리는 SOLID/DRY 원칙 등에 의존하지 않으며 코드 가독성 및 디버깅에 대한 추상적인 논의에 참여하지 않습니다. 이러한 각 주장은 논쟁의 여지가 있기 때문입니다. 우리는 단순히 테스트 작성을 단순화하고 있으며 이에 대한 개발자의 반론은 없습니다.


수정 후에는 각 유형( ObjectType::A , ObjectType::B , ObjectType::C , ObjectType::D , ObjectType::E )에 대해 5개의 팩토리를 갖게 됩니다. 다음은 ObjectType::A (FactoryA)에 대한 팩토리의 예입니다.

 class FactoryA { public function __construct( private readonly ARepository $aRepository, ) { } public function createByParameters(array $parameters): ObjectA { if (!array_key_exists('id', $parameters) || !is_int($parameters['id'])) { throw new ErrorException('Some message'); } $aEntity = $this->aRepository->findById($parameters['id']); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { // 6 7 $data = $parameters['default']; } else { throw new ErrorException('Some message'); } } return new ObjectA($data); } }


일반 공장은 다음과 같습니다.


 class SomeFactory { public function __construct( private readonly FactoryA $factoryA, private readonly FactoryB $factoryB, private readonly FactoryC $factoryC, private readonly FactoryD $factoryD, private readonly FactoryE $factoryE, ) { } /** * @throws ErrorException */ public function createByParameters(ObjectType $type, array $parameters): ObjectE|ObjectD|ObjectA|ObjectB|ObjectC { switch ($type) { case ObjectType::A: return $this->factoryA->createByParameters($parameters); case ObjectType::B: return $this->factoryB->createByParameters($parameters); case ObjectType::C: return $this->factoryC->createByParameters($parameters); case ObjectType::D: return $this->factoryD->createByParameters($parameters); case ObjectType::E: return $this->factoryE->createByParameters($parameters); } throw new RuntimeException('some message'); } }


보시다시피 전체 코드가 증가했습니다. FactoryA 에 대한 테스트와 SomeFactory 에 대한 수정된 테스트를 살펴보겠습니다.


 class FactoryATest extends TestCase { public function testCreateByParametersReturnsObjectAWithDefaultMethods(): void { $factoryA = new FactoryA( $aRepository = $this->createMock(ARepository::class), ); $parameters = [ 'id' => $id = 5, 'default' => ['someData'], ]; $aRepository->expects($this->once()) ->method('findById') ->with($id) ->willReturn(null); $actualResult = $factoryA->createByParameters($parameters); $this->assertInstanceOf(ObjectA::class, $actualResult); // additional checkers for $actualResult } }


 class SomeFactoryTest extends TestCase { public function testCreateByParametersReturnsObjectA(): void { $someFactory = new SomeFactory( $factoryA = $this->createMock(FactoryA::class), $this->createMock(FactoryB::class), $this->createMock(FactoryC::class), $this->createMock(FactoryD::class), $this->createMock(FactoryE::class), ); $parameters = ['someParameters']; $factoryA->expects($this->once()) ->method('createByParameters') ->with($parameters) ->willReturn($objectA = $this->createMock(ObjectA::class)); $this->assertSame($objectA, $someFactory->createByParameters(ObjectType::A, $parameters)); } // the same test for another types and fabrics }


총 테스트 수는 5개(가능한 유형 수) 증가했으며, 공장 테스트 수는 동일하게 유지되었습니다. 그렇다면 이 코드를 더 좋게 만드는 것은 무엇입니까? 가장 큰 장점은 코드 검토 후 수정에 필요한 노력이 줄어든다는 것입니다. 실제로 FactoryA 에서 종속성을 변경하면 FactoryA 에 대한 테스트만 영향을 받습니다.


동의합니다. 코드는 이미 더 좋아 보이고 의도치 않게 단일 책임 원칙을 부분적으로 고수했습니다. 이게 끝인가요? 앞서 언급했듯이 여전히 각 엔터티에 대해 5개의 테스트를 작성해야 합니다. 게다가 이 서비스에 대한 인수로 팩토리를 생성자에 끝없이 전달해야 하며, 새로운 유형을 도입하거나 이전 유형을 제거하면 SomeFactory 에 대한 모든 테스트(현재는 5개에 불과하지만)가 변경됩니다. 따라서 대부분의 개발자가 볼 수 있는 논리적 솔루션은 레지스트리를 만들고(특히 인터페이스별 클래스 등록에 대한 기본 지원이 있는 경우) 다음과 같이 DTO 및 팩토리에 대한 인터페이스를 선언하는 것입니다.


 interface ObjectInterface { } class ObjectA implements ObjectInterface { // some logic }


 interface FactoryInterface { public function createByParameters(array $parameters): ObjectInterface; public static function getType(): ObjectType; }


 class FactoryB implements FactoryInterface { public static function getType(): ObjectType { return ObjectType::B; } public function createByParameters(array $parameters): ObjectB { // some logic return new ObjectB($parameters); } }


getType 메소드를 정적으로 정의하는 선택을 강조하겠습니다. 현재 구현에서는 이 메서드가 정적이든 동적이든 차이가 없습니다. 그러나 이 메서드에 대한 테스트를 작성하기 시작하면(이 아이디어가 아무리 터무니없게 보이더라도) 동적 메서드의 경우 테스트가 다음과 같다는 것을 알 수 있습니다.


 public function testGetTypeReturnsTypeA(): void { $mock = $this->getMockBuilder(FactoryA::class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $this->assertSame($mock->getType(), ObjectType::A); }


정적 메서드의 경우 훨씬 더 짧아 보입니다.


 public function testGetTypeReturnsTypeA(): void { $this->assertSame(FactoryA::getType(), ObjectType::A); }


따라서 게으름 덕분에 우리는 (아마도 모르고) 올바른 솔루션을 선택했고 getType 메서드가 잠재적으로 FactoryB 클래스 객체의 상태에 의존하는 것을 방지했습니다.


레지스트리 코드를 살펴보겠습니다.


 class SomeRegistry { /** @var array<int, FactoryInterface> */ private readonly array $factories; /** * @param FactoryInterface[] $factories */ public function __construct(array $factories) { $mappedFactory = []; foreach ($factories as $factory) { if (array_key_exists($factory::getType()->value, $mappedFactory)) { throw new RuntimeException('Duplicate message'); } $mappedFactory[$factory::getType()->value] = $factory; } $this->factories = $mappedFactory; } public function createByParams(ObjectType $type, array $parameters): ObjectInterface { $factory = $this->factories[$type->value] ?? null; if ($factory === null) { throw new RuntimeException('Not found exception'); } return $factory->createByParameters($parameters); } }

보시다시피 우리는 3가지 테스트를 작성해야 합니다: 1) 복제 테스트, 2) 팩토리를 찾을 수 없을 때의 테스트, 3) 팩토리를 찾을 때의 테스트. SomeFactory 클래스는 이제 프록시 메서드처럼 보이므로 제거할 수 있습니다.


 class SomeFactory { public function __construct( private readonly SomeRegistry $someRegistry, ) { } public function createByParameters(ObjectType $type, array $parameters): ObjectInterface { return $this->someRegistry->createByParams($type, $parameters); } }


테스트 수 감소(5개에서 3개로) 외에도 새 공장을 추가/제거해도 이전 테스트가 변경되지 않습니다(새 공장 등록이 기본이고 프레임워크에 통합되어 있다고 가정).


진행 상황을 요약하면, 코드 검토 후 피드백 처리 비용을 줄이기 위한 솔루션을 추구하면서 유형 기반 객체 생성을 완전히 개편했습니다. 우리 코드는 이제 어디에서도 명시적으로 언급하지 않았음에도 불구하고 단일 책임 및 개방/폐쇄 원칙(SOLID 약어의 "S" 및 "O")을 준수합니다.


다음으로 작업을 더 복잡하게 만들고 코드를 덜 명확하게 변경하면서 동일한 작업을 수행해 보겠습니다. FactoryA 클래스의 코드를 살펴보겠습니다.


 class FactoryA implements FactoryInterface { public function __construct( private readonly ARepository $aRepository, ) { } public static function getType(): ObjectType { return ObjectType::A; } public function createByParameters(array $parameters): ObjectA { if (!array_key_exists('id', $parameters) || !is_int($parameters['id'])) { throw new ErrorException('Some message'); } $aEntity = $this->aRepository->findById($parameters['id']); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { $data = $parameters['default']; } else { throw new ErrorException('Some message'); } } return new ObjectA($data); } }


이 코드에 대한 테스트 작성을 단순화할 수 있습니까? 첫 번째 if 블록을 분석해 보겠습니다.


 if (!array_key_exists('id', $parameters) || !is_int($parameters['id'])) { throw new ErrorException('Some message'); }


테스트를 통해 이를 다루어 보겠습니다.


 public function testCreateByParametersThrowsErrorExceptionWhenParameterIdDoesntExist(): void { $this->expectException(ErrorException::class); $factoryA = new FactoryA( $this->createMock(ARepository::class), ); $factoryA->createByParameters([]); } public function testCreateByParametersThrowsErrorExceptionWhenParameterIdNotInt(): void { $this->expectException(ErrorException::class); $factoryA = new FactoryA( $this->createMock(ARepository::class), ); $factoryA->createByParameters(['id' => 'test']); }


존재에 대한 질문이 쉽게 다루어 진다면 , 유형에 대한 테스트에는 많은 함정이 있습니다. 이 테스트에서는 문자열을 전달했지만 다른 유형은 어떻습니까? 큰 숫자는 정수로 간주됩니까, 아니면 부동 소수점 숫자로 간주됩니까(예를 들어 PHP에서 10의 100승은 부동 소수점 유형의 1.0E+100과 같은 짧은 표현을 반환합니다)? 가능한 모든 시나리오에 대해 DataProvider를 작성할 수도 있고, 유효성 검사 논리를 별도의 클래스로 추출하여 다음과 같은 결과를 얻을 수도 있습니다.


 class FactoryA implements FactoryInterface { public function __construct( private readonly ARepository $aRepository, private readonly ExtractorFactory $extractorFactory ) { } public static function getType(): ObjectType { return ObjectType::A; } public function createByParameters(array $parameters): ObjectA { $extractor = $this->extractorFactory->createByArray($parameters); try { $id = $extractor->getIntByKey('id'); } catch (ExtractorException $extractorException) { throw new ErrorException('Some message', previous: $extractorException); } $aEntity = $this->aRepository->findById($id); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { $data = $parameters['default']; } else { throw new ErrorException('Some message'); } } return new ObjectA($data); } }


한편으로 우리는 새로운 종속성을 추가했고 아마도 그것을 만들어야 했을 수도 있습니다. 하지만 그 대가로 다른 모든 공장에서는 그런 문제를 걱정할 필요가 없습니다. 현재 팩토리의 테스트는 단지 하나이며 id 매개변수의 가능한 모든 변형을 다룹니다.


 public function testCreateByParametersThrowsErrorExceptionWhenParameterIdDoesntExist(): void { $this->expectException(ErrorException::class); $factoryA = new FactoryA( $this->createMock(ARepository::class), $extractorFactory = $this->createMock(ExtractorFactory::class), ); $parameters = ['someParameters']; $extractorFactory->expects($this->once()) ->method('createByArray') ->with($parameters) ->willReturn($extractor = $this->createMock(Extractor::class)); $extractor->expects($this->once()) ->method('getIntByKey') ->with('id') ->willThrowException($this->createMock(ExtractorException::class)); $factoryA->createByParameters($parameters); }


다음 코드 블록을 살펴보겠습니다.


 $aEntity = $this->aRepository->findById($id); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { // next code


이 블록에서는 getSomeParams 메서드를 사용하여 null 또는 엔터티를 반환하는 종속성 aRepository ( findById )의 메서드가 호출됩니다. getSomeParams 메소드는 차례로 데이터 배열을 반환합니다.


보시다시피 $aEntity 변수는 getSomeParams 메소드를 호출하는 데만 필요합니다. 그렇다면 getSomeParams 존재하는 경우 직접 결과를 가져오고, 존재하지 않는 경우 빈 배열을 가져오는 것은 어떨까요?


 $data = $this->aRepository->findSomeParamsById($id); if (count($data) === 0) {


테스트 전과 후를 비교해 보겠습니다. 변경 전에는 3가지 가능한 동작이 있었습니다. 1) 엔터티가 발견되고 getSomeParams 비어 있지 않은 데이터 배열을 반환할 때, 2) 엔터티가 발견되고 getSomeParams 빈 데이터 배열을 반환할 때, 3) 엔터티를 찾을 수 없습니다.


 // case 1 $aRepository->expects($this->once()) ->method('findById') ->with($id) ->willReturn($this->createConfiguredMock(SomeEntity::class, [ 'getSomeParams' => ['not empty params'] ])); // case 2 $aRepository->expects($this->once()) ->method('findById') ->with($id) ->willReturn($this->createConfiguredMock(SomeEntity::class, [ 'getSomeParams' => [] ])); // case 3 $aRepository->expects($this->once()) ->method('findById') ->with($id) ->willReturn(null);


수정된 코드에는 두 가지 가능한 시나리오만 있습니다. findSomeParamsById 빈 배열을 반환하거나 그렇지 않습니다.


 // case 1 $aRepository->expects($this->once()) ->method('findSomeParamsById') ->with($id) ->willReturn([]); // case 2 $aRepository->expects($this->once()) ->method('findSomeParamsById') ->with($id) ->willReturn(['not empty params']);


테스트 수를 줄이는 것 외에도 $this->createConfiguredMock(SomeEntity::class, [..] 제거했습니다.
다음으로 블록을 살펴보겠습니다.


 if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { $data = $parameters['default']; } else { throw new ErrorException('Some message'); } }


필요한 유형의 데이터를 추출할 수 있는 클래스가 이미 있으므로 이를 사용하여 팩토리 코드에서 검사를 제거할 수 있습니다.


 if (count($data) === 0) { try { $data = $extractor->getArrayByKey('default'); } catch (ExtractorException $extractorException) { throw new ErrorException('Some message', previous: $extractorException); } }


결국 우리는 다음과 같은 클래스를 얻습니다.


 class FactoryA implements FactoryInterface { public function __construct( private readonly ARepository $aRepository, private readonly ExtractorFactory $extractorFactory ) { } public static function getType(): ObjectType { return ObjectType::A; } public function createByParameters(array $parameters): ObjectA { $extractor = $this->extractorFactory->createByArray($parameters); try { $id = $extractor->getIntByKey('id'); } catch (ExtractorException $extractorException) { throw new ErrorException('Some message', previous: $extractorException); } $data = $this->aRepository->findSomeParamsById($id); if (count($data) === 0) { try { $data = $extractor->getArrayByKey('default'); } catch (ExtractorException $extractorException) { throw new ErrorException('Some message', previous: $extractorException); } } return new ObjectA($data); } }


createByParameters 메소드에는 다음과 같은 4개의 테스트만 있습니다.

  • 첫 번째 예외에 대한 테스트( getIntByKey )
  • findSomeParamsById 비어 있지 않은 결과를 반환했을 때의 테스트
  • findSomeParamsById 빈 결과를 반환하고 두 번째 예외( getArrayByKey )가 트리거될 때의 테스트
  • findSomeParamsById 빈 결과를 반환하고 ObjectA default 배열의 값으로 생성되었을 때의 테스트

그러나 작업 요구 사항에서 허용하고 ErrorException ExtractorException, 으로 대체할 수 있는 경우 코드는 훨씬 더 짧아집니다.


 class FactoryA implements FactoryInterface { public function __construct( private readonly ARepository $aRepository, private readonly ExtractorFactory $extractorFactory ) { } public static function getType(): ObjectType { return ObjectType::A; } /** * @throws ExtractorException */ public function createByParameters(array $parameters): ObjectA { $extractor = $this->extractorFactory->createByArray($parameters); $id = $extractor->getIntByKey('id'); $data = $this->aRepository->findSomeParamsById($id); if (count($data) === 0) { $data = $extractor->getArrayByKey('default'); } return new ObjectA($data); } }


그리고 두 가지 테스트만 있을 것입니다:

  • findSomeParamsById 비어 있지 않은 결과를 반환했을 때의 테스트

  • findSomeParamsById 빈 결과를 반환하고 ObjectA default 배열의 값으로 생성되었을 때의 테스트


완료된 작업을 요약해 보겠습니다.


처음에는 테스트 범위가 필요한 잘못 작성된 코드가 있었습니다. 모든 개발자는 오류로 인해 충돌이 발생할 때까지 자신의 코드에 자신감을 갖고 있기 때문에 테스트를 작성하는 것은 누구도 좋아하지 않는 길고 단조로운 작업입니다. 더 적은 수의 테스트를 작성하는 유일한 방법은 이러한 테스트에서 다루어야 하는 코드를 단순화하는 것입니다. 결국, 개발자는 테스트 횟수를 단순화하고 줄임으로써 특정 이론적 관행을 따르지 않고도 코드를 개선할 수 있습니다.