علیرضا ارومند
علیرضا ارومند
خواندن ۸ دقیقه·۳ سال پیش

فصل دوازدهم Clean Architecture - کامپوننت‌ها

پیش‌مقدمه بخش چهارم کتاب - Component Principal:

اگر معماری نرم‌افزار را بخواهیم با فرایند معماری و ساخت ساختمان مقایسه کنیم، اصول SOLID به ما می‌گویند که چطور آجرها را در دیوار‌ها مورد استفاده قرار دهیم. در ادامه Component Principalها به ما می‌گویند چگونه اتاق‌ها و اجزای بزرگ‌تر را در کنار هم استفاده کنیم. یک نرم‌افزار بزرگ هم مانند یک ساختمان بزرگ از اجزایی تشکیل شده است.

در بخش چهارم از این کتاب توضیح می‌دهیم که component چیست و از چه بخش‌هایی باید تشکیل شوند و چگونه در کنار هم قرار بگیرند تا سیستم را ایجاد کنند.

شروع فصل دوازدهم:

1. مقدمه:

یک واحد قابل توزیع را Component می‌نامیم. یک component بخش کوچکی از سیستم است که می‌تواند جداگانه انتشار یابد. در Dotnet این اجز در قالب DLL انتشار پیدا می‌کنند. در جاوا در قالب Jar فایل و در زبان‌هایی مانند جاوا اسکریپت مجموعه ای از فایل‌ها که کار خاصی را انجام می‌دهند. فصل مشترک در تمامی زبان‌ها این است که مجموعه ای از کارهای مرتبط با هم که در قالب یک بسته قابل انتشار است برای ما یک component ایجاد می‌کند.

این Componentها می‌توانند در کنار هم قرار بگیرند تا یک برنامه اجرایی را تشکیل دهند یا می‌توانند به تنهایی برای استفاده سایرین منتشر شوند. جدای از اینکه در نهایت چگونه از این Componentها استفاده می‌شود، همیشه یک Component خوب این ویژگی را دارد که مستقلا توسعه و انتشار داده شود.

2. تاریخچه Componentها:

در سال‌های اولیه توسعه نرم‌افزار، برنامه‌نویس‌ها مجبور بودند حافظه و ساختار برنامه را کنترل کنند. یکی از اولین خط‌ کدها در برنامه ، عبارت Origin بود که نشانی محل برنامه را در آن بارگذاری می شد.

تکه کد زیر را در نظر بگیرد که در آن یک روال به نام GETSTRتعریف شده است که متن ورودی از کیبورد را دریافت کرده و در یک بافر ذخیره می‌کند. در ادمه یک تکه کد کوچک هم برای تست این روال وجود دارد.

دستور *200 را در ابتدای برنامه را در نظر بگیرید. این خط به کامپایلر اعلام می‌کند که کدی تولید کند که در آدرس 200 بارگذاری می‌شود.

این نوع برنامه نویسی برای اکثر توسعه دهنده‌های این روزها عجیب خواهد بود. در اغلب موارد، این روزها برای توسعه دهنده‌ها اهمیتی ندارد که برنامه آن‌ها در کدام خانه از حافظه بارگذاری می‌شود. اما در زمان‌های قدیم یکی از تصمیاتی که هر توسعه دهنده‌ای باید می‌گرفت این بود که برنامه اون در کدام خانه از حافظه بارگذاری شود. در آن زمان برنامه‌ها قابل جابجایی نبودند.

در آن زمان چطور می‌شد به کتابخانه‌ای از توابع دسترسی پیدا کرد؟ در ان زمان لازم بود که سورس کد کتابخانه‌ها به سورس کد برنامه متصل شود و در واحد یک برنامه کامپایل شوند. در ان زمان کتابخانه‌ها در قالب فایل‌های سورس کد منتشر می‌شدند نه فایل‌های باینری.

یکی از مشکلاتی که در آن زمان‌ها وجود داشت این بود که دستگاه‌ها کند بودند و حافظه بسیار گران بود و در نتیجه محدودیت‌های زیادی در این حوزه وجود داشت. کامپایلر‌ها نیاز داشتند که چندین بار بررسی کنند اما حافظه اصلی برای نگهداری کل سورس کد بسیار محدود بود. در نتیجه کامپایلر دائم مجبور بود سورس کد را از دستگاه‌های کند بازخوانی نماید.

این فرایند تکراری و خسته کننده بسیار کند بود. در نتیجه کامپایل کردن سورس کد بسیار کند انجام می‌پذیرفت. اگر نیاز به کامپایل برنامه‌ی بزرگی داشتیم بعضا تا ساعت‌ها این کار به طول می‌انجامید.

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

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

با یک نگاه اجمالی می‌توان دریافت که این روند صحیح نمی‌باشد. با بزرگ شدن کتابخانه محدوده تعیین شده برای آن کوچک خواهد شد. چاره این نیست جز اینکه فضای بیشتری برای کتابخانه در نظر گرفته شود. این فرایند تکه تکه شدن برنامه‌ها و رشد آن‌ها در حافظه ادامه پیدا می‌کند و مسلم است که باید فکری برای این وضعیت داشته باشیم.

3. قابلیت جابجایی:

راهکار حل این مشکل ایجاد قابلیت جابجایی برای کد‌ها بود. ایده پشت این موضوع بسیار ساده بود. کامپایلرها تغییر پیدا کردند تا کد‌های برنامه را طوری به باینری تبدیل کنند که قابلیت جابجایی داشته باشند و با یک فرایند هوشمند در حافظه بارگذاری شوند. در این شرایط Loaderها تعیین می‌کنند که کد قابل جابجایی کجای حافظه بارگذاری شود. کد قابل جابجایی دارای علامت‌هایی بود که به Loader می گفت که کدام قسمت از داده های بارگذاری شده باید تغییر کند تا در آدرس انتخابی بارگذاری شود. برای انجام این فرایند، معمولا فقط کافی است تا آدرس شروع را به همه آدرس‌های موجود در کدهای باینری شده اضافه شود.

حال برنامه نویس می تواند به Loader بگوید که کتابخانه را در کجا بارگذاری کرده و برنامه را در کجا بارگذاری کند. در واقع ، Loader چندین ورودی باینری را می پذیرد و به سادگی آنها را یکی پس از دیگری در حافظه بارگذاری می کند و آنها را همانطور که بارگذاری می کند ، جابجا می کند. این به برنامه نویسان اجازه می دهد فقط توابع مورد نیاز خود را بارگذاری کنند.

کامپایلر‌ها تغییرات دیگری نیز پیدا کردند. آن‌ها به گونه‌ای تغییر یافند که نام توابع را نیز به عنوان متادیتا به در فایل بانتری منتشر کنند. اگر برنامه یک تابع از یک کتابخانه را صدا بزند، کامپایلر نامه آن تابع را به عنوان یک external reference منتشر می کند. اگر کتابخانه‌ای تابعی را برای صدا زده شدن تعریف کند کامپایلر آن را به عنوان یک external definition تعریف می‌کند. حال loader می‌تواند external reference را به external definition متصل کند. اینگونه بود که Linking Loader ایجاد شد.

4. آشنایی با Linkerها:

تولد Linking Loaderها این توانایی را به برنامه‌نویسان داد تا برنامه‌های خود را به قطعات کوچکتری که قابل کامپایل و بارگذاری بودند تقسیم کنند. در ابتدا قطعات کوچکی از برنامه‌ها به قطعات کوچکی از کتابخانه‌ها متصل می‌شدند. اما رفته رفته توسعه دهنده‌ها شجاع‌تر و بلند‌پرواز تر شدند و در اواخر دهه 60 و اوایل دهه 70 میلادی اندازه برنامه‌ها بسیار رشد کرد.

در نهایت Linker Loader ها بسیار کند شدند. کتابخانه‌های توابع معمولا روی دستگاه‌های ذخیره سازی کندی مثل نوار‌ها ذخیره می‌شدند. بعضا این کتاخانه‌ها روی هارد دیسک‌ها نگهداری می‌شدند که آن‌ها نیز به نسبت کند بودند. در این شرایط Linking Loader مجبور بود چندین باینری را بارگذاری کند تا نیازهای external referenceها و external definitionها را برطرف کند. در همین شرایط برنامه‌ها بزرگ و بزرگتر شدند تا جایی که بعضا چندین ساعت زمان برای بارگذاری یک برنامه لازم بود.

در نهایت فرایند Load از Linkجدا شد. برنامه نویس‌ها فرایند کند‌تر را که همان Linker بود جدا کرده و در قالب برنامه‌ای به عنوان Linker قرار دادند. خروجی این Linker چیزی بود که به راحتی توسط Loaderبارگذاری می‌شد. این جدا سازی شرایطی را ایجاد کرد که برنامه‌نویسان توانستند برنامه‌های خود را یک بار با فرایندی کند به هم Link کنند و از آن به بعد هر زمانی که نیاز بود آن‌ها را به سرعت باگذاری و اجرا کنند.

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

سورس کدهای C از فایلهای با پسوند .c کامپایل می‌شدند و در قالب فایل های .oقرار می‌گرفتند. سپس Linker آن‌ها را ترکیب می‌کرد و فایل‌هایی را ایجاد می‌کرد که به سرعت قابلیت اجرا داشتند. در آن زمان کامپایل کردن یک ماژول خاص خیلی سریع انجام می‌گرفت اما کامپایل کامل برنامه‌ها بعضا طولانی می‌شد. بعد از این مرحله هم نوبت اتصال به وسیله Linker بود. باز هم سناریوی قبلی و زمان زیاد تبدیل به مشکلی اساسی شد.

به نظر می‌رسید که برنامه نویس‌ها در یک حلقه بی‌نهایت از رفع مشکل و ایجاد مجدد مشکل قرار گرفته اند. از دهه 60 میلادی تا دهه 80 میلادی دائما برنامه‌نویسان با مشکل سرعت انجام کار سر و کله میز‌دند. در یک زمان مشکل سرعت بارگذاری بود و با جدا سازی فرایند اتصال از بارگذاری این مشکل موقت حل شد. اما با افزایش اندازه برنامه‌ها این بار سرعت در فرایند اتصال و کامپایل تبدیل به گلوگاه اصلی فرایند شد. این مشکل با قوانین مورفی همخوانی دارد.

برنامه‌ها اینقدر بزرگ می‌شوند تا تمام زمان کامپایل و اتصال را استفاده کنند.

اما مورفی تنها سخنگوی شهر نیست. همزمان با اون مور نیز قوانینی را بیان کرد. در نهایت برنده این قانون گذاری‌ها مور بود. دیسک‌ها روز به روز کوچکتر و سریع‌تر شدند. حافظه‌های اصلی نیز بسیار پرظرفیت ‌تر و ارزان تر شد و این امکان وجود داشت که بسیاری از اطلاعات روی حافظه اصلی نگهداری شود. سرعت ساعت کامپیوتر نیز از 1 مگاهرتز به 100 مگاهرتز افزایش پیدا کرد.

در اواسط دهه 90 این رشد‌های ایجاد شده در سخت‌افزار به قدری بود که فرایند اتصال بسیار سریع شد. این فرایند اینقدر سریع شد که دیگر جاه طلبی برنامه‌نویسان برای بزرگ کردن اندازه برنامه‌ها مشکلی در فرایند اتصال و کاهش سرعت آن ایجاد نمی‌کرد. در بسیاری از شرایط ایده اولیه Linking Loaderمجدد منتطقی به نظر می‌رسید.

ایجاد نقطه شروع عصر Active-Xها و Jarفایل‌ها و کتابخانه‌های اشتراکی بود. کامپیوتر‌ها و سخت‌افزار اینقدر سریع شدند که حالا باز این امکان ایجاد شد که فرایند اتصال و بارگذاری همزمان انجام شود. چندین کتابخانه می‌توانستند در کسری از ثانیه به هم متصل شوند و یک برنامه اجرایی را ایجاد کنند. ایجاد بود که نسل جدید معماری به کمک componentها متولد شد.

این روز‌ها انتشار Componentها به کمک Jarفایل‌ها و DLLها بسیار فراگیر و روزمره است. این روزها به سادگی می‌توانید برنامه‌ها را که دارید به کمک Componentها گسترش دهید. برای مثلا افزودن امکانات ReSharper به Visual Studio به سادگی اتفاق می‌افتد.

5. جمع بندی:

بسیاری از امکانات و معماری‌ها امروزه به سادگی مورد استفاده قرار می‌گیرند. اما همانطور که مشاهده کردید برای اینکه امکانات اولیه برای فکر کردن به این معماری‌ها در اختیار ما قرار گیرد بعضا تا 50 سال آزمون و خطا و تلاش پیشینیان اتفاق افتاده است.

clean architecturesoftware architecturecomponent
شاید از این پست‌ها خوشتان بیاید