چرا بعضی پروژهها بعد از مدتی تبدیل به کابوس میشن؟
احتمالاً برای خیلی از برنامهنویسها پیش اومده که یک پروژه رو با کلی انرژی شروع کنن. همهچیز مرتب و ساده به نظر میرسه؛ چندتا فایل، چندتا API و یک دیتابیس.
اما چند ماه بعد، داستان کاملاً عوض میشه.
اضافه کردن یک قابلیت ساده ممکنه چند روز زمان ببره، تغییر یک بخش باعث خراب شدن چند بخش دیگه میشه و هر برنامهنویسی که وارد پروژه میشه، چند ساعت فقط دنبال این میگرده که بفهمه «اینجا دقیقاً چه خبره؟»
اینجاست که پای معماری نرمافزار وسط میاد.
معماری نرمافزار اصلاً یعنی چی؟
اگر خیلی ساده بخوایم بگیم، معماری نرمافزار یعنی تصمیم بگیریم یک نرمافزار چطور ساخته بشه و بخشهای مختلفش چطور با هم ارتباط داشته باشن.
مثلاً فرض کن داریم یک فروشگاه اینترنتی میسازیم.
قرار نیست همهچیز رو داخل یک فایل بنویسیم. معمولاً بخشهایی مثل این داریم:
کاربران
محصولات
سفارشها
پرداخت
ارسال
احراز هویت
حالا سؤال اینه:
این بخشها کجا قرار بگیرن؟
چطور با هم ارتباط داشته باشن؟
دادهها چطور جابهجا بشن؟
اگر بعداً سیستم بزرگتر شد، چطور تغییرش بدیم؟
مجموعه جوابهایی که برای این سؤالها میدیم، بخش مهمی از معماری نرمافزار رو تشکیل میده.
مشکل از جایی شروع میشه که پروژه بزرگتر میشه
وقتی پروژه تازه شروع شده، خیلی از تصمیمهای اشتباه خودشون رو نشون نمیدن.
مثلاً ممکنه برای اینکه سریعتر پیش بریم، همهچیز رو قاطی کنیم:
UserController
↓
Database
↓
Payment
↓
Email
در ظاهر شاید مشکلی وجود نداشته باشه.
اما چند ماه بعد که پروژه بزرگتر شد، تغییر دادن هر قسمت روی قسمتهای دیگه هم تأثیر میذاره.
مثلاً میخوایم دیتابیس رو تغییر بدیم.
ناگهان متوجه میشیم دهها قسمت از برنامه مستقیماً به ساختار دیتابیس وابسته هستن.
یا میخوایم سیستم پرداخت رو تغییر بدیم.
میبینیم منطق پرداخت در چند جای مختلف پروژه پخش شده.
اینجاست که پروژه کمکم تبدیل به چیزی میشه که برنامهنویسها بهش میگن:
کد اسپاگتی! 🍝
کد اسپاگتی یعنی چی؟
کد اسپاگتی یعنی بخشهای مختلف برنامه آنقدر به هم وابسته و درهمتنیده شدن که فهمیدن و تغییر دادنشون سخت شده.
مثلاً یک تابع کوچک برای ثبت سفارش داریم که همزمان:
اطلاعات کاربر رو بررسی میکنه
محصول رو از دیتابیس میخونه
موجودی رو کم میکنه
پرداخت رو انجام میده
ایمیل ارسال میکنه
لاگ ثبت میکنه
در نگاه اول شاید بگیم:
«خب، کارش رو انجام میده دیگه!»
مشکل زمانی شروع میشه که بخوایم فقط یکی از این قسمتها رو تغییر بدیم.
مثلاً تصمیم میگیریم سرویس ایمیل رو عوض کنیم.
اگر منطق ارسال ایمیل داخل دهها قسمت پروژه پخش شده باشه، تغییر سادهای مثل این میتونه تبدیل به یک دردسر بزرگ بشه.
معماری خوب قرار نیست پروژه رو پیچیدهتر کنه
یک تصور اشتباه اینه که هرچه معماری پروژه پیچیدهتر باشه، پروژه حرفهایتره.
در حالی که معمولاً هدف معماری خوب برعکسه.
معماری خوب باید پیچیدگی رو مدیریت کنه، نه اینکه خودش پیچیدگی جدید ایجاد کنه.
مثلاً اگر یک پروژه کوچک داریم که فقط یک فرم ساده و چند API داره، احتمالاً نیازی نیست از چندین سرویس جدا، Message Broker و دهها لایه مختلف استفاده کنیم.
گاهی یک ساختار ساده و تمیز بهترین انتخابه.
Monolith همیشه بد نیست
یکی از بحثهای معروف در معماری نرمافزار، مقایسهی Monolith و Microservices هست.
در معماری Monolithic، بخشهای مختلف برنامه معمولاً در قالب یک اپلیکیشن واحد قرار دارن.
در Microservices، سیستم به سرویسهای کوچکتر تقسیم میشه که هرکدوم مسئولیت مشخصی دارن.
خیلیها وقتی اسم Microservices رو میشنون، فکر میکنن:
Microservices یعنی معماری حرفهای و Monolith یعنی معماری قدیمی.
اما واقعیت به این سادگی نیست.
یک Monolith که خوب طراحی شده باشه میتونه سالها بدون مشکل کار کنه.
از طرف دیگه، اگر بدون دلیل پروژه رو به دهها Microservice تقسیم کنیم، مشکلات جدیدی به وجود میاد:
ارتباط بین سرویسها
مدیریت خطا
مانیتورینگ
Deployment
امنیت
هماهنگی دادهها
پیچیدگی بیشتر در توسعه
پس سؤال اصلی این نیست که:
«Monolith بهتره یا Microservices؟»
سؤال بهتر اینه:
«برای پروژه من چه معماریای منطقیتره؟»
وابستگی زیاد، دشمن پروژه است
یکی از مهمترین چیزهایی که در طراحی نرمافزار باید حواسمون بهش باشه، Coupling یا وابستگی بین بخشهاست.
فرض کن بخش سفارش مستقیماً به یک پیادهسازی خاص از سیستم پرداخت وابسته باشه.
اگر بعداً بخوایم سیستم پرداخت رو تغییر بدیم، مجبور میشیم بخش سفارش رو هم تغییر بدیم.
هرچه وابستگیها بیشتر باشن، تغییر دادن پروژه سختتر میشه.
در مقابل، اگر بخشهای مختلف تا حد ممکن مستقل باشن، تغییر دادن یک قسمت تأثیر کمتری روی قسمتهای دیگه خواهد داشت.
یک معماری خوب چه ویژگیهایی داره؟
معماری خوب لزوماً یک نسخه ثابت برای همه پروژهها نیست، اما معمولاً چند ویژگی مهم داره.
۱. قابل فهم باشه
اگر فقط کسی که معماری رو طراحی کرده بتونه پروژه رو بفهمه، احتمالاً یک جای کار مشکل داره.
۲. تغییر دادن بخشها راحت باشه
تغییر یک قابلیت نباید باعث بشه نصف پروژه رو دوباره بررسی کنیم.
۳. مسئولیتها مشخص باشن
هر بخش بهتره وظیفه مشخصی داشته باشه.
مثلاً منطق پرداخت نباید با منطق ارسال ایمیل قاطی بشه.
۴. وابستگیها کنترل شده باشن
بخشهای مختلف نباید بدون دلیل به جزئیات داخلی یکدیگر وابسته باشن.
۵. متناسب با اندازه پروژه باشه
یک پروژه کوچک نباید به اندازه یک سیستم بانکی پیچیده طراحی بشه.
Clean Architecture چیه؟
یکی از رویکردهایی که برای مدیریت این پیچیدگی استفاده میشه، Clean Architecture هست.
ایده اصلی اینه که قسمتهای مهم و اصلی برنامه تا حد ممکن به جزئیات بیرونی وابسته نباشن.
برای مثال، منطق اصلی سیستم نباید وابستگی شدیدی به یک دیتابیس یا یک فریمورک خاص داشته باشه.
در چنین ساختاری میتونیم به شکل ساده این ذهنیت رو داشته باشیم:
UI / API
↓
Application Logic
↓
Business Rules
↓
Infrastructure / DB
البته پیادهسازی واقعی Clean Architecture میتونه پیچیدهتر از این مثال باشه، اما ایده اصلی همین جداسازی مسئولیتها و کنترل وابستگیهاست.
آیا همیشه باید Clean Architecture استفاده کنیم؟
نه.
این یکی از مهمترین نکاتیه که باید یادمون بمونه.
معماری نرمافزار مسابقه استفاده از بیشترین تعداد Pattern و Layer نیست.
اگر برای یک پروژه کوچک پنج لایه درست کنیم، ده Interface بنویسیم و همهچیز رو بیش از حد انتزاعی کنیم، ممکنه به جای حل مشکل، مشکل جدیدی ایجاد کنیم.
گاهی بهترین معماری، سادهترین معماریایه که نیازهای فعلی پروژه رو بهخوبی جواب میده.
معماری خوب از کجا شروع میشه؟
قبل از اینکه سریع سراغ انتخاب Framework یا Architecture Pattern بریم، بهتره چند سؤال ساده از خودمون بپرسیم:
پروژه قراره چه کاری انجام بده؟
چند کاربر قراره داشته باشه؟
کدوم قسمتها احتمالاً بیشتر تغییر میکنن؟
چه بخشهایی از سیستم به هم وابسته هستن؟
تیم توسعه چقدر بزرگه؟
پروژه قراره چقدر رشد کنه؟
جواب این سؤالها کمک میکنه معماری مناسبتری انتخاب کنیم.
یک اشتباه رایج: معماری برای آیندهای که هنوز وجود نداره!
گاهی برنامهنویسها از همون روز اول برای پروژهای که شاید یک سال دیگه بزرگ بشه، معماری بسیار پیچیده طراحی میکنن.
مثلاً پروژه هنوز ۱۰۰ کاربر نداره، ولی از همین الان چندین سرویس، چند دیتابیس و سیستمهای پیچیده وارد پروژه میکنیم چون:
«شاید یه روز میلیونها کاربر داشته باشیم!»
این رویکرد همیشه منطقی نیست.
بهتره معماری قابلیت رشد داشته باشه، اما این به معنی ساختن تمام زیرساختهای آینده از روز اول نیست.
حرف آخر
معماری نرمافزار در نهایت درباره انتخاب چند اسم عجیب مثل Microservices، Clean Architecture یا Hexagonal Architecture نیست.
اصل ماجرا اینه که نرمافزارمون رو طوری طراحی کنیم که:
فهمیدنش راحت باشه، تغییر دادنش دردسر نباشه و رشد کردنش باعث فروپاشی پروژه نشه.
هرچه پروژه بزرگتر میشه، اهمیت این موضوع بیشتر خودش رو نشون میده.
پس دفعه بعد که پروژه جدیدی رو شروع کردی، قبل از اینکه سریع کدنویسی رو شروع کنی، چند دقیقه به ساختار پروژه فکر کن.
قرار نیست از همون اول کاملترین معماری دنیا رو بسازی.
فقط سعی کن کاری نکنی که نسخه چند ماه بعد خودت، از نسخه امروزت متنفر بشه! 😄
