فصل هشتم Clean Architecture -  باز برای توسعه بسته برای تغییر

۱. مقدمهاصل OC یا Open Closed Principle در سال 1988 توسط برتراند مایر معرفی شد. او اینگونه این اصل را تعریف می‌کرد:نرم‌افزار باید برای توسعه باز و برای تغییر بسته باشند.به عبارت دیگر رفتار یک نرم افزار باید بدون تغییر در نرم‌افزار قابل گسترش باشد. دقیقا به همین دلیل است که ما در مورد مهندسی نرم‌افزار مطالعه می‌کنیم. اگر برای افزودن یک ویژگی به نرم‌افزار نیاز داشته باشیم که تغییرات زیادی ایجاد کنیم، احتمالا معماری مناسبی نداریم و باید به فکر بازبینی و تغییر آن باشیم.اغلب توسعه دهنده‌ّها و علاقه مندان به این حوزه OCP را اصلی میدانند که به آن‌ها در طراحی کلاس‌ها و متد‌ها راهنمایی می‌کند. اما اگر کمی دقیق‌تر بررسی کنیم، در می‌یابیم که این اصل فقط در این سطح کمک حال ما نیست. در ادامه با این موضع دقیق‌تر آشنا می‌شویم.۲. یک تجربه فکریفرض کنید که سامانه تحت وبی داریم که اطلاعات مالی را نمایش می‌دهد، صفحه قابلیت اسکرول دارد و اعداد منفی با رنگ قرمز نمایش داده می شوند. در ادامه کارفرما از ما می‌خواهد که امکان پرینت این داده‌ها را به او بدهیم، با این تفاوت که دیگر خبری از اسکرول نیست و باید اطلاعات در قالب صفحاتی با کادر بندی صحیح نمایش داده شوند و با توجه به سیاه و سفید بودن پرینتر‌ها، اعداد منفی باید در پرانتز نمایش داده شوند.مسلما باید کدهای جدیدی نوشته شود، اما چقدر از کدهای قبلی باید تغییر کند؟ یک معماری نرم‌افزار خوب مقدار تغییرات در کدهای قدیمی را به حداقل می‌رساند، یا اگر کمی ایده‌آل گرا باشیم به صفر می‌رساند. اما معماری نرم‌افزار چگونه این کار را انجام می‌دهد؟ جواب ساده است، با جدا کردن بخش‌هایی که به دلایل مختلفی تغییر می‌کنند یا همان SRP که قبلا درباره آن صحبت کرده ایم. و در ادامه وابستگی‌های بین آن‌ها را ساختاردهی می‌کند که این اصل یعنی Dependency Inversion Principle را بعدا بررسی خواهیم کرد.با رعایت اصل SRP با ساختاری مشابه تصویر زیر مواجه خواهیم شد. ابتدا داده‌ها توسط بخش پردازش داده تحلیل و ساختار دهی می‌شود و بعد توسط دو ماژول مجزا برای نمایش در وب یا پرینت آماده می‌شود.نکته حیاتی در طراحی این بخش از نرم‌افزار این است که بدانیم این ماژول دو کار کاملا مجزا انجام می‌دهد که به دلایل مختلفی تغییر می‌کند. یک بخش از کار محاسبه و پردازش داده‌های ورودی است و بخش دیگر آماده سازی برای نمایش که در حال حاضر در دو فرمت وب و پرینت شده باید وجود داشته باشد.با در نظر گرفتن این طراحی باید به این موضوع دقت کنیم که باید وابستگی‌های داخل سورس کد را به گونه‌ای سازماندهی کنیم که تغییر در یک بخش باعث تغییر در قسمت دیگر نشود و در ادامه امکان افزودن عملکرد‌های جدید بدون تغییر بخش‌های موجود نیز وجود داشته باشد. برای این کار ما این مسئولیت‌ها را در قالب کلاس‌های مجزا توسعه می‌دهیم و کلاس‌ها را در قالب کامپوننت‌هایی مطابق با تصویر زیر دسته بندی می‌کنیم. در این طراحی ما Controllerها و Interactorها را در بالا سمت چپ و راست داریم. در قسمت پایین سمت راست پایگاه داده و پایین سمت چپ Presenterها و Viewها را داریم.در این تصویر I نمایش Interface و Ds برای تعیین ساختار‌های داده است. فلش‌ها با پیکان باز برای نمایش استفاده کردن و فلش‌ها با پیکان بسته برای نمایش پیاده سازی کردن به کار رفته اند. اولین نکته‌ای که می‌خواهم اینجا به آن دقت کنید، وابستگی سورس کد است. فلش از A به ‌B یعنی A به ‌‌B وابسته است و با تغییر آن تغییر می‌کند. اما ‌B هیچ اطلاعی از A ندارد. با این تعریف مطابق تصویر بالا FinancialDataMapper از FinancialDataGateway با خبر بوده و این با خبر از جنس پیاده سازی کردن است. اما FinancialDataGateway چیزی در مورد FinancialDataMapper و وجود آن و نحوه پیاده سازی آن نمی‌داند. نکته دیگر اینکه اگر به مرزهایی که با دو خط کشیده شده است نگاه کنید تنها در یک جهت توسط پیکان‌ها قطع شده اند و از اینجا می‌توانیم نتیجه بگیریم که وابستگی کامپوننت‌های ما یک طرفه است. به نموداد کامپوننت زیر هم اگر دقت کنید در می‌یابیم که پیکان‌ها به ماژولی اشاره می‌کنند که می‌خواهیم از تغییر در امان بماند.بگذارید یک بار دیگر تکرار کنم که برای مثال اگر بخواهیم کامپوننت A از تغییرات کامپوننت ‌B در امان باشد، به این معناست که کامپوننت B باید به کامپوننت A وابسته باشد. ما می‌خواهیم Controller از تغییرات Presenter و Presenter از تغییرات View و Interactor از تغییرات سایر قسمت‌ها در امان باشد. در طراحی بالا Interactor در مناسب‌ترین جایگاه مطابق با OCP قرار دارد. تغییرات در Database, Controller, Presenter و View هیچ تاثیری روی Interactor ندارد.اما چرا همچین امتیازی برای Interactor قائل می‌شویم؟ بخاطر اینکه Interactor قواعد کسب و کار را نگهدارید می‌کند. قواعد سطح بالای کسب و کار بدون وابستگی به تکنولوژی و تغییرات در این قسمت قرار دارند. در حقیقت قلب سیستم توسط Interactor پیاده سازی شده است و بقیه بخش‌ها پیاده سازی نیازمندی‌های جانبی را در بر گرفته اند. دقت می‌کنید که چگونه به صورت سلسله مراتبی سطوح مختلف را از تغییرات حفظ می‌کنیم؟! بالاترین سطح نرم‌افزار ما Interactor است و هیچ وابستگی ندارد و در پایین ترین سطح View قرار دارد که کمترین محافظت را در برابر تغییرات دارد. Presenter از View سطح بالاتر و از Controllerسطح پایین تر است.OCP اینگونه به ما در معماری سیستم یاری می‌رساند. معمار نرم‌افزار بخش‌های مختلف را بر مبنای اینکه چه موقع، چرا و چگونه تغییر می‌کنند جدا کرده و سپس آن‌ها را در سطوح مختلفی از کامپوننت‌ها قرار می‌دهد که در این روش کامپوننت‌های سطوح بالایی از تغییرات سطوح پایین مصون می‌مانند.۳. تعیین جهت وابستگیاگر از پیچیدگی ساختاری که در تصاویر بالا آمده بود وحشت زده شده اید پیشنهاد می‌کنم یک بار دیگر به تصاویر نگاه کنید. بیشتر پیچیدگی موجود در این تصاویر مربوط به بخش‌هایی است که اضافه کرده ایم تا وابستگی در جهت صحیح ایجاد شود. برای مثال FinancialDataGateway را بین FinancialReportGenerator و FinancialDataMapper قراردادیم تا وابستگی بین این دو قسمت را معکوس کنیم در غیر این صورت Interactorبه Database وابسته می‌شد. موارد مشابهی را نیز در سایر بخش‌ها می‌توانید بیابید.۴. مخفی سازی اطلاعاتدر این طراحی FinancialReportRequester هدف دیگری را دنبال می‌کند. این اینترفیس ایجاد شده تا از دریافت اطلاعات بیش از حد نیاز FinancialReportController در مورد جزئیات Interactor جلوگیری کند. اگر این اینترفیس وجود نداشت Controller به FinancialEntities وابسته غیر مستقیم پیدا می‌کرد. در این صورت اصلی که می‌گوید: “اجزای نرم‌افزار نباید به بخش‌هایی که مستقیما مورد استفاده قرار نمی‌دهند وابسته باشند” را زیر سوال می‌بردیم. بنا بر این اصل با اینکه برای ما محافظت از Interactor بالاترین اهمیت را دارد اما تلاش می‌کنیم تا Controller را هم از وابستگی غیر مستقیم به Interactor در امان نگه داریم.۵. جمع بندی:اصل OC یکی از نیروهای محرکه معماری نرم‌افزار است. مهم‌ترین هدف این اصل ایجاد راحتی در توسعه سیستم بدون تغییر در بخش‌های موجود است. برای رسیدن به این هدف باید سیستم را به کامپوننت‌هایی تجزیه کنیم و این کامپوننت‌ها را در سطوح مختلفی طوری طبقه بندی کنیم که سطوح بالاتر از تغییرات سطوح پایین‌تر در امان بمانند.