سارا رضائی
سارا رضائی
خواندن ۱۴ دقیقه·۴ سال پیش

برنامه نویسی ماژولار

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

تاریخچه ی برنامه نویسی ماژولار، به دهه ی 1960 میلادی بر می گردد، زمانی که برنامه نویس ها، نرم افزارهای بزرگ را به بخش های کوچکتر شکستند. هرچند بیشتر از 6 دهه از عمر این رویکردِ برنامه نویسی می گذرد، اما امروزه هم برای توسعه ی نرم افزار بسیار مفید است و احتمالا اکثر برنامه نویس های امروزی، بدون این که بدانند، از برنامه نویسی ماژولار بهره می گیرند. ساده ترین مثالِ آن، زمانی است که (در زبان های شی گرا) یک کلاس، یا یک متد تعریف می کنند.

در یک دنیای ایده آل، هر ماژول کاملا از دیگر ماژول ها مستقل است. برنامه نویس ها می توانند بدون هیچ دانشی از ماژول های دیگر، روی یک ماژول خاص کار کنند. در چنین دنیایی، پیچیدگی سیستم، به اندازه ی پیچیدگیِ پیچیده ترین ماژول است.

ولی ما در دنیای واقعی زندگی می کنیم. دنیایی که هر ماژول نیاز دارد با دیگران کار کند. در نتیجه باید از ماژول های دیگر باخبر باشد و آن ها را بشناسد. این یعنی وابستگی بین ماژول ها وجود دارد و وابستگی باعث می شود تغییر در یک ماژول، روی سایرین تاثیر بگذارد. برای مثال، یک متد را در نظر بگیرید که 2 پارامتر دارد. اگر یکی از پارامترهای این متد حذف شود، کسانی که از آن استفاده می کنند باید تغییر کنند و خود را با signature جدید انطباق دهند. در طراحی ماژولار، چشم انداز ما باید به حداقل رساندنِ وابستگی بین ماژول ها باشد و تا حد ممکن در این زمینه تلاش کنیم.

برای مدیریت وابستگی ها، هر ماژول باید شامل دو عنصر مهم باشد:

  • واسط (interface)

در یک تعریف کلی می توان گفت، واسط، مشخص کننده ی نحوه ی استفاده از یک ماژول است. وسیله ای است که یک ماژول را به دیگری متصل می کند و مشخص می کند که دیگران، از یک ماژول، چه انتظاری باید داشته باشند. در واقع، شامل تمام چیزهایی است که یک برنامه نویس، که روی یک ماژول دیگر کار می کند، برای استفاده از ماژول فعلی باید بداند.

برای درک بهتر مطالب این نوشته، هر آنچه که از کلمات واسط (interface) و انتزاع (abstraction) در ذهن دارید را به صورت موقت، فراموش کنید و سعی کنید با مفاهیم این عبارات، در این نوشته، همراه شوید.
  • پیاده سازی (implementation)

هر ماژول، باید شامل کدی باشد که قول و قراری که توسط واسط هایش به دیگر ماژول ها داده شده را پیاده سازی کند. می توان گفت، هرچیزی غیر از واسط ماژول، پیاده سازی آن است.

یک برنامه نویس که روی یک ماژول کار می کند، باید تمام واسط ها و پیاده سازی های آن ماژول، به علاوه ی واسط های سایر ماژول های وابسته را بشناسد. ولی نیازی به شناختن پیاده سازی ماژول های دیگر ندارد.

با این دیدگاه، هر قطعه ای از کد، که یک واسط و یک پیاده سازی دارد یک ماژول محسوب می شود. در یک زبان برنامه نویسی شی گرا، کلاس ها و method های آن ها، و در یک زبان برنامه نویسی غیر شی گرا، function ها را می توان ماژول در نظر گرفت. در سطوح بالاتر، service ها و subsystem ها هم ماژول هستند.

اگر با زبان های برنامه نویسی شی گرا مثل #C کار کرده اید، یک نکته ی مهم که باید به آن توجه کنید این است که interface در طراحی ماژولار، مفهوم وسیع تری از کلمه ی interface در زبان های برنامه نویسی مثل #C دارد.
وقتی به برنامه نویسی ماژولار فکر می کنیم، باید فراتر از شی گرای بودن یا نبودن به آن نگاه کنیم. در یک زبان شی گرا، ممکن است جداسازی ماژول ها را به وسیله ی کلاس ها انجام دهیم، در یک زبان دیگر ممکن است این کار را از طریق function ها انجام دهیم.

بهترین ماژول ها، آن هایی هستند که واسط شان، بسیار ساده تر از پیاده سازی آن هاست. ساده بودنِ واسط از دو جهت مفید است:

1. پیچیدگی ای که ماژول به دنیای بیرون تحمیل می کند، به حداقل می رسد

2. تغییرات و اصلاحاتِ آینده، بیشتر در پیاده سازیِ ماژول تاثیر خواهد داشت و واسط از این تغییرات متاثر نخواهد شد، در نتیجه سایر ماژول ها تغییر نخواهند کرد.

از جنبه های مهم طراحی ماژولار، می توان به Separation of concerns یا SoC اشاره کرد که یکی از اصول پایه ای طراحی نرم افزار است. بر اساس این اصل، سیستم نرم افزاری، باید به بخش های مستقلی تقسیم شود، که هر بخش concern جداگانه ای را پاسخ می دهد.
این اصل را می توانید به شکل های مختلف در سیستم های نرم افزاری ببینید:
1. کپسوله سازی اطلاعات در objectها، در زبان های شی گرا.
2. در طراحی لایه ای، که در آن سیستم دارای لایه های مختلف مثل Business layer یا DataAccess layer است.
3. در MVC که concern های UI از ساختار اصلی نرم افزار جدا می شوند.
4. در طراحی سرویس گرا، سرویس ها، این جداسازی را انجام می دهند.
5. در زبان هایی مانند C یا Pascal، به وسیله ی function ها concern ها را جدا می کنیم.
6. در AOP این جداسازی توسط aspect ها انجام می شود.
7. در بستر شبکه، لایه های مختلف وجود دارد که هر کدام وظایف و concern های خود را دارند.
شاید بتوان گفت، هر جا مرزبندی دیدیم، احتمالا بر اساس اصل SoC رفتار شده است. منظور از مرز، هر محدودیت منطقی یا فیزیکی است که مجموعه ی مشخصی از مسئولیت ها را ترسیم می کند.

دو نوع واسط را می توان برای یک ماژول تعریف کرد:

  • رسمی

واسطِ رسمی یک ماژول، کاملا در ظاهر آن مشخص است. برای مثال، ورودی ها و نوع خروجیِ یک متد، واسط رسمی آن هستند. برای یک کلاس، همه ی اعضای public آن (شامل method ها و property ها) واسطِ رسمی آن هستند. معمولا واسط رسمی، توسط زبان برنامه نویسی قابل شناسایی و بررسی است، برای مثال در زبان #C اگر ورودی یک method شامل 4 پارامتر باشد و یک ماژول آن را با 3 پارامتر صدا بزند، خطای compile خواهد داشت.

  • غیر رسمی

این نوع واسط، توسط زبان برنامه نویسی قابل تشخیص نیست، زیرا در رفتار آن ماژول پنهان شده است. برای مثال فرض کنید یک method، در ورودی خود 3 پارامتر می گیرد و پارامتر دوم نام فایلی است که قرار است حذف شود و این از signature متد قابل تشخیص نیست. یا این که در یک کلاس، قبل از این که یک method صدا زده شود، باید یک property مقدار دهی شود. در واقع اگر یک برنامه نویس که روی یک ماژول دیگر کار می کند، برای استفاده از یک ماژول، نیاز به دانستن یک سری اطلاعات داشته باشد، آن اطلاعات، واسط غیر رسمی آن ماژول است.

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

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


انتزاع (Abstractions)

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

انتزاع، مفهومی نیست که فقط مربوط به نرم افزار باشد. در هنر، زمانی که هنرمند، یک تصویر از آنچه در ذهن دارد خلق می کند، بدون این که جزئیات آن سوژه در دنیای واقعی را ترسیم کند، انتزاع وجود دارد. هنر انتزاعی هنری است که سعی در نمایش دقیق واقعیت بصری ندارد اما در عوض از اشکال ، رنگ ها ، فرم ها و علائم حرکتی برای دستیابی به تأثیر آن استفاده می کند.
در واقع فرم ها، در هنر انتزاعی، بسیار ساده شده اند و گرچه ممکن است یک هنرمند هنگام نقاشی، یک شی واقعی در ذهن داشته باشد، اما ممکن است آن شی با استفاده از رنگها و بافت ها برای ایجاد یک احساس، تلطیف ، تحریف یا اغراق شود، به نوعی که به سختی بتوان با نگاه اولیه به اثر، object دنیای واقعی را شناسایی کرد.
انتزاع، در ریاضی هم کاربرد دارد. بسیاری از زمینه های ریاضیات، قبل از اینکه قوانین و مفاهیم اساسی شناسایی، و بعنوان ساختارهای انتزاعی شناخته شوند، با مطالعه ی مسائل دنیای واقعی آغاز شدند. به عنوان مثال ، هندسه، ریشه در محاسبه فواصل و مساحت ها در دنیای واقعی دارد و جبر با روش های حل مسائل در حساب آغاز شده است.
ولی هنگامی که کسی از شما یک مساله ی ریاضی می پرسد، برای حل آن نیازی به داشتن اشیاء واقعی ندارید، تنها با استفاده از انتزاعات، یعنی صورتی از واقعیت که جزئیات آن حذف شده است، می توانید مساله را حل کنید.

ایده ی اصلی انتزاع این است که:

1. اشیاء و موقعیت های مشابه را شناسایی کنیم

2. تفاوت های آن ها را نادیده بگیریم

3. روی یک جنبه ی خاص و مشترک (که مربوط به مساله ای است که در حال حل آن هستیم و تمام آن اشیاء یا موقعیت ها آن را منعکس می کنند) تمرکز کنیم.

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

اما ارتباط انتزاع با برنامه نویسی ماژولار چیست؟

هر ماژول از طریق واسط هایش، یک انتزاع فراهم می کند. واسط، یک چشم اندازِ ساده شده از عملکرد ماژول ارائه می دهد. از نظرِ انتزاع، جزئیات پیاده سازی "بی اهمیت" هستند، بنابراین از واسط حذف می شوند.

وقتی از انتزاع صحبت می کنیم، کلمه ی "بی اهمیت" حیاتی است. هرچقدر مسائل بی اهمیت بیشتری از یک انتزاع حذف شوند، بهتر است و نکته ی مهم این است که فقط موارد بی اهمیت از انتزاع حذف شوند. بی دقتی در این زمینه، می تواند دو مشکل ایجاد کند:

  1. ممکن است جزئیاتی که واقعا مهم نیستند، در انتزاع وجود داشته باشند. این مساله باعث می شود برنامه نویس هایی که می خواهند از این انتزاع استفاده کنند، دچار بارِ شناختیِ زیاد شوند.
  2. امکان دارد جزئیاتی که واقعا مهم هستند، از انتزاع حذف شوند. این مساله باعثِ ابهام می شود. برنامه نویس ها ممکن است تمام اطلاعات لازم برای استفاده از این انتزاع را نداشته باشند. به این نوع انتزاع "false abstraction" گفته می شود.

نکته ی کلیدی در طراحی انتزاع، این است که بفهمیم چه چیزی واقعا مهم است، و به دنبال طراحی ای باشیم، که میزان اطلاعاتی که مهم هستند را به حداقل می رساند.

چه زمانی می فهمیم به یک انتزاع خوب از یک ماژول دست یافته ایم؟زمانی که استفاده کنندگان آن ماژول، نیاز به دانستن هیچ چیزی بیشتر از آنچه که در واسط اش فراهم شده نداشته باشند.


عمق ماژول

بهترین ماژول ها آن هایی هستند که واسط های ساده ای دارند و عملکرد قدرتمندی را فراهم می کنند.
تصویر زیر را در نظر بگیرید:

ماژول های عمیق بهتر هستند. آن ها دسترسی به عملکرد پیچیده ی خود را از طریق یک واسط ساده امکان پذیر می سازند. در مقابل، ماژول های کم عمق یک واسط خیلی پیچیده دارند که عملکرد بسیار ساده ای را پنهان کرده است.
ماژول های عمیق بهتر هستند. آن ها دسترسی به عملکرد پیچیده ی خود را از طریق یک واسط ساده امکان پذیر می سازند. در مقابل، ماژول های کم عمق یک واسط خیلی پیچیده دارند که عملکرد بسیار ساده ای را پنهان کرده است.


در این تصویر، هر ماژول، به شکل یک مستطیل نمایش داده شده است. مساحت هر مستطیل، نشان دهنده ی پیاده سازیِ آن است. ضلع بالایی، نشان دهنده ی واسط ماژول است و اندازه ی آن، میزان پیچیدگی واسط را نشان می دهد. بهترین ماژول ها، ماژول های عمیق هستند که عملکرد زیادی را پشت یک واسط ساده پنهان کرده اند. یک ماژول عمیق، یک انتزاع خوب است چرا که مقدار بسیار کمی از پیچیدگی درونی خودش را برای دنیای بیرون، آشکار می کند.
مفهوم "عمق ماژول"، راهی برای فکر کردن به سود و هزینه است. سود ارائه شده توسط ماژول، عملکرد آن است و هزینه ی آن (از نظر پیچیدگی سیستم)، واسط های آن هستند. واسطِ یک ماژول، نشان دهنده ی پیچیدگی ای است که آن ماژول به سایر بخش های سیستم تحمیل می کند، بنابراین هرچقدر واسط، ساده تر و کوچک تر باشد، پیچیدگیِ تحمیل شده کمتر است. بهترین ماژول ها آن هایی هستند که بیشترین سود و کمترین هزینه را دارند.

به عنوان مثالی از ماژول عمیق، می توانیم به garbage collector زبان Java اشاره کنیم. این ماژول، هیچ واسطی ندارد و به صورت نامرئی، در پشت صحنه کار می کند. با این که پیچیدگیِ پیاده سازی آن بسیار زیاد است، ولی اضافه کردن آن به یک سیستم، هیچ پیچیدگی (از نظر واسط ها) به سیستم اضافه نمی کند. در واقع پیچیدگیِ آن، به صورت کامل از دید برنامه نویس هایی که از این زبان استفاده می کنند پنهان شده است.

یک مثال از ماژول کم عمق، این method است:

private void AddNullValueForAttribute(String attribute) { data.put(attribute, null); }

از نظر مدیریت پیچیدگی، این ماژول نه تنها کمکی نمی کند، بلکه اوضاع را بدتر می کند. این متد هیچ انتزاعی ندارد و استفاده کننده ها باید بدانند که ورودی متد، در متغیر data ذخیره می شود. در واقع، برای یک برنامه نویس، شناختن واسط این ماژول، اصلا ساده تر از دانستن پیاده سازی آن نیست (در حالی که این دقیقا کاری است که واسط باید انجام دهد و شناخت ماژول را برای دنیای بیرون ساده تر کند). اگر برای این متد، مستند تهیه شود، احتمالا مستندات آن، طولانی تر از پیاده سازی آن خواهند بود! در واقع اگر این متد نبود، زحمت کمتری برای استفاده کنندگان وجود داشت و آن ها می توانستند خودشان به راحتی متغیر data را مقداردهی کنند. این ماژول، نه تنها هیچ سودی ندارد، بلکه یک هزینه هم اضافه می کند و آن زحمت یاد گرفتن یک واسط جدید توسط برنامه نویس ها است.

ماژول کم عمق، ماژولی است که واسط آن، به نسبت پیاده سازی اش، پیچیدگی بیشتری دارد. سودی که این ماژول ها ارائه می دهند، (این که کسی مجبور نیست بداند در داخل آن ها چه کاری انجام می شود) کمتر از هزینه ی یادگیری و استفاده از واسط های آن هاست.


کلاس های کم عمق

یکی از اشتباهات رایج میان برنامه نویس ها، این است که، سعی می کنند کلاس ها را "کوچک" در نظر بگیرند، نه عمیق. هنر را در این می دانند که کلاس های بزرگ را به چندین کلاس کوچک بشکنند. درباره ی متدها هم همینطور است، هر متدی که بیشتر از n خط کد دارد را به متدهای کوچکتر تقسیم می کنند. این رویکرد، به تعداد زیادی کلاس و method منتهی می شود، که همه به هم وابسته اند و به پیچیدگی سیستم می افزایند.

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

برای مثال کد زیر را در نظر بگیرید:

FileInputStream fileStream = new FileInputStream(fileName); BufferedInputStream bufferedStream = new BufferedInputStream(fileStream); ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);

کلاس FileInputStream فقط عملیات مربوط به IO را فراهم می کند و امکان buffering ندارد. اگر بخواهیم از امکان buffering بهره مند شویم، باید از کلاس BufferedInputStream استفاده کنیم. همچنین اگر بخواهیم با اشیاء serialize شده کار کنیم، ناچار به استفاده از کلاس ObjectInputStream هستیم. اگر یک برنامه نویس فراموش کند که یک شی از کلاس BufferedInputStream بسازد، buffering نخواهد داشت و عملیات کند خواهد بود.

شاید در دفاع از این مدل برنامه نویسی، بتوان این مساله را مطرح کرد که احتمالا، همه نیاز به استفاده از buffering نخواهد داشت، بنابراین آن را جدا کنیم تا استفاده کنندگان در مورد بودن یا نبودنش تصمیم بگیرند. قطعا این جداسازی خوب است، ولی دقت کنیم که واسط ها، باید طوری طراحی شوند، که موارد معمول را تا حد ممکن ساده کنند. یعنی در این مورد ما باید فکر کنیم، احتمالا اکثر کسانی که از این کلاس استفاده می کنند، به buffering نیاز خواهند داشت، پس برای آن عده ی کمی که نیاز ندارند، باید یک مکانیزم برای غیر فعال کردنش فراهم کنیم. مثلا شاید از طریق یک constructor مجزا یا یک متد که این ویژگی را غیرفعال می کند. مهم این است که نیاز اکثریت استفاده کنندگان را شناسایی کنیم و برای آن ها بهترین واسط را طراحی کنیم.

خلاصه

برای مدیریت پیچیدگی، باید نرم افزار را به بخش های مستقل تقسیم کنیم. این تقسیم بندی می تواند از جنبه های گوناگون انجام شود. به این بخش ها، که ممکن است کلاس، method یا یک سیستم باشد، ماژول گفته می شود. با این تقسیم بندی، به اصل SoC وفادار مانده ایم.

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

یک موضوع مهم در طراحی کلاس ها و سایر ماژول ها، این است که تا جای ممکن عمق آن ها را بیشتر کنیم، تا برای اکثریتِ مواردِ استفاده، واسط ساده ای را، برای یک پیاده سازی پیچیده، فراهم کنند.


منبع

A philosophy of software design


برنامه نویسیطراحی ماژولاربرنامه نویسی ماژولارانتزاع
linkedin.com/in/sara-rez
شاید از این پست‌ها خوشتان بیاید