paint-brush
Kodu Geliştirme ve İnceleme Sırasında Kavgaları Önlemeile@zavodnoyapl
1,848 okumalar
1,848 okumalar

Kodu Geliştirme ve İnceleme Sırasında Kavgaları Önleme

ile Aleksei Dovbenko23m2024/02/11
Read on Terminal Reader

Çok uzun; Okumak

Birim testleri ve tembellik: teknoloji ekiplerindeki bireysel becerileri artırmak için tartışmasız bir yöntem.
featured image - Kodu Geliştirme ve İnceleme Sırasında Kavgaları Önleme
Aleksei Dovbenko HackerNoon profile picture
0-item
1-item


Yeni bir ekip oluşturmaya gelince, liderlerin (Takım Lideri, Teknoloji Lideri) tüm ekip üyeleri yeni olduğundan ve her birinin kodu organize etme ve seçme konusunda kendi yaklaşımı olduğundan, birleşik bir programlama stili oluşturma zorluğuyla karşı karşıya kaldıkları bir sır değil. uygulamalar. Tipik olarak bu, kod incelemeleri sırasında uzun tartışmalara yol açar ve bu tartışmalar sonunda SOLID, KISS, DRY vb. gibi iyi bilinen uygulamaların çeşitli yorumlarına dönüşür. Bu uygulamaların arkasındaki ilkeler oldukça bulanıktır ve yeterli ısrarla, bulunması kolaydır. birinin diğeriyle çeliştiği paradokslar. Örneğin Tek Sorumluluk ve DRY'yi ele alalım.


Tek Sorumluluk İlkesini (SOLID'deki "S") tanımlamanın bir varyasyonu, her nesnenin bir sorumluluğa sahip olması gerektiğini ve bu sorumluluğun tamamen sınıf içinde kapsanması gerektiğini belirtir. DRY ilkesi (Kendinizi Tekrar Etmeyin), kod tekrarından kaçınmayı önerir. Ancak kodumuzda farklı katmanlarda/hizmetlerde/modüllerde kullanılabilecek bir veri aktarım nesnemiz (DTO) varsa bu ilkelerden hangisini takip etmeliyiz? Kuşkusuz, birçok programlama kitabı benzer durumları ele alır; tipik olarak aynı özellik ve mantık kümesine sahip ancak farklı alanlara ait farklı nesneler/işlevlerle uğraşıyorsak bunun çoğaltma teşkil etmediğini belirtir. Ancak bu nesnelerin farklı alanlara ait olması gerektiği nasıl kanıtlanabilir ve en önemlisi lider bu ifadeyi ileri sürmeye ve kanıtlamaya hazır mı (ve kendinden emin mi)?

 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.


Bu makale, bu tür tartışmalı durumların çoğundan kaçınmaya olanak tanıyan bir yaklaşım önermektedir. Ayrıca, uygulamadaki her geliştirici (liderin itirazı olmaksızın) neyi yanlış yaptıklarını ve bunu nasıl geliştirebileceklerini anlayacaktır.


Başlangıç olarak birkaç ek koşul ve tanım sunalım:

  1. İncelemeye gönderildiğinde görev tamamlanmış sayılır ve incelemeyi geçerse herhangi bir değişiklik yapılmadan yayınlanabilir. Yani kodda önceden planlanmış değişiklik/ekleme olasılığını dikkate almıyoruz.

  2. Ekip, görevlerin uygulanmasında hiçbir zorlukla karşılaşmayan, aynı derecede deneyimli ve nitelikli uzmanlardan oluşur; tek farklılık yaklaşımlarında yatmaktadır.

  3. Kod stili tutarlıdır ve kod denetleyicileri tarafından doğrulanır.

  4. Geliştirme süresi kritik değildir, en azından ürünün güvenilirliğinden daha az kritiktir.


    Tamamlanmamış bir görevi incelemeye sunmak mantıksız olduğundan, kendi başına oldukça açık olmasına rağmen, ilk koşulun gerekliliğini daha sonra ele alacağız. İkinci koşulla, her ekip üyesinin algoritma seçme ve verilen görevi uygulama konusunda sorun yaşamamasını sağlıyoruz. Üçüncü koşulda ise ekibin belirli bir stile (PSR) bağlı kaldığını ve “hangisi daha iyi, CamelCase mi, Snake_case mi?” gibi soruların ortaya çıkmadığını varsayıyoruz. Ve son koşul, bu çalışmada görevin tamamlanması için harcanan çabadaki değişikliklerin hesaplanmasından kaçınılmasıdır.

Birim testleri

Pek çok okuyucu, birim testinin kod kalitesini artırdığının farkındadır. Tipik olarak, bunu belirttikten sonra, örnek olarak Test odaklı geliştirme (TDD) metodolojisinden bahsedilir; bu, aslında kod kalitesini artırır ancak pratikte nispeten nadiren uygulanır çünkü uygulamadan önce test yazmak, yüksek düzeyde programlama becerisi gerektirir.


Birim testi, daha önce bahsedilen iyi bilinen uygulamalara güvenmeden kodun geliştirilmesine nasıl yardımcı olabilir? Öncelikle birim testlerinin, sahte nesneler/modülleri bağımlılık olarak kullanarak belirli bir yöntemi/modülü/sınıfı test etmek için uygulandığını hatırlayalım.


İlk koşulun ardından, incelemeye sunulduğu anda görevin tamamlanmış olduğu kabul edilmelidir. Bu nedenle tamamlanmış bir görev olarak kabul ettiğimiz şeyin tanımını verelim. Bir görev yalnızca aşağıda listelenen tüm koşulları karşıladığında tamamlanmış sayılır:

  • Atanan görevin gereksinimleri karşılanır.

  • Tüm yeni kodlar, programdaki çeşitli algoritmik koşullar da dahil olmak üzere birim testlerinin kapsamında olmalıdır.

  • Yeni kod mevcut testleri bozmuyor.

    Yeni testler yazmak ve eskilerini korumak için sınırsız zamanımız olduğundan (koşul 4) ve her geliştirici bu testleri yazabildiğinden ve görev gereksinimlerini karşılayabildiğinden (koşul 2), herhangi bir görevin potansiyel olarak tamamlanabileceğini düşünebiliriz. Artık tamamlanmış bir görevin tanımını tanıttığımıza göre, koşul 1'i gerekçelendirebiliriz: kod, testlerin kapsamına girmiyorsa incelemeye gönderilemez; aksi takdirde kod incelenmeden reddedilecektir. Bu nedenle geliştirici, geri bildirimden sonra kod sorunlarını düzeltmenin testleri düzeltmeyi içerdiğini bilir. Bu görünüşte küçük nokta, iyi kod yazmanın temel itici gücü haline gelir.


    Aşağıdaki kod örneğini ele alalım (bu makalede örnekler için PHP dili kullanılmıştır, ancak nesne yönelimli programlama paradigmasını destekleyen herhangi bir C benzeri dil olabilir):


 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'); } }


Burada önerilen yaklaşımın etkinliğini göstermek için tüm uygulamaları kasıtlı olarak ihlal ettik. Ancak sunulan algoritmanın işlevsel olduğunu unutmayın; türe bağlı olarak belirli parametrelere sahip bir varlık oluşturulur. Bununla birlikte, asıl görevimiz bu kodun inceleme aşamasına ulaşmamasını sağlayarak geliştiricinin onu bağımsız olarak geliştirmesini sağlamaktır. Koşul 1'i takiben, kodu incelemeye göndermek için testler yazmamız gerekir. Böyle bir test yazalım:


 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 } }


Oldukça basit olduğu ortaya çıktı, ancak bu, beş türden biri için gerekli sekiz testten yalnızca biri. Tüm testler yazıldıktan sonra, inceleme sırasında değişiklik gerektiren herhangi bir geri bildirim bu testleri bozabilir ve geliştiricinin bunları yeniden yazması veya ayarlaması gerekir. Örneğin, yeni bir bağımlılık (diyelim ki bir günlükçü) eklemek, tüm testlerde fabrika başlatmada değişikliklere neden olacaktır:


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


Bir yorumun maliyetinin nasıl arttığına dikkat edin: Daha önce bir bağımlılık eklemek/değiştirmek yalnızca SomeFactory sınıfında değişiklik gerektiriyorsa, artık tüm testlerin (40'tan fazla olabilir) de değiştirilmesi gerekecektir. Doğal olarak, bu tür değişikliklerin birkaç kez yinelenmesinden sonra geliştirici, geri bildirimi ele almak için gereken çabayı en aza indirmek isteyecektir. Bu nasıl yapılabilir? Cevap açıktır; her tür için varlık oluşturma mantığını ayrı bir sınıfa ayırın. Lütfen, SOLID/DRY ilkelerine vb. dayanmadığımızı ve bu argümanların her birine itiraz edilebileceğinden kod okunabilirliği ve hata ayıklama hakkında soyut tartışmalara girmediğimizi unutmayın. Biz sadece testlerin yazımını basitleştiriyoruz ve geliştiricinin buna karşı herhangi bir karşı argümanı yok.


Değişiklikten sonra her tür için 5 fabrikamız olacak ( ObjectType::A , ObjectType::B , ObjectType::C , ObjectType::D , ObjectType::E ). Aşağıda ObjectType::A (FactoryA) için bir fabrika örneği verilmiştir:

 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); } }


Ve genel fabrika şöyle görünecek:


 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'); } }


Gördüğümüz gibi genel kod arttı. FactoryA testlerine ve SomeFactory için değiştirilmiş teste bakalım.


 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 }


Toplam test sayısı 5 (olası tür sayısı) artarken, fabrikalara yönelik test sayısı aynı kaldı. Peki bu kodu daha iyi yapan şey nedir? Ana avantaj, kod incelemesinden sonra düzeltmeler için gereken çabanın azalmasıdır. Aslında FactoryA bağımlılıklar değiştirilirken yalnızca FactoryA yönelik testler etkilenir.


Kabul ediyorum, kod zaten daha iyi görünüyor ve belki de istemeden tek sorumluluk ilkesine kısmen bağlı kaldık. Bu işin sonu mu? Daha önce de belirttiğimiz gibi yine de her varlık için 5 test yazmamız gerekiyor. Üstelik, bu hizmet için argüman olarak fabrikaları sonsuza dek yapıcıya aktarmamız gerekecek ve yeni bir tür eklemek (veya eskisini kaldırmak), SomeFactory için tüm testlerde (şu anda yalnızca 5 olmasına rağmen) değişikliklere yol açacaktır. Bu nedenle, çoğu geliştiricinin muhtemelen göreceği mantıksal bir çözüm, bir kayıt defteri oluşturmak (özellikle arayüze göre sınıf kaydı için yerel destek varsa) ve DTO'lar ve fabrikalar için aşağıdaki gibi arayüzler bildirmektir:


 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 yöntemini statik olarak tanımlama seçimini vurgulayalım. Mevcut uygulamada bu yöntemin statik veya dinamik olması arasında bir fark yoktur. Ancak bu yöntem için bir test yazmaya başlarsak (bu fikir ne kadar saçma görünse de), dinamik bir yöntem söz konusu olduğunda testin şöyle görüneceğini fark edebiliriz:


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


Statik bir yöntem için ise çok daha kısa görünecektir:


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


Böylece tembellik sayesinde (belki de farkında olmadan) doğru çözümü seçtik ve getType yönteminin FactoryB sınıfı nesnesinin durumuna bağlı olma potansiyelini engelledik.


Kayıt koduna bakalım:


 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); } }

Gördüğümüz gibi 3 test yazmamız gerekiyor: 1) çoğaltma testi, 2) fabrika bulunamadığında test ve 3) fabrika bulunduğunda test. SomeFactory sınıfı artık bir proxy yöntemine benziyor ve bu nedenle kaldırılabilir.


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


Test sayısındaki azalmaya (5'ten 3'e) ek olarak, yeni bir fabrikanın eklenmesi/kaldırılması eski testlerde değişiklik yapılmasını gerektirmez (yeni fabrikaların kaydının yerel ve çerçeveye entegre olduğu varsayılarak).


İlerlememizi özetlemek gerekirse: Kod incelemesinden sonra geri bildirimleri ele alma maliyetini azaltacak bir çözüm arayışında, türlere dayalı nesnelerin oluşturulmasını tamamen yeniledik. Kodumuz artık Tek Sorumluluk ve Açık/Kapalı ilkelerine (SOLID kısaltmasında "S" ve "O") hiçbir yerde açıkça bahsetmemiş olsak da uygundur.


Şimdi görevi daha karmaşık hale getirelim ve aynı işi kodda daha az belirgin değişikliklerle gerçekleştirelim. FactoryA sınıfındaki kodu inceleyelim:


 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); } }


Bu kod için testlerin yazımını basitleştirebilir miyiz? İlk if bloğunu parçalayalım:


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


Bunu testlerle ele almaya çalışalım:


 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']); }


Varoluş sorunu kolayca ele alınıyorsa , tip testi pek çok tuzak içeriyor. Bu testte bir dizeyi geçtik, peki ya diğer türler? Büyük bir sayı bir tam sayı mı yoksa kayan noktalı sayı olarak mı kabul edilir (örneğin, PHP'de, 10 üzeri 100, kayan nokta türünün 1.0E+100 gibi kısa bir temsilini döndürür)? Olası tüm senaryolar için bir DataProvider yazabilir veya doğrulama mantığını ayrı bir sınıfa çıkarıp şöyle bir şey elde edebilirsiniz:


 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); } }


Bir yandan yeni bir bağımlılık ekledik ve hatta belki de onu yaratmak zorunda kaldık. Ama bunun karşılığında diğer tüm fabrikalarda bu tür sorunlar hakkında endişelenmemize gerek yok. Mevcut fabrikadaki test yalnızca bir testtir ve id parametresinin tüm olası varyasyonlarını kapsar:


 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); }


Bir sonraki kod bloğuna bakalım:


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


Bu blokta, null veya getSomeParams yöntemine sahip bir varlık döndüren aRepository ( findById ) bağımlılığının yöntemi çağrılır. getSomeParams yöntemi ise bir dizi veri döndürür.


Gördüğümüz gibi $aEntity değişkeni yalnızca getSomeParams yöntemini çağırmak için gereklidir. Öyleyse, neden varsa getSomeParams sonucunu doğrudan, yoksa boş bir diziyi alamıyorsunuz?


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


Öncesi ve sonrası testleri karşılaştıralım. Değişikliklerden önce 3 olası davranışımız vardı: 1) varlık bulunduğunda ve getSomeParams boş olmayan bir veri dizisi döndürdüğünde, 2) varlık bulunduğunda ve getSomeParams boş bir veri dizisi döndürdüğünde, 3) varlık bulunamadı.


 // 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);


Değiştirilen kodda yalnızca iki olası senaryo vardır: findSomeParamsById boş bir dizi döndürür veya döndürmez.


 // 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']);


Test sayısını azaltmanın yanı sıra $this->createConfiguredMock(SomeEntity::class, [..] dan da kurtulduk.
Sonra bloğa bakalım:


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


Gerekli türdeki verileri çıkarabilen bir sınıfımız zaten olduğundan, fabrika kodundaki kontrolleri kaldırarak bunu kullanabiliriz:


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


Sonunda şöyle bir sınıf elde ediyoruz:


 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 yönteminin yalnızca 4 testi olacaktır:

  • ilk istisna için bir test ( getIntByKey )
  • findSomeParamsById boş olmayan bir sonuç döndürdüğü test
  • findSomeParamsById boş bir sonuç döndürdüğünde ve ikinci istisna ( getArrayByKey ) tetiklendiğinde yapılan bir test
  • findSomeParamsById boş bir sonuç döndürdüğü ve ObjectA default dizideki değerlerle oluşturulduğu bir test

Ancak görev gereksinimleri buna izin veriyorsa ve ErrorException , ExtractorException, ile değiştirilebiliyorsa kod daha da kısa olacaktır:


 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); } }


Ve sadece iki test olacak:

  • findSomeParamsById boş olmayan bir sonuç döndürdüğü test

  • findSomeParamsById boş bir sonuç döndürdüğü ve ObjectA default dizideki değerlerle oluşturulduğu bir test


Yapılan çalışmaları özetleyelim.


Başlangıçta, test kapsamına alınması gereken kötü yazılmış kodlarımız vardı. Herhangi bir geliştirici koduna güvendiğinden (bir şey hatayla çökene kadar), bunun için testler yazmak kimsenin hoşlanmadığı uzun ve monoton bir iştir. Daha az test yazmanın tek yolu, bu testlerin kapsaması gereken kodu basitleştirmektir. Sonunda geliştirici, test sayısını basitleştirerek ve azaltarak, herhangi bir spesifik teorik uygulamayı takip etmesine gerek kalmadan kodu geliştirir.