यह कोई रहस्य नहीं है कि जब एक नई टीम बनाने की बात आती है, तो नेताओं (टीम लीडर, टेक लीडर) को एकीकृत प्रोग्रामिंग शैली स्थापित करने की चुनौती का सामना करना पड़ता है, क्योंकि टीम के सभी सदस्य नए होते हैं, और कोड को व्यवस्थित करने और चयन करने के लिए प्रत्येक का अपना दृष्टिकोण होता है। अभ्यास. आमतौर पर, इससे कोड समीक्षाओं के दौरान लंबी बहस होती है, जो अंततः SOLID, KISS, DRY, आदि जैसी प्रसिद्ध प्रथाओं की विभिन्न व्याख्याओं में बदल जाती है। इन प्रथाओं के पीछे के सिद्धांत काफी धुंधले हैं, और पर्याप्त दृढ़ता के साथ, इसे ढूंढना आसान है विरोधाभास जहां एक दूसरे का खंडन करता है। उदाहरण के लिए, आइए एकल उत्तरदायित्व और DRY पर विचार करें।
एकल उत्तरदायित्व सिद्धांत (SOLID में "S") को परिभाषित करने की एक भिन्नता में कहा गया है कि प्रत्येक वस्तु की एक जिम्मेदारी होनी चाहिए, और यह जिम्मेदारी वर्ग के भीतर पूरी तरह से समाहित होनी चाहिए। DRY सिद्धांत (डोंट रिपीट योरसेल्फ) कोड दोहराव से बचने का सुझाव देता है। हालाँकि, यदि हमारे कोड में एक डेटा ट्रांसफर ऑब्जेक्ट (डीटीओ) है जिसका उपयोग विभिन्न परतों/सेवाओं/मॉड्यूल में किया जा सकता है, तो हमें इनमें से किस सिद्धांत का पालन करना चाहिए? निस्संदेह, कई प्रोग्रामिंग पुस्तकें समान स्थितियों को संबोधित करती हैं, आमतौर पर यह बताती हैं कि यदि हम गुणों और तर्क के समान सेट के साथ विभिन्न वस्तुओं/कार्यों के साथ काम कर रहे हैं, लेकिन विभिन्न डोमेन से संबंधित हैं, तो यह दोहराव नहीं है। हालाँकि, कोई यह कैसे साबित कर सकता है कि ये वस्तुएं अलग-अलग डोमेन से संबंधित होनी चाहिए, और, सबसे महत्वपूर्ण बात यह है कि क्या नेता इस कथन पर जोर देने और साबित करने के लिए तैयार (और आश्वस्त) है?
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.
यह लेख एक ऐसे दृष्टिकोण का सुझाव देता है जो ऐसी अधिकांश विवादास्पद स्थितियों से बचने की अनुमति देता है। इसके अलावा, व्यवहार में प्रत्येक डेवलपर (नेता की आपत्ति के बिना) समझ जाएगा कि वे क्या गलत कर रहे हैं और इसे कैसे सुधारें।
आरंभ करने के लिए, आइए कई अतिरिक्त शर्तें और परिभाषाएँ प्रस्तुत करें:
समीक्षा के लिए प्रस्तुत करने के समय, कार्य पूरा माना जाता है, और यदि यह समीक्षा में पास हो जाता है, तो इसे बिना किसी बदलाव के जारी किया जा सकता है। दूसरे शब्दों में, हम कोड में पूर्व नियोजित परिवर्तन/परिवर्धन की संभावना पर विचार नहीं करते हैं।
टीम में समान रूप से अनुभवी और योग्य विशेषज्ञ शामिल हैं जिन्हें कार्यों को लागू करने में किसी चुनौती का सामना नहीं करना पड़ता है; एकमात्र विसंगति उनके दृष्टिकोण में है।
कोड शैली सुसंगत है और कोड चेकर्स द्वारा सत्यापित है।
विकास का समय महत्वपूर्ण नहीं है, कम से कम उत्पाद की विश्वसनीयता से कम महत्वपूर्ण नहीं है।
हम पहली शर्त की आवश्यकता पर बाद में विचार करेंगे, हालाँकि यह अपने आप में बिल्कुल स्पष्ट है, क्योंकि किसी अधूरे कार्य को समीक्षा के लिए प्रस्तुत करना अतार्किक है। दूसरी शर्त के साथ, हम यह सुनिश्चित करते हैं कि टीम के प्रत्येक सदस्य को एल्गोरिदम चुनने और सौंपे गए कार्य को लागू करने में कोई समस्या नहीं है। तीसरी स्थिति में, हम मानते हैं कि टीम एक विशिष्ट शैली (पीएसआर) का पालन करती है, और "क्या बेहतर है, कैमलकेस या स्नेक_केस" जैसे प्रश्न नहीं उठते हैं। और अंतिम शर्त इस कार्य में कार्य पूरा करने के लिए प्रयास में परिवर्तन की गणना करने से बचती है।
कई पाठक जानते हैं कि यूनिट परीक्षण से कोड गुणवत्ता में सुधार होता है। आमतौर पर, इसे बताने के बाद, परीक्षण-संचालित विकास (टीडीडी) पद्धति का एक उदाहरण के रूप में उल्लेख किया गया है, जो वास्तव में कोड गुणवत्ता को बढ़ाता है लेकिन व्यवहार में अपेक्षाकृत कम ही लागू होता है क्योंकि कार्यान्वयन से पहले परीक्षण लिखने के लिए उच्च-स्तरीय प्रोग्रामिंग कौशल सेट की आवश्यकता होती है।
पहले बताई गई सुप्रसिद्ध प्रथाओं पर भरोसा किए बिना यूनिट परीक्षण कोड को बेहतर बनाने में कैसे मदद कर सकता है? सबसे पहले, आइए याद रखें कि निर्भरता के रूप में नकली वस्तुओं/मॉड्यूल का उपयोग करके एक विशिष्ट विधि/मॉड्यूल/वर्ग का परीक्षण करने के लिए यूनिट परीक्षण लागू किए जाते हैं।
पहली शर्त का पालन करते हुए, कार्य को समीक्षा के लिए प्रस्तुत करते समय पूरा माना जाना चाहिए। इसलिए, आइए एक परिभाषा प्रस्तुत करें जिसे हम पूर्ण कार्य मानते हैं। कोई कार्य तभी पूरा माना जाता है जब वह नीचे सूचीबद्ध सभी शर्तों को पूरा करता है:
सौंपे गए कार्य की आवश्यकताएँ पूरी की जाती हैं।
सभी नए कोड को प्रोग्राम में विभिन्न एल्गोरिथम स्थितियों सहित यूनिट परीक्षणों द्वारा कवर किया जाना चाहिए।
नया कोड मौजूदा परीक्षणों को नहीं तोड़ता।
चूँकि हमारे पास नए परीक्षण लिखने और पुराने परीक्षण (शर्त 4) को बनाए रखने के लिए असीमित समय है, और प्रत्येक डेवलपर इन परीक्षणों को लिख सकता है और कार्य आवश्यकताओं (शर्त 2) को पूरा कर सकता है, हम मान सकते हैं कि कोई भी कार्य संभावित रूप से पूरा किया जा सकता है। अब, चूँकि हमने पूर्ण किए गए कार्य की परिभाषा पेश की है, हम शर्त 1 को उचित ठहरा सकते हैं: यदि कोड परीक्षणों द्वारा कवर नहीं किया गया है तो उसे समीक्षा के लिए प्रस्तुत नहीं किया जा सकता है; अन्यथा, कोड बिना समीक्षा के अस्वीकार कर दिया जाएगा। इसलिए, एक डेवलपर जानता है कि फीडबैक के बाद कोड समस्याओं को ठीक करने में परीक्षणों को ठीक करना शामिल है। यह प्रतीत होता है कि मामूली बिंदु अच्छा कोड लिखने के लिए एक मौलिक प्रेरक शक्ति बन जाता है।
आइए निम्नलिखित कोड उदाहरण पर विचार करें (इस आलेख में, PHP भाषा का उपयोग उदाहरणों के लिए किया जाता है, लेकिन यह ऑब्जेक्ट-ओरिएंटेड प्रोग्रामिंग प्रतिमान के समर्थन के साथ कोई भी सी-जैसी भाषा हो सकती है):
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 } }
यह काफी सरल निकला, लेकिन यह पांच प्रकारों में से एक के लिए आवश्यक आठ परीक्षणों में से एक है। सभी परीक्षण लिखे जाने के बाद, समीक्षा के दौरान कोई भी प्रतिक्रिया जिसमें परिवर्तन की आवश्यकता होती है, इन परीक्षणों को तोड़ सकती है, और डेवलपर को उन्हें फिर से लिखना या समायोजित करना होगा। उदाहरण के लिए, एक नई निर्भरता (मान लीजिए, एक लकड़हारा) जोड़ने से सभी परीक्षणों में फ़ैक्टरी आरंभीकरण में परिवर्तन होंगे:
$someFactory = new SomeFactory( $aRepository = $this->createMock(ARepository::class), $this->createMock(LoggerInterface::class) );
ध्यान दें कि किसी टिप्पणी की लागत कैसे बढ़ गई है: यदि पहले किसी निर्भरता को जोड़ने/बदलने के लिए केवल SomeFactory
वर्ग में संशोधन की आवश्यकता होती थी, तो अब सभी परीक्षणों (जो 40 से अधिक हो सकते हैं) को भी बदलने की आवश्यकता होगी। स्वाभाविक रूप से, ऐसे परिवर्तनों के कई पुनरावृत्तियों के बाद, एक डेवलपर फीडबैक को संबोधित करने के लिए आवश्यक प्रयास को कम करना चाहेगा। यह कैसे किया जा सकता है? उत्तर स्पष्ट है - प्रत्येक प्रकार के लिए इकाई निर्माण तर्क को एक अलग वर्ग में अलग करें। कृपया ध्यान दें कि हम SOLID/DRY सिद्धांतों आदि पर भरोसा नहीं कर रहे हैं, और हम कोड पठनीयता और डिबगिंग के बारे में अमूर्त चर्चा में शामिल नहीं हो रहे हैं, क्योंकि इनमें से प्रत्येक तर्क पर विवाद हो सकता है। हम केवल परीक्षणों के लेखन को सरल बना रहे हैं, और इसके विरुद्ध किसी डेवलपर के पास कोई प्रतिवाद नहीं है।
संशोधन के बाद, हमारे पास प्रत्येक प्रकार के लिए 5 कारखाने होंगे ( ObjectType::A
, ObjectType::B
, ObjectType::C
, ObjectType::D
, ObjectType::E
)। नीचे 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 हैं)। इसलिए, एक तार्किक समाधान, जो अधिकांश डेवलपर्स संभवतः देखेंगे, एक रजिस्ट्री बनाना है (विशेष रूप से यदि इंटरफ़ेस द्वारा कक्षा पंजीकरण के लिए मूल समर्थन है) और डीटीओ और कारखानों के लिए इंटरफेस घोषित करना है:
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); }
इस प्रकार, आलस्य के लिए धन्यवाद, हमने सही समाधान चुना (शायद अनजाने में) और FactoryB
क्लास ऑब्जेक्ट की स्थिति के आधार पर getType
विधि को संभावित रूप से रोका।
आइए रजिस्ट्री कोड देखें:
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 में, 100 की शक्ति 10 फ्लोट प्रकार के 1.0E+100 जैसा संक्षिप्त प्रतिनिधित्व लौटाएगा)? आप सभी संभावित परिदृश्यों के लिए डेटाप्रोवाइडर लिख सकते हैं, या आप सत्यापन तर्क को एक अलग वर्ग में निकाल सकते हैं और कुछ इस तरह प्राप्त कर सकते हैं:
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
इस ब्लॉक में, निर्भरता aRepository
( findById
) की विधि को कॉल किया जाता है, जो या तो शून्य या getSomeParams
विधि के साथ एक इकाई लौटाता है। 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
सरणी से मानों के साथ बनाया गया था
आइए किए गए कार्यों का सारांश प्रस्तुत करें।
प्रारंभ में, हमारे पास खराब तरीके से लिखा गया कोड था जिसके लिए परीक्षण कवरेज की आवश्यकता थी। चूँकि कोई भी डेवलपर अपने कोड में आश्वस्त होता है (जब तक कि कोई त्रुटि के साथ क्रैश न हो जाए), इसके लिए परीक्षण लिखना एक लंबा और नीरस कार्य है जिसे कोई भी पसंद नहीं करता है। कम परीक्षण लिखने का एकमात्र तरीका उस कोड को सरल बनाना है जिसे इन परीक्षणों में शामिल करने की आवश्यकता है। अंत में, परीक्षणों की संख्या को सरल और कम करके, डेवलपर किसी विशिष्ट सैद्धांतिक प्रथाओं का पालन किए बिना, कोड में सुधार करता है।