এটা কোন গোপন বিষয় নয় যে যখন একটি নতুন দল গঠনের কথা আসে, তখন নেতারা (টিম লিডার, টেক লিডার) একটি ইউনিফাইড প্রোগ্রামিং স্টাইল প্রতিষ্ঠার চ্যালেঞ্জের মুখোমুখি হন, কারণ দলের সকল সদস্যই নতুন, এবং প্রত্যেকের কোড সংগঠিত করার এবং নির্বাচন করার নিজস্ব পদ্ধতি রয়েছে। অনুশীলন সাধারণত, এটি কোড পর্যালোচনার সময় দীর্ঘ বিতর্কের দিকে পরিচালিত করে, যা শেষ পর্যন্ত সুপরিচিত অভ্যাস যেমন SOLID, KISS, DRY ইত্যাদির বিভিন্ন ব্যাখ্যায় বৃদ্ধি পায়। প্যারাডক্স যেখানে একটি অন্যটির বিরোধিতা করে। উদাহরণস্বরূপ, আসুন একক দায়িত্ব এবং DRY বিবেচনা করি।
একক দায়বদ্ধতার নীতি (SOLID-এ "S") সংজ্ঞায়িত করার একটি ভিন্নতা বলে যে প্রতিটি বস্তুর একটি দায়িত্ব থাকা উচিত এবং এই দায়িত্বটি সম্পূর্ণভাবে ক্লাসের মধ্যে অন্তর্ভুক্ত করা উচিত। DRY নীতি (নিজেকে পুনরাবৃত্তি করবেন না) কোড ডুপ্লিকেশন এড়ানোর পরামর্শ দেয়। যাইহোক, যদি আমাদের কোডে ডেটা ট্রান্সফার অবজেক্ট (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.
এই নিবন্ধটি এমন একটি পদ্ধতির পরামর্শ দেয় যা এই ধরনের বেশিরভাগ বিতর্কিত পরিস্থিতি এড়ানোর অনুমতি দেয়। তদ্ব্যতীত, অনুশীলনে প্রতিটি বিকাশকারী (নেতার আপত্তি ছাড়াই) বুঝতে পারবে তারা কী ভুল করছে এবং কীভাবে এটি উন্নত করা যায়।
শুরু করার জন্য, আসুন কয়েকটি অতিরিক্ত শর্ত এবং সংজ্ঞা প্রবর্তন করি:
পর্যালোচনার জন্য জমা দেওয়ার সময়, টাস্কটি সম্পূর্ণ বলে বিবেচিত হয় এবং যদি এটি পর্যালোচনায় উত্তীর্ণ হয়, তবে এটি কোনো পরিবর্তন ছাড়াই প্রকাশ করা যেতে পারে। অন্য কথায়, আমরা কোডে পূর্ব-পরিকল্পিত পরিবর্তন/সংযোজনের সম্ভাবনা বিবেচনা করি না।
দলটি সমানভাবে অভিজ্ঞ এবং যোগ্য বিশেষজ্ঞদের নিয়ে গঠিত যারা কাজগুলি বাস্তবায়নে কোন চ্যালেঞ্জের সম্মুখীন হন না; একমাত্র অমিল তাদের দৃষ্টিভঙ্গির মধ্যে রয়েছে।
কোড শৈলী সামঞ্জস্যপূর্ণ এবং কোড চেকার দ্বারা যাচাই করা হয়।
উন্নয়ন সময় সমালোচনামূলক নয়, পণ্যের নির্ভরযোগ্যতার তুলনায় অন্তত কম গুরুত্বপূর্ণ।
আমরা পরে প্রথম শর্তের প্রয়োজনীয়তা বিবেচনা করব, যদিও এটি নিজে থেকেই বেশ স্পষ্ট, কারণ এটি পর্যালোচনার জন্য একটি অসমাপ্ত কাজ জমা দেওয়া অযৌক্তিক। দ্বিতীয় শর্তের সাথে, আমরা নিশ্চিত করি যে প্রতিটি দলের সদস্যের একটি অ্যালগরিদম বেছে নেওয়া এবং নির্ধারিত কাজটি বাস্তবায়নে কোনও সমস্যা নেই। তৃতীয় শর্তে, আমরা অনুমান করি যে দলটি একটি নির্দিষ্ট শৈলী (PSR) মেনে চলে এবং "কী ভাল, ক্যামেলকেস বা স্নেক_কেস" এর মতো প্রশ্ন ওঠে না। এবং চূড়ান্ত শর্ত এই কাজে টাস্ক সমাপ্তির প্রচেষ্টার পরিবর্তন গণনা করা থেকে বিরত থাকে।
অনেক পাঠক সচেতন যে ইউনিট পরীক্ষা কোডের মান উন্নত করে। সাধারণত, এটি বলার পরে, টেস্ট-চালিত উন্নয়ন (TDD) পদ্ধতি একটি উদাহরণ হিসাবে উল্লেখ করা হয়, যা প্রকৃতপক্ষে কোডের গুণমানকে উন্নত করে কিন্তু বাস্তবে তুলনামূলকভাবে খুব কমই প্রয়োগ করা হয় কারণ বাস্তবায়নের আগে পরীক্ষা লেখার জন্য একটি উচ্চ-স্তরের প্রোগ্রামিং দক্ষতা সেট প্রয়োজন।
পূর্বে উল্লিখিত সুপরিচিত অনুশীলনের উপর নির্ভর না করে ইউনিট পরীক্ষা কীভাবে কোড উন্নত করতে সহায়তা করতে পারে? প্রথমে, আসুন স্মরণ করি যে ইউনিট পরীক্ষাগুলি নির্ভরতা হিসাবে মক অবজেক্ট/মডিউল ব্যবহার করে একটি নির্দিষ্ট পদ্ধতি/মডিউল/শ্রেণী পরীক্ষা করার জন্য প্রয়োগ করা হয়।
প্রথম শর্ত অনুসরণ করে, পর্যালোচনার জন্য জমা দেওয়ার সময় কাজটি সম্পূর্ণ বলে বিবেচনা করা উচিত। অতএব, আসুন আমরা একটি সম্পূর্ণ কাজ বিবেচনা করি তার একটি সংজ্ঞা প্রবর্তন করি। একটি কাজ শুধুমাত্র তখনই সম্পন্ন বলে গণ্য করা হয় যখন এটি নীচে তালিকাভুক্ত সমস্ত শর্ত পূরণ করে:
নির্ধারিত কাজের প্রয়োজনীয়তা পূরণ করা হয়।
প্রোগ্রামের বিভিন্ন অ্যালগরিদমিক অবস্থা সহ সমস্ত নতুন কোড ইউনিট পরীক্ষার দ্বারা আবৃত করা আবশ্যক।
নতুন কোড বিদ্যমান পরীক্ষা ভঙ্গ করে না।
যেহেতু আমাদের কাছে নতুন পরীক্ষা লেখার জন্য এবং পুরানোগুলি (শর্ত 4) বজায় রাখার জন্য সীমাহীন সময় আছে, এবং প্রতিটি বিকাশকারী এই পরীক্ষাগুলি লিখতে পারে এবং কাজের প্রয়োজনীয়তাগুলি (শর্ত 2) পূরণ করতে পারে, আমরা বিবেচনা করতে পারি যে কোনও কাজ সম্ভাব্যভাবে সম্পন্ন করা যেতে পারে। এখন, যেহেতু আমরা একটি সম্পূর্ণ কাজের সংজ্ঞা প্রবর্তন করেছি, তাই আমরা শর্ত 1 ন্যায্যতা দিতে পারি: কোডটি পর্যালোচনার জন্য জমা দেওয়া যাবে না যদি এটি পরীক্ষার দ্বারা আচ্ছাদিত না হয়; অন্যথায়, কোডটি পর্যালোচনা ছাড়াই প্রত্যাখ্যান করা হবে। অতএব, একজন বিকাশকারী জানেন যে প্রতিক্রিয়ার পরে কোডের সমস্যাগুলি ফিক্সিং পরীক্ষাগুলি ফিক্সিং জড়িত৷ এই আপাতদৃষ্টিতে ছোটখাট পয়েন্টটি ভাল কোড লেখার জন্য একটি মৌলিক চালিকা শক্তি হয়ে ওঠে।
আসুন নিম্নলিখিত কোড উদাহরণটি বিবেচনা করি (এই নিবন্ধে, পিএইচপি ভাষা উদাহরণের জন্য ব্যবহার করা হয়েছে, তবে এটি অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং প্যারাডাইমের সমর্থন সহ যে কোনও সি-এর মতো ভাষা হতে পারে):
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-এর বেশি হতে পারে) পরিবর্তন করতে হবে। স্বাভাবিকভাবেই, এই ধরনের পরিবর্তনের বেশ কয়েকটি পুনরাবৃত্তির পরে, একজন বিকাশকারী প্রতিক্রিয়া জানাতে প্রয়োজনীয় প্রচেষ্টাকে কমিয়ে আনতে চাইবেন। কিভাবে এই কাজ করা যেতে পারে? উত্তরটি সুস্পষ্ট - প্রতিটি ধরণের জন্য সত্তা সৃষ্টির যুক্তিকে একটি পৃথক শ্রেণিতে বিচ্ছিন্ন করুন। অনুগ্রহ করে মনে রাখবেন যে আমরা সলিড/ড্রাই নীতির উপর নির্ভর করছি না, ইত্যাদি, এবং আমরা কোড পঠনযোগ্যতা এবং ডিবাগিং সম্পর্কে বিমূর্ত আলোচনায় জড়িত নই, কারণ এই যুক্তিগুলির প্রত্যেকটি বিতর্কিত হতে পারে। আমরা কেবল পরীক্ষার লেখা সহজতর করছি, এবং এর বিরুদ্ধে কোনও বিকাশকারীর পক্ষে কোনও পাল্টা যুক্তি নেই৷
পরিবর্তনের পরে, আমাদের প্রতিটি প্রকারের জন্য 5টি কারখানা থাকবে ( ObjectType::A
, ObjectType::B
, ObjectType::C
, ObjectType::D
, ObjectType::E
)। নিচে ObjectType::A
(ফ্যাক্টরিএ) এর জন্য কারখানার একটি উদাহরণ দেওয়া হল:
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 পর্যন্ত), একটি নতুন কারখানার যেকোন সংযোজন/অপসারণ পুরানো পরীক্ষায় পরিবর্তন আনতে পারে না (ধরে নেওয়া হয় যে নতুন কারখানার নিবন্ধন স্থানীয় এবং কাঠামোর সাথে একত্রিত)।
আমাদের অগ্রগতির সংক্ষিপ্তসারের জন্য: একটি কোড পর্যালোচনার পরে প্রতিক্রিয়া জানানোর খরচ কমানোর জন্য একটি সমাধানের অন্বেষণে, আমরা প্রকারের উপর ভিত্তি করে বস্তুর প্রজন্মকে সম্পূর্ণরূপে সংশোধন করেছি। আমাদের কোড এখন একক দায়বদ্ধতা এবং খোলা/বন্ধ নীতিগুলি (সলিড সংক্ষিপ্ত রূপের "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-blockটি ভেঙে ফেলি:
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 ফ্লোট টাইপের মতো একটি সংক্ষিপ্ত উপস্থাপনা দেবে)? আপনি সমস্ত সম্ভাব্য পরিস্থিতির জন্য একটি ডেটাপ্রোভাইডার লিখতে পারেন, অথবা আপনি একটি পৃথক ক্লাসে বৈধতা যুক্তি বের করতে পারেন এবং এর মতো কিছু পেতে পারেন:
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
একটি খালি ফলাফল ফেরত দেয় এবং default
অ্যারে থেকে মান দিয়ে ObjectA
তৈরি করা হয় যাইহোক, যদি টাস্কের প্রয়োজনীয়তা এটির অনুমতি দেয়, এবং 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
একটি খালি ফলাফল ফেরত দেয় এবং default
অ্যারে থেকে মান দিয়ে ObjectA
তৈরি করা হয়
এর কাজ সংক্ষিপ্ত করা যাক.
প্রাথমিকভাবে, আমাদের কাছে খারাপভাবে লিখিত কোড ছিল যা পরীক্ষার কভারেজের প্রয়োজন ছিল। যেহেতু যেকোন বিকাশকারী তাদের কোডে আত্মবিশ্বাসী (যতক্ষণ না কিছু একটি ত্রুটি সহ ক্র্যাশ হয়), এটির জন্য পরীক্ষা লেখা একটি দীর্ঘ এবং একঘেয়ে কাজ যা কেউ পছন্দ করে না। কম পরীক্ষা লেখার একমাত্র উপায় হল কোডটি সরলীকরণ করা যা এই পরীক্ষার দ্বারা কভার করা দরকার। শেষ পর্যন্ত, পরীক্ষার সংখ্যা সরলীকরণ এবং হ্রাস করে, বিকাশকারী অগত্যা কোনো নির্দিষ্ট তাত্ত্বিক অনুশীলন অনুসরণ না করেই কোডটিকে উন্নত করে।