۳۰ روز با TDD: روز پنجم - کد SOLID ایجاد کنید

نوشته روز پنجم به زبان انگلیسی را در این آدرس می‌توانید مطالعه کنید. در روز سوم اولین تست‌مان را نوشتم و در روز چهارم هم مطابق با روش TDD کدمان را تکمیل کردیم تا اولین تستی که نوشته بودیم Pass شود. امروز می‌خواهیم کمی درباره مباحث تئوریکی که پایه کارهای آینده در این سری نوشته‌هاست صحبت کنیم.

آیا کد شما SOLID است؟

اصول SOLID در توسعه نرم‌افزار اصولی هستند که توسط رابرت مارتین معرفی شده‌اند. در اوایل 2000 آقای مارتین 5 اصل را تشریح کرد که توسعه‌دهندگان نرم‌افزار می‌توانند از آن‌ها برای ایجاد نرم‌افزارهای با طراحی خوب، کیفیت بالا و سهولت نگهداری استفاده کنند. این 5 اصل ساده هستند، مروج شیوه‌های خوب در طراحی و تولید نرم‌افزارند و در TDD نیز به ما کمک می‌کنند.
SOLID سرنام این اصول پنج‌گانه است: Single Responsibility Principle , Open/Close Principle, Liskov Substitution Principle, Interface Segregation Principle , Dependency Inversion Principle

در ادامه این نوشته به بررسی مفاهیم این اصول می‌پردازیم:

Single Responsibility Principle

ایده Single Responsibility Principle یا به اختصار SRP این است که هر متد یا کلاس در برنامه شما باید تنها یک دلیل برای تغییر داشته باشد. به صورت منطقی، می‌توان این ایده را این‌طور گسترش داد که هر کلاس یا متد در برنامه باید دقیقاً فقط یک وظیفه (یا کار) داشته باشد. به عبارت بهتر، هر کلاس یا متد باید در برابر یک و فقط یک وظیفه مسئول باشد.

به عنوان مثال اجازه بدهید کلاسی را با توابعی برای سبد خرید در یک سایت فروشگاه الکترونیک را فرض کنیم. به عنوان یک سبد خرید مجازی منطقی است که کلاس یک مجموعه (collection) از مواردی که کاربر آن‌ها را جهت خرید به سبد خود اضافه کرده و احتمالاً یک راه برای برقراری ارتباط با سرویسی جهت پرداخت داشته باشد. برای توسعه دادن این مثال فرض کنیم که این فروشگاه الکترونیک یک روال امتیاز وفاداری (loyalty reward) دارد که به مشتریان بر اساس خریدشان امتیاز جایزه می‌دهد.
هیچ‌کدام از قابلیت‌های برای جایزه دادن، رهگیری یا مدیریت امتیازات، مناسب اضافه شدن به کلاس سبد خرید نیستند. این قابلیت مربوط به امتیازات باید در سرویس جدایی ایجاد شود. سبد خرید نباید مسئول امتیازات باشد و در واقع حتی نباید از وجود برنامه امتیازات خبر داشته باشد. سبد خرید فقط یک کار دارد: ذخیره لیست مواردی که کاربر قصد خرید آن‌ها را دارد. بر این اساس کلاس سبد خرید فقط یک دلیل برای تغییر دارد: زمانی که روش ذخیره‌سازی آیتم‌های لیست خرید مشتری تغییر کند. تغییر در برنامه امتیازدهی به مشتریان، نباید هیچ تاثیری در سبد خرید داشته باشد بنابراین وقتی برنامه امتیازدهی تغییر می‌کند نیازی به تغییر کلاس سبد خرید نیست.

با اطمینان از اینکه متدها و کلاس‌هایی می‌نویسیم که فقط یک وظیفه دارند، این متدها را راحت‌تر قابل آزمایش می‌کنیم. متدهایی که کارهای زیادی انجام می‌دهند، نیازمند تست‌هایی با Arrange پیچیده‌تر هستند که باعث طولانی و مشکل شدن فهم و نگهداری تست‌ها می‌شود.

همچنین می‌توان SRP را به تست‌ها و روشی که آن‌ها را می‌نویسیم نیز تعمیم داد. در حال ایده‌آل هر تستی که می‌نویسیم فقط باید یک چیز را مورد آزمایش قرار دهد. این باعث زیاد شدن تست‌ها می‌شود اما مزایای خاص خود را دارد. اول اینکه خود تست‌ها ساده‌تر نوشته شده و راحت‌تر فهمیده می‌شوند. نکته مهم دیگر این است که وقتی تست شما fail می‌شود، اطلاعات خوب و مشخصی راجع به اینکه کجا دنبال مشکل بگردید خواهید داشت. اگر تست fail شود و فقط یک کار تست شده باشد، فقط یک محل برای بررسی وجود خواهد داشت. زمانی که یک تست بیش از یک کار انجام می‌دهد،‌ در هنگام fail شدن باید زمان بیشتری را صرف پیدا کردن مشکل کرد.

The Open/Close Principle

اصل Open/Close یا به اختصار OCP ارتباط نزدیکی با مباحث Encapsulation و Inheritance که روز دوم در موردشان صحبت کردیم دارد. در حقیقت می‌توان گفت OCP ایده‌ای است که این دو قانون OOP را با هم متحد می‌کند. OCP بیان می‌کند که در نرم‌افزار، خواه متد یا کلاس، باید راه برای توسعه (extension) باز و برای تغییر (modification) بسته باشد. برای اینکه بهتر متوجه دو بخش این عبارت شویم، هر کدام را به صورت جداگانه بررسی می‌کنیم.

وقتی برنامه‌نویسان یک نرم‌افزار می‌نویسند اغلب متکی به کتابخانه‌های نرم‌افزاری نوشته شده توسط سایر برنامه‌نویسان هستند. به منظور اینکه این اجزا (components) به صورت گسترده مورد استفاده قرار می‌گیرند، قابلیت‌های آن‌ها را در کلی‌ترین (general) حالات در نظر می‌گیرند. اغلب اوقات که از قابلیت‌های این component ها به همان صورتی که هست استفاده می‌کنیم، به معنی این است که نیاز ما در دایره حالات کلی تعریف شده در آن کتابخانه قرار دارد. اما بعضی اوقات ممکن است به نسخه خاص‌تری از این component ها نیاز داشته باشیم.
بر اساس OCP این component ها را باید بتوان گسترش داد (راه برای extension باز باشد). راه‌های مختلفی برای این کار وجود دارد: مشخص‌ترین راه ایجاد یک کلاس جدید مشتق شده از کلاس پایه (base) مربوط به component‌ است که یا متدهای موجود آن را override کند یا متدهای جدیدی را بر اساس نیاز به آن اضافه کند.
راه دیگری که نسبت به راه قبلی کمتر واضح است این است که از یک اصل دیگر SOLID به نام Dependency Inversion استفاده کنیم که در ادامه درباره‌اش توضیح خواهم داد.
این دو راه به من کمک می‌کند که قابلیت‌های یک کلاس را توسعه یا تغییر بدهم بدون اینکه داخل آن را دستکاری کنم چرا که “راه برای توسعه باز است”

دومین بخش OCP می‌گوید که راه برای تغییرات بسته است. این بخش با مفاهیم Encapsulation در ارتباط است و بیان می‌کند که با کارهای داخلی component ها باید به صورت خصوصی (private) برخورد شود. در این شرایط OCP‌ می‌گوید که اگر می‌خواهید یک قابلیت اضافی به component اضافه کنید یا روش کار قابلیت موجود را تغییر دهید، گزینه‌های کمی در اختیار دارید و تغییر داخل یک component به نحوی که Public API (یا قانون Encapsulation) را تحت تاثیر قرار دهد یکی از آن گزینه‌ها نیست!

بسته نگه‌داشتن base component ها تضمین می‌کند که دیگر component‌های وابسته به آن‌ها از تغییرات غیرمنتظره مربوط به قابلیت‌های جدید زجر نکشند. همچنین تضمین می‌کند که با آمدن آپدیت برای آن component‌ها شما می‌توانید آن به روزرسانی‌ها را با برنامه خود ترکیب (integrate) کنید.

وقتی در این نوشته و نوشته بعدی شروع به صحبت درباره Dependency Inversion کنیم OCP در TDD‌ بیشتر با معنی می‌شود. اما ذات OCP در mocking‌ (که در نوشته‌های بعدی به آن خواهیم پرداخت) به ما کمک می‌کند که مطمئن شویم راه mocking‌ در کلاس‌های ما با Dependency Inversion باز است.

The Liskov Substitution Principle

اصل Liskov Substitution یا به اختصار LSP بیان می‌کند که یک شی در برنامه شما باید قابلیت جایگزینی با شی‌ از کلاسی که از آن مشتق شده است را بدون ایجاد مشکل در برنامه داشته باشد. به عنوان مثال در بحث قبلی که در خصوص Polymorphism در روز دوم داشتیم درباره ایده super class‌ و Public API‌ در آن صحبت کردیم. برای یادآوری اگر یک کلاس از کلاس پایه (base) ارث بری کرده باشد آن وقت کلاس پایه super class و کلاس ارث بری شده کلاس مشتق (derived) نامیده می‌شود. به عنوان مثال Animal یک super class برای Dog است در حالی که Dog یک کلاس مشتق شده از  Animal است.

بر اساس LSP اگر برنامه من انتظار یک شی از نوع Animal داشته باشد، من باید بتوانم هر کلاسی که از Animal مشتق شده را به جای آن پاس بدهم (مثلاً Dog, Cat, Fish‌ و…) بدون اینکه مشکل و ایرادی در برنامه ایجاد شود. برنامه با این شی مشتق شده به عنوان یک Animal کلی (Generic) برخورد می‌کند (یعنی فقط متدهای Public API‌ مربوط به Animal‌ را می‌توان برای آن شی فراخوانی کرد) و لازم نیست بداند یا اهمیت بدهد که واقعاً چه نوع کلاسی پاس داده شده است.

مثل OCP قدرت اصلی LSP وقتی مشخص می‌شود که درباره Dependency Inversion صحبت کنیم. LSP به همراه OCP و Dependency Inversion امکان mocking را می‌دهد. به صورت خلاصه LSP به قابل آزمایش کردن کدهای ما از طریق ایجاد یک جایگزین برای کلاس‌های وابسته در کد کمک می‌کند که خودش باعث ایزوله شدن تست‌ از برنامه و وابستگی‌هایش می‌شود.

The Interface Segregation Principle

اصل Interface Segregation یا به اختصار ISP بیان می‌کند که مشتری‌ها نباید مجبور به استفاده از interface‌ هایی شوند که استفاده نمی‌کنند. در واقع شما باید interface های خوبی که به طور خاص برای نیاز و قابلیت مورد نظر مشتری هستند ایجاد کنید.

به عنوان مثال شما ممکن است سرویسی برای پذیرش تمام درخواست‌های وام یک بانک داشته باشید. اما مشتری شما ممکن است بین وام‌های امن (مثل وام مسکن و خودرو و …) و وام‌های ناامن (کارت اعتباری و …) تفاوت قائل شوند. به علاوه، API سرویس شما ممکن است متدهای مختلفی برای انواع مختلف وام داشته باشد. بر اساس ISP ایجاد یک intreface کلی که تمام این حالات را پوشش دهد راه اشتباهی است، به جای این کار شما باید چندین interface کوچکتر داشته باشید که نیاز تجاری خاصی را هدف گرفته‌اند.

مزیت این اصل، خیلی با اهمیت آن در TDD در ارتباط است. یک interface بزرگ که پر از متدها و property هایی است که مشتری به ندرت از آن‌ها استفاده می‌کند، interface را سنگین و پیچیده می‌کند. در TDD اینترفیس‌های کوچکتر را راحت‌تر می‌توان mock‌ کرد که باعث می‌شود نتایج تست‌ ما کوچکتر و با پیچیدگی کمتر و فهم آن‌ها راحت‌تر باشند.

The Dependency Inversion Principle

اتصال (Coupling) و انقیاد (binding) در نرم‌افزار یک حقیقت است. با همه تلاش‌هایی که می‌کنیم، در آخر کلاس ما برای اینکه قابل استفاده باشد باید بتواند چیزی را bind کند. بر این اساس، ما باید این اتصالات را تا حد ممکن شل کنیم. این جایی است که اصل Dependency Inversion  یا به اختصار DIP وارد می‌شود.

DIP ایده‌ای است که می‌گوید کد باید به یک چیز انتزاعی (abstractions) وابسته باشد نه یک پیاده‌سازی (implementation) واقعی. به علاوه آن abstratcion ها نباید به جزئیات وابسته باشند و جزئیات نیز نباید به abstraction وابسته باشد.این یک راه پیچیده برای بیان یک ایده ساده است.

به عنوان مثال شما یک نرم‌افزار منابع انسانی فروخته‌اید، برنامه توسط سرویس‌های دیتابیس‌ مختلفی قابل استفاده است. برنامه یک بخش مدیریت کارمندان دارد که در کنار کارهای دیگر، اطلاعات کارمندان را در دیتابیس سازمان به روز می‌کند. مدیریت کارمندان، احتمالاً بخشی دارد که دسترسی به دیتابیس را کنترل می‌کند. شما نبایست بخش مدیریت کارمندان را طوری بنویسید که به MS SQL Server یا Oracle وابسته باشد، به جای این کار، بایستی مدیریت کارمندان را طوری بنویسید که به یک سرویس داده کلی (generic) وابسته باشد که MS SQL Server یا Oracle را بتوان از آن سرویس generic مشتق کرد. در این حالت موقع نصب می‌توانم هر یک از این دیتابیس‌ها را که از سرویس داده پایه من مشتق شده باشند را استفاده کنم. در مثال نرم‌افزار منابع انسانی، ما وابستگی را invert کرده‌ایم، به جای اینکه به MS SQL Server یا Oracle و جزئیاتشان وابسته باشیم به یک abstraction که هر دو این دیتابیس‌ها به آن وابسته هستند،‌ وابستگی ایجاد می‌کنیم.

لطفاً توجه کنید که Dependency Inversion مشابه Dependency Injection نیست. Dependency Injection یک روش برای رسیدن به Dependency Inversion است، ولی این دو با هم یکی نیستند. Dependency Injection را در نوشته بعدی مورد بررسی قرار خواهیم داد. Dependency Injection یک بخش بسیار مهم در TDD است و در نوشته بعدی خواهید دید که چطور DIP و Dependency Injection به ما در استفاده از mocking در تست‌هایمان کمک می‌کند.

ادامه دارد…