Không có gì bí mật khi thành lập một nhóm mới, các nhà lãnh đạo (Trưởng nhóm, Trưởng nhóm công nghệ) phải đối mặt với thách thức trong việc thiết lập một phong cách lập trình thống nhất, vì tất cả các thành viên trong nhóm đều là người mới và mỗi người có cách tiếp cận riêng để tổ chức mã và lựa chọn. thực hành. Thông thường, điều này dẫn đến các cuộc tranh luận kéo dài trong quá trình đánh giá mã, cuối cùng leo thang thành nhiều cách giải thích khác nhau về các phương pháp nổi tiếng như SOLID, KISS, DRY, v.v. Các nguyên tắc đằng sau các phương pháp này khá mờ nhạt và nếu đủ kiên trì, bạn có thể dễ dàng tìm thấy những nghịch lý nơi cái này mâu thuẫn với cái khác. Ví dụ: hãy xem xét Trách nhiệm duy nhất và DRY.
Một biến thể của việc xác định Nguyên tắc Trách nhiệm Duy nhất ("S" trong SOLID) nói rằng mỗi đối tượng phải có một trách nhiệm và trách nhiệm này phải được gói gọn đầy đủ trong lớp. Nguyên tắc DRY (Đừng lặp lại chính mình) gợi ý tránh trùng lặp mã. Tuy nhiên, nếu chúng ta có một đối tượng truyền dữ liệu (DTO) trong mã có thể được sử dụng trong các lớp/dịch vụ/mô-đun khác nhau, thì chúng ta nên tuân theo nguyên tắc nào sau đây? Không còn nghi ngờ gì nữa, nhiều sách lập trình đề cập đến các tình huống tương tự, thường nêu rõ rằng nếu chúng ta đang xử lý các đối tượng/hàm khác nhau với cùng một bộ thuộc tính và logic nhưng thuộc về các miền khác nhau thì điều đó không tạo thành sự trùng lặp. Tuy nhiên, làm thế nào người ta có thể chứng minh rằng những đối tượng này PHẢI thuộc các lĩnh vực khác nhau và quan trọng nhất là người lãnh đạo có sẵn sàng (và tự tin) để khẳng định và chứng minh tuyên bố này không?
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.
Bài viết này gợi ý một cách tiếp cận cho phép tránh được hầu hết các tình huống gây tranh cãi như vậy. Hơn nữa, mỗi nhà phát triển trong thực tế (không bị lãnh đạo phản đối) sẽ hiểu họ đang làm gì sai và cách cải thiện nó.
Để bắt đầu, hãy giới thiệu một số điều kiện và định nghĩa bổ sung:
Tại thời điểm gửi để xem xét, nhiệm vụ được coi là hoàn thành và nếu vượt qua quá trình xem xét, nó có thể được giải phóng mà không có bất kỳ thay đổi nào. Nói cách khác, chúng tôi không xem xét khả năng thay đổi/bổ sung được lên kế hoạch trước trong mã.
Đội ngũ bao gồm các chuyên gia có trình độ và kinh nghiệm ngang nhau, không gặp khó khăn gì trong việc thực hiện nhiệm vụ; sự khác biệt duy nhất nằm ở cách tiếp cận của họ.
Kiểu mã nhất quán và được xác minh bởi người kiểm tra mã.
Thời gian phát triển không quan trọng, ít nhất là ít quan trọng hơn độ tin cậy của sản phẩm.
Chúng ta sẽ xem xét sự cần thiết của điều kiện đầu tiên sau, mặc dù bản thân nó khá rõ ràng, vì việc gửi một nhiệm vụ chưa hoàn thành để xem xét là phi logic. Với điều kiện thứ hai, chúng tôi đảm bảo rằng mỗi thành viên trong nhóm không gặp vấn đề gì khi chọn thuật toán và thực hiện nhiệm vụ được giao. Trong điều kiện thứ ba, chúng tôi giả định rằng nhóm tuân thủ một phong cách cụ thể (PSR) và không phát sinh các câu hỏi như "cái gì tốt hơn, CamelCase hay snake_case". Và điều kiện cuối cùng không tính đến sự thay đổi nỗ lực hoàn thành nhiệm vụ trong công việc này.
Nhiều độc giả biết rằng kiểm thử đơn vị sẽ cải thiện chất lượng mã. Thông thường, sau khi nêu điều này, phương pháp phát triển dựa trên thử nghiệm (TDD) được đề cập làm ví dụ, phương pháp này thực sự giúp nâng cao chất lượng mã nhưng tương đối hiếm khi được áp dụng trong thực tế vì viết bài kiểm tra trước khi triển khai đòi hỏi bộ kỹ năng lập trình cấp cao.
Làm cách nào thử nghiệm đơn vị có thể giúp cải thiện mã mà không cần dựa vào các phương pháp nổi tiếng đã đề cập trước đó? Trước tiên, hãy nhớ lại rằng các bài kiểm tra đơn vị được áp dụng để kiểm tra một phương thức/mô-đun/lớp cụ thể bằng cách sử dụng các đối tượng/mô-đun giả làm phần phụ thuộc.
Theo điều kiện đầu tiên, nhiệm vụ phải được coi là hoàn thành tại thời điểm nộp hồ sơ để xem xét. Vì vậy, hãy giới thiệu một định nghĩa cho những gì chúng ta coi là một nhiệm vụ đã hoàn thành. Một nhiệm vụ chỉ được coi là hoàn thành khi nó đáp ứng tất cả các điều kiện được liệt kê dưới đây:
Đáp ứng yêu cầu nhiệm vụ được giao.
Tất cả mã mới phải được kiểm tra đơn vị, bao gồm các điều kiện thuật toán khác nhau trong chương trình.
Mã mới không phá vỡ các thử nghiệm hiện có.
Vì chúng tôi có thời gian không giới hạn để viết các bài kiểm thử mới và duy trì các bài kiểm thử cũ (điều kiện 4) và mỗi nhà phát triển có thể viết các bài kiểm thử này và đáp ứng các yêu cầu nhiệm vụ (điều kiện 2), nên chúng tôi có thể coi rằng bất kỳ nhiệm vụ nào cũng có thể được hoàn thành. Bây giờ, vì chúng tôi đã giới thiệu định nghĩa về một nhiệm vụ đã hoàn thành nên chúng tôi có thể biện minh cho điều kiện 1: mã không thể được gửi để xem xét nếu nó không được kiểm tra; nếu không, mã sẽ bị từ chối mà không được xem xét. Do đó, nhà phát triển biết rằng việc khắc phục các sự cố về mã sau khi phản hồi liên quan đến việc sửa các bài kiểm tra. Điểm tưởng chừng như nhỏ này lại trở thành động lực cơ bản để viết mã tốt.
Hãy xem xét ví dụ mã sau (trong bài viết này, ngôn ngữ PHP được sử dụng làm ví dụ, nhưng nó có thể là bất kỳ ngôn ngữ giống C nào có hỗ trợ cho mô hình lập trình hướng đối tượng):
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'); } }
Ở đây, chúng tôi cố tình vi phạm tất cả các thông lệ để chứng minh tính hiệu quả của phương pháp được đề xuất. Tuy nhiên, lưu ý rằng thuật toán được trình bày có tính chức năng; tùy thuộc vào loại, một thực thể có các tham số cụ thể sẽ được tạo. Tuy nhiên, nhiệm vụ chính của chúng tôi là đảm bảo rằng mã này không đạt đến giai đoạn xem xét, khiến nhà phát triển phải cải thiện nó một cách độc lập. Theo điều kiện 1, để gửi mã để xem xét, chúng ta cần viết bài kiểm tra. Hãy viết một bài kiểm tra như vậy:
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 } }
Hóa ra nó khá đơn giản, nhưng đây chỉ là một trong tám bài kiểm tra cần thiết cho một trong năm loại. Sau khi tất cả các bài kiểm tra được viết, mọi phản hồi trong quá trình đánh giá yêu cầu thay đổi đều có thể làm hỏng các bài kiểm tra này và nhà phát triển sẽ phải viết lại hoặc điều chỉnh chúng. Ví dụ: việc thêm một phần phụ thuộc mới (giả sử là một trình ghi nhật ký) sẽ dẫn đến những thay đổi đối với quá trình khởi tạo ban đầu trong tất cả các thử nghiệm:
$someFactory = new SomeFactory( $aRepository = $this->createMock(ARepository::class), $this->createMock(LoggerInterface::class) );
Lưu ý rằng chi phí của một nhận xét đã tăng lên như thế nào: nếu trước đây việc thêm/thay đổi một phần phụ thuộc chỉ yêu cầu sửa đổi lớp SomeFactory
thì bây giờ tất cả các thử nghiệm (có thể nhiều hơn 40) cũng sẽ cần phải được thay đổi. Đương nhiên, sau nhiều lần lặp lại những thay đổi như vậy, nhà phát triển sẽ muốn giảm thiểu nỗ lực cần thiết để giải quyết phản hồi. Điều này có thể giải quyết như thế nào? Câu trả lời rất rõ ràng - tách logic tạo thực thể cho từng loại thành một lớp riêng biệt. Xin lưu ý rằng chúng tôi không dựa vào các nguyên tắc SOLID/DRY, v.v. và chúng tôi không tham gia vào các cuộc thảo luận trừu tượng về khả năng đọc và gỡ lỗi mã, vì mỗi lập luận này có thể bị tranh chấp. Chúng tôi chỉ đơn giản là đơn giản hóa việc viết các bài kiểm tra và không có lập luận phản bác nào của nhà phát triển chống lại điều này.
Sau khi sửa đổi, chúng ta sẽ có 5 nhà máy cho mỗi loại ( ObjectType::A
, ObjectType::B
, ObjectType::C
, ObjectType::D
, ObjectType::E
). Dưới đây là ví dụ về nhà máy dành cho 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); } }
Và nhà máy chung sẽ trông như thế này:
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'); } }
Như chúng ta có thể thấy, mã tổng thể đã tăng lên. Hãy xem xét các thử nghiệm dành cho FactoryA
và thử nghiệm đã sửa đổi cho 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 }
Tổng số cuộc kiểm tra tăng thêm 5 (số loại có thể), trong khi số cuộc kiểm tra đối với các nhà máy vẫn giữ nguyên. Vậy điều gì làm cho mã này tốt hơn? Ưu điểm chính là giảm nỗ lực cần thiết cho việc sửa lỗi sau khi xem xét mã. Thật vậy, khi thay đổi các phần phụ thuộc trong FactoryA
, chỉ các thử nghiệm cho FactoryA
bị ảnh hưởng.
Đồng ý rằng, mã đã trông đẹp hơn và có lẽ vô tình, chúng tôi đã tuân thủ một phần nguyên tắc trách nhiệm duy nhất. Đây có phải là kết thúc của nó? Như đã đề cập trước đó, chúng ta vẫn cần viết 5 bài kiểm tra cho mỗi thực thể. Hơn nữa, chúng ta sẽ phải chuyển liên tục các nhà máy vào hàm tạo làm đối số cho dịch vụ này và việc giới thiệu một loại mới (hoặc loại bỏ loại cũ) sẽ dẫn đến thay đổi trong tất cả các thử nghiệm (mặc dù hiện tại chúng chỉ có 5) cho SomeFactory
. Do đó, một giải pháp hợp lý mà hầu hết các nhà phát triển có thể sẽ thấy là tạo một sổ đăng ký (đặc biệt nếu có hỗ trợ riêng cho việc đăng ký lớp theo giao diện) và khai báo các giao diện cho DTO và nhà máy như:
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); } }
Hãy nêu bật lựa chọn xác định phương thức getType
là tĩnh. Trong cách triển khai hiện tại, không có sự khác biệt nào cho dù phương thức này là tĩnh hay động. Tuy nhiên, nếu chúng ta bắt đầu viết chương trình kiểm thử cho phương pháp này (dù ý tưởng này có vẻ vô lý đến mức nào), chúng ta có thể nhận thấy rằng trong trường hợp phương pháp động, bài kiểm thử sẽ như sau:
public function testGetTypeReturnsTypeA(): void { $mock = $this->getMockBuilder(FactoryA::class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $this->assertSame($mock->getType(), ObjectType::A); }
Trong khi đối với một phương thức tĩnh, nó sẽ trông ngắn hơn nhiều:
public function testGetTypeReturnsTypeA(): void { $this->assertSame(FactoryA::getType(), ObjectType::A); }
Như vậy, nhờ sự lười biếng, chúng ta đã chọn đúng giải pháp (có thể là vô tình) và ngăn phương thức getType
có khả năng phụ thuộc vào trạng thái của đối tượng lớp FactoryB
.
Hãy xem mã đăng ký:
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); } }
Như chúng ta có thể thấy, chúng ta phải viết 3 bài kiểm tra: 1) bài kiểm tra trùng lặp, 2) bài kiểm tra khi không tìm thấy nhà máy và 3) bài kiểm tra khi tìm thấy nhà máy. Lớp SomeFactory
bây giờ trông giống như một phương thức proxy và do đó có thể bị xóa.
class SomeFactory { public function __construct( private readonly SomeRegistry $someRegistry, ) { } public function createByParameters(ObjectType $type, array $parameters): ObjectInterface { return $this->someRegistry->createByParams($type, $parameters); } }
Ngoài việc giảm số lượng thử nghiệm (từ 5 xuống 3), bất kỳ việc bổ sung/loại bỏ nhà máy mới nào cũng không kéo theo những thay đổi đối với các thử nghiệm cũ (giả sử rằng việc đăng ký nhà máy mới là nguyên gốc và được tích hợp vào khuôn khổ).
Để tóm tắt tiến trình của chúng tôi: trong quá trình theo đuổi giải pháp nhằm giảm chi phí giải quyết phản hồi sau khi xem xét mã, chúng tôi đã cải tiến hoàn toàn việc tạo đối tượng dựa trên loại. Mã của chúng tôi hiện tuân thủ các nguyên tắc Trách nhiệm duy nhất và Mở/Đóng ("S" và "O" trong từ viết tắt SOLID), mặc dù chúng tôi không đề cập rõ ràng đến chúng ở bất kỳ đâu.
Tiếp theo, hãy thực hiện nhiệm vụ phức tạp hơn và thực hiện cùng một công việc với những thay đổi ít rõ ràng hơn trong mã. Hãy kiểm tra mã trong lớp 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); } }
Chúng ta có thể đơn giản hóa việc viết bài kiểm tra cho mã này không? Hãy chia nhỏ khối if đầu tiên:
if (!array_key_exists('id', $parameters) || !is_int($parameters['id'])) { throw new ErrorException('Some message'); }
Hãy thử che nó bằng các bài kiểm tra:
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']); }
Nếu câu hỏi về sự tồn tại được giải đáp một cách dễ dàng thì việc kiểm tra loại này có rất nhiều cạm bẫy. Trong thử nghiệm này, chúng tôi đã chuyển một chuỗi, nhưng còn các loại khác thì sao? Một số lớn được coi là số nguyên hay số dấu phẩy động (ví dụ: trong PHP, 10 lũy thừa 100 sẽ trả về một biểu diễn ngắn như 1.0E+100 của kiểu float)? Bạn có thể viết DataProvider cho tất cả các tình huống có thể xảy ra hoặc bạn có thể trích xuất logic xác thực vào một lớp riêng biệt và nhận được kết quả như:
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); } }
Một mặt, chúng tôi đã thêm một phần phụ thuộc mới và có lẽ chúng tôi thậm chí phải tạo ra nó. Nhưng bù lại, ở tất cả các nhà máy khác, chúng tôi không phải lo lắng về những vấn đề như vậy. Thử nghiệm trong nhà máy hiện tại chỉ là một thử nghiệm và nó bao gồm tất cả các biến thể có thể có của tham số 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); }
Hãy xem khối mã tiếp theo, cụ thể là:
$aEntity = $this->aRepository->findById($id); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { // next code
Trong khối này, phương thức của phần phụ thuộc aRepository
( findById
) được gọi, phương thức này trả về null hoặc một thực thể có phương thức getSomeParams
. Phương thức getSomeParams
lần lượt trả về một mảng dữ liệu.
Như chúng ta có thể thấy, biến $aEntity
chỉ cần thiết để gọi phương thức getSomeParams
. Vì vậy, tại sao không nhận trực tiếp kết quả của getSomeParams
nếu nó tồn tại và một mảng trống nếu không?
$data = $this->aRepository->findSomeParamsById($id); if (count($data) === 0) {
Hãy so sánh các bài kiểm tra trước và sau. Trước khi thay đổi, chúng tôi có 3 hành vi có thể xảy ra: 1) khi thực thể được tìm thấy và getSomeParams
trả về một mảng dữ liệu không trống, 2) khi tìm thấy thực thể và getSomeParams
trả về một mảng dữ liệu trống, 3) khi thực thể không được tìm thấy.
// 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);
Trong mã đã sửa đổi, chỉ có hai trường hợp có thể xảy ra: findSomeParamsById
trả về một mảng trống hoặc không trả về.
// 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']);
Ngoài việc giảm số lượng thử nghiệm, chúng tôi đã loại bỏ $this->createConfiguredMock(SomeEntity::class, [..]
.
Tiếp theo, chúng ta hãy nhìn vào khối:
if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { $data = $parameters['default']; } else { throw new ErrorException('Some message'); } }
Vì chúng ta đã có một lớp có thể trích xuất dữ liệu thuộc loại được yêu cầu nên chúng ta có thể sử dụng nó, loại bỏ các kiểm tra khỏi mã xuất xưởng:
if (count($data) === 0) { try { $data = $extractor->getArrayByKey('default'); } catch (ExtractorException $extractorException) { throw new ErrorException('Some message', previous: $extractorException); } }
Cuối cùng, chúng ta có được một lớp như sau:
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); } }
Phương thức createByParameters sẽ chỉ có 4 bài kiểm tra, đó là:
getIntByKey
)findSomeParamsById
trả về kết quả không trốngfindSomeParamsById
trả về kết quả trống và ngoại lệ thứ hai ( getArrayByKey
) được kích hoạtfindSomeParamsById
trả về kết quả trống và ObjectA
được tạo bằng các giá trị từ mảng default
Tuy nhiên, nếu yêu cầu tác vụ cho phép và ErrorException
có thể được thay thế bằng ExtractorException,
mã sẽ còn ngắn hơn nữa:
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); } }
Và sẽ chỉ có hai bài kiểm tra:
một bài kiểm tra khi findSomeParamsById
trả về kết quả không trống
một thử nghiệm khi findSomeParamsById
trả về kết quả trống và ObjectA
được tạo bằng các giá trị từ mảng default
Hãy tóm tắt công việc đã thực hiện.
Ban đầu, chúng tôi có mã viết kém cần được kiểm tra. Vì bất kỳ nhà phát triển nào cũng tự tin vào mã của họ (cho đến khi có lỗi xảy ra), viết bài kiểm tra cho nó là một công việc lâu dài và đơn điệu mà không ai thích. Cách duy nhất để viết ít bài kiểm tra hơn là đơn giản hóa mã cần được thực hiện trong các bài kiểm tra này. Cuối cùng, bằng cách đơn giản hóa và giảm số lượng thử nghiệm, nhà phát triển sẽ cải thiện mã mà không nhất thiết phải tuân theo bất kỳ thực tiễn lý thuyết cụ thể nào.