۳۰ روز با TDD: روز دوم - مروری بر اصول شیگرایی
نوشته اصلی را در این آدرس میتوانید مطالعه کنید، سعی من این هست که فقط بخشهای مهم و مفید رو ترجمه کنم، همچنین عباراتی تخصصی رو تا حد ممکن ترجمه نکنم مگر اینکه معادل مناسبی به زبان فارسی براش وجود داشته باشه.
در نوشته امروز مرور مختصری خواهیم داشت بر اصول برنامهنویسی شیگرا (OOP یا Object Oriented Programming). حتی اگر برنامهنویس با سابقه و با تجربهای هستید باز هم توصیه میکنم حداقل بخشهای مربوط به Polymorphism و Interface ها را مطالعه کنید چرا که این مباحث معمولاً خیلی اشتباه فهمیده میشوند و البته بخش مهمی از کار TDD را تشکیل میدهند.
OOP چیست؟
Object Oriented Programming یا OOP یک روش برنامهنویسی است که در آن برنامهنویس از اشیا و ایدههای جهان واقعی مدلی انتزاعی (abstract) در کد ایجاد میکند. وقتی با OOP برنامهای ایجاد میکنید، کلاسهایی مینویسید که اشیا دنیای واقعی را مدل میکنند. در برنامه شما از این کلاسها، نمونهها (وهله یا instance) در قالب اشیاء (Objects) ایجاد میشوند و در طول کار برنامه این متدهایی بر روی این اشیاء فراخوانی شده یا به عنوان پارامتر به متدهای دیگر پاس داده میشوند.
عبارات class و object را نمیتوان به جای هم به کار برد. کلاس تعریفی از این است که شی باید چطوری باشد. در واقع کد #C یا VB شما نقشهای است که حاوی متدها و دیگر اعضای کلاس است و object یک نمونه (وهله یا instance) از کلاس است.
OOP دارای سه اصل اساسی است: Encapsulation و Inheritance و Polymorphism
با هم مروری خواهیم داشت بر این اصول مهم.
Encapsulation
معمولاً به Encapsulation با قانون "جعبه سیاه" (Black box) اشاره میشود. در واقع Encapsulation میگوید که وضعیت داخلی یک object باید محافظت شده (protected) بوده و برای موجودیتهای خارجی (External Entities) غیرقابل دسترس باشد. در واقع کد خارجی نمیتواند به صورت مستقیم وضعیت یک شی را ببیند یا تغییر دهد. هر گونه دسترسی به اجزای داخلی از طریق یک Public API انجام میشود و این یعنی اطمینان object از اینکه وضعیت داخلی معتبری دارد چرا که وضعیت داخلی اشیا از طریق متدهای عمومی (Public) تغییر میکند.
به عنوان یک برنامهنویش OOP این مهم هست که وضعیت داخلی (internal state) رو private نگه داریم یا از طریق متدهای public در اختیار کلاسهای خارجی بگذاریم یا از طریق متدهای protected در اختیار کلاسهایی که از کلاس ما ارثبری کردند قرار بدهیم.
نتیجه قانون "جعبه سیاه" این است که من به عنوان مصرفکننده (consumer) یک object، نباید دلواپس کارهای داخلی آن object باشم. در واقع من فقط باید یاد بگیرم که چطور از API های Public استفاده کنم و میتوانم مطمئن باشم که هر کاری که میخواهم object انجام دهد، توسط منطق اعتبارسنجی داخلی که در object پیادهسازی شده مورد بررسی قرار گرفته و در برابر استفاده نادرست محافظت میشود. من به این Black box یک ورودی میدهم و انتظار یک خروجی دارم و به آنچه در این میان میگذرد کاری نخواهم داشت.
اما Encapsulation چه ارتباطی با TDD دارد؟ تستها (آزمونهای واحد) هم مثل هر کد دیگر Object ها را مصرف (consume) میکنند. بنابراین طبق آنچه گفتیم تست باید فقط به Public API کد دسترسی داشته باشد و نه کارهای داخلی کلاسها. این یک مفهوم بسیار مهم برای TDD کاران تازهکار است: شما فقط Public API را تست میکنید نه متدهای private یا protected.
در ادامه این نوشتهها خواهیم دید که متدهای private و protected برای خدمت به Public API ایجاد میشوند و بنابراین نیازی به تست کردن آنها نیست و از طریق تست کردن Public API در واقع آنها هم تست میشوند.
Inheritance
وقتی در حال توسعهدادن نرمافزار خود بر پایه کلاسها هستید، متوجه میشوید که بعضی کلاسها با هم در ارتباط هستند. بعضی مواقع دو یا چند کلاس شبیه به هم نظر میرسند. به عنوان مثال در شکل زیر میتوانید ببیند که سه کلاس Car و Plane و Boat از چند جنبه به هم شبیه هستند:

در حالی که هر کدام از این کلاسها یک نوع متفاوت از وسیله نقلیه را نشان میدهند، میتوانیم ببینیم که ویژگیهای مشترکی دارند. به جای اینکه چند بار کارهای مشابه را در این کلاسها تکرار کنیم، بهتر است که همه این کارها را در یک جا خلاصه کنیم تا بتوانیم توسط اشیا مختلف از آن استفاده کنیم. اینجاست که مفهوم وراثت (Inheritance) وارد میشود.
با Inheritance من میتوانم یک کلاس پایه (base class) تعریف کنم (در این مثال کلاس Vehicle) و Car و Plane و Boat را از آن ارث ببرم این باعث ایجاد یک رابطه والد/فرزند میشود که در شکل زیر نمایش داده میشود:

حالا Car و Plane و Boat میتوانند قابلیتهای خود شامل سه property به نامهای FuelCapacityInGallons و PassengerCapacity و RagneOnFullTankInMiles را از کلاس پایه Vehicle ارثبری کنند. اگر قابلیتهای کلاس Vehicle برای هر یک از کلاسهای مشتق شده (یعنی Car و Plane و Boat) کافی باشد برنامهنویس لازم نیست هیچ کار دیگری بکند. با این حال اگر کلاس مشتق شده به یک قابلیت متفاوت نسبت به پیادهسازی انجام شده در کلاس پایه نیاز داشته باشد، به راحتی میتواند آن قابلیت را با override کردن Property در کلاس خودش، به صورت سفارشی شده پیادهسازی کند. شکل زیر این حالت را نشان میدهد:

در شکل بالا فراخوانی RangeOnFullTankInMiles در کلاس Plane از نسخه مخصوص پیادهسازی شده در خود کلاس Plane استفاده میکند در حالی که در کلاسهای Car و Boat از همان پیادهسازی انجام شده در کلاس پایه Vehicle استفاده میشود.
Polymorphism
اغلب برنامهنویسها مفاهیم Encapsulation و Inheritance را خیلی ساده متوجه میشوند. اما Polymorphism از آن مفاهیمی است که برنامهنویسها معمولاً برای فهمیدنش دچار مشکل هستند و این جای تاسف دارد چرا که بخش زیادی از قدرت اصلی OOP مربوط به تواناییهای Polymorphism هست.
ایده چندریختی (Polymorphism) این است که گرچه دو کلاس ممکن است آنچنان به هم شبیه باشند که مجموعه رفتار (behaviour) مشابهی را به اشتراک بگذارند و بر اساس این رفتارهای مشابه به شکل یکسان با آنها برخورد شود ولی این امکان وجود دارد که آن رفتارها به شکلهای متفاوتی پیادهسازی شوند
در آخرین شکل بالا، همه وسایل نقلیه از کلاس Vehicle مشتق شدهاند ولی به شکلهای مختلفی حرکت میکنند: ماشینها در جاده حرکت میکنند، کشتیها در آب شناور هستند و هواپیماها در هوا پرواز میکنند. ولی همه اینها وسلیه نقلیه هستند پس اگر من یک متد داشته باشم که به یک وسلیه نقلیه نیاز داشته و مهم نباشد وسیله نقلیه چه چیزی هست، میتوانم نوع (Type) از جنس Vehicle را به عنوان پارامتر ورودی در نظر بگیرم و در زمان فراخوانی شی از هر یک از کلاسهای Car یا Boat یا Plane را به آن پاس بدهم.
در واقع class من حتی لزوماً نیازی نیست بداند که چه نوع وسیله نقلیهای به آن پاس داده شده است چرا که این وسیله نقلیه از کلاس Vehicle مشتق شده که من همه چیزی که در موردش لازم دارم را از طریق property ها و method های عمومیاش (Public API) میدانم. من میتوانم تمام property ها و method های عمومی Vehicle را فراخوانی کنم. اگر کلاس وسیله نقلیهای که از آن استفاده میکنم، تعریف یا پیادهسازی دیگری از یک property یا method داشت میتوانم از آن تعریف خاص به جای تعریف کلاس پایه Vehicle استفاده کنم.
این یک ایده بسیار قدرتمند است که به کد من قابلیت کلی یا general شدن را میدهد. لازم نیست برای هر یک از کلاسهای مشتق شده از کلاس Vehicle متدهای مختلفی تعریف کنم. کافی است که یک وهله (instance) از کلاس پایه Vehicle را در کدم بپذیرم و این یعنی از هر یک از کلاسهای مشتق شده از Vehicle نیز میتوانم استفاده کنم.
اما بر اساس این ایده کاری که نمیشود انجام داد این است که متدهایی که به صورت خاص در یک کلاس خاص نوشته شده را استفاده کنیم. مثلاً اگر متدی به نام OpenTrunk در کلاس Car داشته باشم که در کلاس پایه Vehicle تعریف نشده باشد باید قبل از فراخوانی این متد، object وسیله نقلیه را به Car تبدیل (Cast) کنم. به طور کلی اگر دیدید خیلی از این Cast ها در کدتان دارید، احتمالاً یک عیب در طراحی کلاسهایتان دارید: یا کلاس پایه شما به اندازه کافی، general نیست یا کلاس پایه، برای متدی که دارید مینویسید خیلی خیلی کلی است.
Interface
Polymorphism بر اساس وراثت (Inheritance) همیشه بهترین گزینه نیست. به عنوان مثال کلاسهای زیر را در نظر بگیرید (propert ها برای بهتر شدن خوانایی دیاگرام برداشته شدند)

به سه کلاس ردیف پایین یعنی Plane و Bird و FlyingSquirrel نگاه کنید. واضح است که همه این کلاسها کار "پرواز کردن" را انجام میدهند اما به شیوههای متفاوتی. این کلاسها یک رفتار مشترک دارند اما به جز این چیز مشترک دیگری ندارند. دو تا از آنها حیوان هستند و یکی حیوان نیست. تلاش برای قرار دادن این سه کلاس متفاوت در یک کلاس مشترک کار اشتباهی است. پیدا کردن یک abstarction با معنی برای این کلاسها کار بسیار بسیار دشواری است.
خبر خوب این است که لازم نیست برای این کار تلاش کنیم. میتوانیم از Interface استفاده کنیم. Interface در واقع یک قرارداد است که اعلام میکند کد شما Public API مشخصی را پیادهسازی و پشتیبانی خواهد کرد. در واقع لیستی از property ها و method های عمومی (public) است که شما قول میدهید آنها را در کلاستان تعریف کنید. اینکه این قابلیت چطور تعریف میشود جز قرارداد نیست، تنها چیزی که در این قرارداد مهم است این است که آن متدها و ویژگیها وجود خواهند داشت و قابل فراخوانی هستند.
برای اعمال Interface به مساله جاری، من میتوانم یک Interface به نام IFly با یک متد به نام Fly درست کنم. مرحله بعدی پیادهسازی (implement) کردن این Interface در کلاسهای Plane و Bird و FlyingSquirrel به شکل زیر است:

با توجه به اینکه هر کدام از کلاسها Interface من یعنی IFly را implement کردهاند، هر کدام متد Fly خود را خواهند داشت. مثل چندریختی مبتنی بر وراثت (Inheritance based Polymorphism) استفاده از Interface ها این امکان را به من میدهد که متدهایی تعریف کنم که پارامتر ورودی از جنس IFly دارند و میتوانند اشیا از هر نوع کلاسی که این Interface را implement کرده باشد بپذیرند.
شناخت و فهمیدن چندریختی مبتنی بر اینترفیس (interface based Polymorphism) از مهارتهای ضروری TDD است. وقتی در این سری نوشتهها به مبحث mocking و وابستگیها (dependencies) برسیم، دانش شما در این حوزه هر روز به کار خواهد آمد.
ادامه دارد ...