علیرضا ارومند
علیرضا ارومند
خواندن ۱۲ دقیقه·۵ سال پیش

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

1. مقدمه:

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

2. مربع اعداد:

برای اینکه بهتر با برنامه نویسی تابعی آشنا شویم، کار را با یک مثال ادامه می‌دهیم. فرض کنید می‌خواهیم مربع اعداد 0 تا 24 را چاپ کنیم. این برنامه در زبان جاوا به شکل زیر نوشته می‌شود.

نمایش مربع اعداد 0 تا 24
نمایش مربع اعداد 0 تا 24

در زبان برنامه‌نویسی مثل Clojure که زبانی تابعی و مشتق شده از Lisp است، برنامه بالا به صورت زیر پیاده سازی می‌شود.

نمایش مربع اعداد 0 تا 24 با زبان تابعی
نمایش مربع اعداد 0 تا 24 با زبان تابعی

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

برنامه با زبان تابعی در چند خط
برنامه با زبان تابعی در چند خط

احتمالا متوجه شده اید که println, take, map و range همگی توابع داخل برنامه هستند. در زبان Lisp شما برای اجرای یک تابع آن‌را داخل یک پرانتز می‌نویسید. برای مثال (range) تابع range را صدا می‌زند.

عبارت (fn [x] (* x x)) یک تابع ناشناس است که تابع ضرب را صدا می‌زند و پارامتر‌های ورودی آن را تکراری ارسال می‌کند. در حقیقت از ضرب برای محاسبه مربع عدد ورودی خود استفاده می‌کند. بیایید یک بار دیگر نگاهی به تابع خودمان بیاندازیم. این کار را با داخلی ترین تابع شروع می‌کنیم.

  • تابع range لیستی بی‌نهایتی از اعداد را که از صفر شروع می‌شوند بازگشت می‌دهد.
  • این لیست به تابع map ارسال می‌شود، که تابع ناشناس محاسبه مربع را صدا برای هر عنصر لیست صدا می‌زند. به این شکل لیست بی‌نهایت جدیدی از مربع اعداد را به دست می‌آوریم.
  • این لیست جدید به تابع take ارسال می‌شود که تنها 25 عنصر ابتدایی لیست مربع اعداد را بازگشت می‌دهد.
  • در نهایت تابع println مقدار دریافتی در ورودی را که همان لیست 25 عنصری از مربع اعداد است را چاپ می‌کند.

اگر از شنیدن نام لیست بی‌نهایتی از اعداد وحشت‌زده شده‌اید باید بگویم که نگران نباشید. تنها 25 عنصر ابتدایی آن ایجاد می‌شود. در واقع اعضای لیست‌های بی‌نهایت تا زمانی که مورد نیاز نباشند ایجاد نمی‌شوند.

اگر هنوز هم کل این سورس‌کد برای شما غیرعادی و غیرقابل درک به نظر می‌رسد، با توجه به اینکه در این مطلب قصد آموزش Clojure را نداریم پیشنهاد می‌کنم به منابع آموزشی این زبان مراجعه کنید.

هدف من از این مثال نمایش تفاوت فاحش بین Clojure و جاوا بود. برنامه جاوا از متغیر‌های اصطلاحا mutable برای انجام عملیات استفاده می‌کند. مقدار این متغیر‌ها در طول زمان امکان تغییر دارد. به متغیر i که برای کنتل اجرای حلقه استفاده می‌شود نگاه کنید. با هر‌بار اجرای حلقه مقدار آن تغییر می‌کند. همچین متغیر‌هایی در Clojure وجود ندارد. در Clojure متغیر‌ها مقدار دهی اولیه می‌شوند و دیگر هرگز مقدار آن‌ها تغییر نمی‌کند.

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

3. تغییر ناپذیری و معماری:

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

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

به عنوان یک معمار احتمالا موضوع همزمانی باید برای شما جذاب باشد. شما باید مطمئن باشید که برنامه‌ای که معمار آن بوده‌اید در شرایط چند‌پردازنده‌ای و چند نخی با قدرت به کار خود ادامه می‌دهد. احتمالا از خودتان می‌‌پرسید تغییر‌ناپذیری قابل پیاده‌سازی است؟

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

بیایید با هم نگاهی به این توافقات و پیش‌شرط‌ها داشته باشیم.

4. تفکیک تغییر پذیری:

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

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

در حافظه‌های Transactional با متغیر‌های موجود در حافظه همانطور رفتار می‌شود که دیتابیس‌ها با رکورد‌های موجود در پایگاه داده رفتار میکند و به کمک Transactionها متغیر‌ها را از تغییرات ناصحیح محافظت می‌کند.

برای مثالی از این کار می‌توان به قابلیت atom در Clojure اشاره کرد.

در این مثال متغیر counter از نوع atom تعریف شده است. در Clojure انواع atom متغیر‌های خاصی هستند که در شرایط بسیار ویژه که توسط swap! به سیستم تحمیل می‌شود، اجازه تغییر دارند.

تابع swap! که در مثال قبل مشاهده نمودید دو ورودی دریافت می‌کند. متغیر atom که باید مقدار ان تغییر کند و تابعی که مقدار جدید را برای atom محاسبه می‌کند. در مقال ما counter atom مقدار با نتیجه inc به روز رسانی می‌شود که تابع ساده‌ای است که تنها مقدار ورودی را افزایش می‌دهد.

استراتژی تابع swap برای انجام این‌کار استفاده از الگوریتم مقایسه و جابجایی است. ابتدا مقدار جاری counter خوانده می‌شود و برای تابع inc ارسال می‌شود. زمانی که نتیجه inc بازگشت داده می‌شود مقدار counter را هم بازگشت می‌دهد، متغیر counter قفل می‌شود و مقدار آن با مقداری که به inc ارسال شده بود مقایسه می‌شود. در صورتی که این دو مقدار با هم برابر باشند، یعنی عملیات قابل انجام است و مقدار جدید در counter جایگزین می‌شود و قفل آزاد می‌شود. در غیر این صورت بدون جابجای مقادیر قفل آزاد می‌شود و عملیات از اول انجام می‌شود.

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

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

به عنوان یک معمار نرم‌افزار باید تلاش کنیم تا جای ممکن برنامه‌ها را به سمت استفاده از متغیر‌های تغییر ناپذیر هدایت کنیم و اجزا بیشتری به این شکل داشته باشیم. باید تا حد امکان اجزایی که قابلیت تغییر متغیر‌ها را دارند کم کنیم.

5. آشنایی با Event Sourcing:

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

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

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

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

اما شاید نیازی نباشد کاری کنیم تا این روش و برنامه تا قیامت به کار خود ادامه دهند. شاید به اندازه کافی پردازنده و حافظه در اختیار داشته باشیم تا کاری کنیم که برنامه برای مدت معلومی به کار خود ادامه ادامه دهد.

این ایده ای است که پشت Event Sourcing قرار دارد. در این روش ما به جای نگهداری وضعیت، تراکنش‌ها را نگهداری می‌کنیم. وقتی به وضعیتی نیاز داشته باشیم به سادگی تراکنش‌ها را از ابتدا تا انتها ایجاد می‌کنیم و نتیجه وضعیت مورد نظر ما است.

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

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

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

اگر به اندازه کافی حافظه و پردازنده در اختیار داشته باشیم می‌توانیم به طور کامل برنامه خود را بر مبنای عدم تغییر بسازیم و بگوییم برنامه‌ای بر اساس Functional programming ایجاد کرده ایم.

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

6. نتیجه:

در پایان مجددا به مواردی که در این قسمت یاد گرفتیم توجه می‌کنیم.

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

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

در حقیقت در نیم قرن گذشته بیش از اینکه یاد بگیریم چه کارهایی باید انجام دهیم، یادگرفته ایم که چه کارهایی نباید انجام دهیم.

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

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

پی نوشت 1: در صورت امکان برای آشنایی با Event Sourcing حتما این فیلم رو مشاهده کنید.

پی نوشت 2: خواهش می‌کنم دست‌هاتونو بشورید، مواظب خودتون باشید، سالم بمونید.

clean architecturefunctional programmingمعماری تمیزبرنامه‌نویسی تابعی
شاید از این پست‌ها خوشتان بیاید