در این نوشته گپِ مختصری میزنیم پیرامون مفاهیم AOP . توی این مدل از برنامه نویسی، همانندِ سایرِ مدل ها، مثل OOP یا برنامه نویسی به شکل Functional ، تلاش اینه که یکسری از دغدغه هایی که توی دنیای توسعه ی نرم افزار وجود داره رو به یک نحوی حل کنیم.
برای مثال در OOP سعی میکنیم به یک نحوی Logic رو نزدیک کنیم به Entity هامون، به طوری که بشه برنامه رو به شکل Maintainable توسعه داد، یا به نوعی قابلیت Maintenance توی کدمون بره بالا. یا مثلا وقتی در مورد برنامه نویسی Functional صحبت میکنیم، در واقع تمرکزِ اصلیِ ما روی این مساله هستش که بتونیم Reusability رو تا حد ممکن بالا ببریم و علاوه بر اون تا جای ممکن، برنامه نویسی رو ساده کنیم. به طوری که وقتی شما یک Function رو نگاه میکنید، به سادگی بتونید از روی Signature اون متوجه بشید که به چه شکلی کار میکنه و در واقع به راحتی قابل حدس و predictable باشه.
در توسعه ی نرم افزار مسائلی وجود دارند که حالت جانبی دارند. به این معنا که گاهی اوقات این مسائل در Functionality هسته ی سیستمِ ما قرار نمی گیرند. زمانی که ما یک نیازمندی داشته باشیم که جزء بیزینسِ اصلیِ برنامه مون نیست، موضوعی که پیش میاد اینه که به احتمال زیاد، این Logic پخش میشه در جاهای مختلفی از برنامه ی ما. برای مثال Non-functional requirements (نیازمندی های غیر وظیفه مند) معمولا از این نوع نیازمندی ها هستند.
یا مثلا بحثِ Security رو در نظر بگیرید. زمانیکه در مورد Security یک سرویس صحبت میکنیم، مساله ای که وجود داره اینه که Logic ای که برای این قسمت در نظر میگیریم، به احتمال خیلی زیاد، در جاهای مختلفی از نرم افزارمون، مدام در حال تکرار شدن هستش. برای روشن شدن موضوع بهتره مثال های زیر رو بررسی کنیم.
فرض کنید قصد داریم چک کنیم که آیا یک کاربر اجازه ی دسترسی به یک Resource رو داره یا نه. این Resource ممکنه در جاهای مختلفی از برنامه در قالب متدها و ... تکرار بشه. حالا اگر خودمون بخواهیم به صورت دستی این مساله رو مدیریت کنیم، نتیجه ی کارمون یک قطعه کد میشه، که باید همه جا تکرار بشه. به این نقاط، اصطلاحا Cross Cutting Concern گفته میشه. جلوتر این مفهوم رو کامل میشکافیم و توضیح میدیم. برای درک این مساله تصور کنید که از وسط اپلیکیشنِ ما، یک برش افقی زده بشه و توی این برش، مشاهده میکنیم که در همه جا، این نیاز وجود داره که یک مفهومی قرار داده بشه. در این مثال، این مفهوم، Security بود.
یک مثال دیگه که خیلی همیشه در موردش صحبت میشه بحث Logging هستش. احتمالا زمانی که در حال توسعه ی نرم افزار هستیم، نیاز داشته باشیم که خیلی جاها به شکل سیستماتیک یک log به برنامه اضافه کنیم. مدل کلاسیک این عمل، به این صورت میشه که فرضا در ابتدا و انتهای یک متد یک log میذاریم تا بتونیم ورودی و خروجی هامونو چک کنیم. اگر در پروژه، چنین نیازمندی هایی داشته باشیم و بخواهیم بصورت کلاسیک پیاده سازیشون کنیم، نیازه که فرضا براش یک متد بنام log بنویسیم تا از این متد در جاهای مختلفی استفاده کنیم. همونطور که قبلا هم بهش اشاره شد این نقاط همون Cross Cutting Concern ها هستند که به نوعی همه جای برنامه ی ما رو تحت تاثیر خودشون قرار میدن.
مثالی دیگه ای که میشه بررسیش کرد بحثِ Observability هستش. بالا بودنِ این شاخصه در سیستم های نرم افزاری خیلی مهمه. مخصوصا این روزها با اومدن سرویس های جدید، مساله ی Cloud Native بودنِ اپلیکیشن خیلی اهمیت پیدا کرده. به این صورت که یک اپلیکیشن، میتونه روی سرورهای مختلفی بیاد بالا، یا مثلا اگر طول عمر کوتاهی داشته باشه، میتونه منتقل بشه به یک سرور دیگه. در این حالت Observability اهمیت زیادی پیدا میکنه. به این معنی که ما بتونیم مشاهده کنیم اون سیستمی که در حال ساختنش هستیم، تا چه حد قابل مانیتور شدنه. باز هم در اینجا با مفهوم Cross Cutting Concern ها مواجه میشیم. اگر بخواهیم بخش هایی در کدمون مانیتور بشه، نیازمند این هستیم که باز هم یک قطعه کدی رو مدام تکرار کنیم. اتفاقی که در اینجا میوفته اینه که مجددا مثلِ بحثِ log و Security که قبلا بهش اشاره شد، لازم میشه که کدمون رو در جاهای مختلفی Duplicate کنیم. مدیریتِ کد در این حالات به شکل دستی ،بسیار چالش برانگیز میشه.
باید توجه زیادی کنیم که AOP مثل بقیه ی پارادایم های توسعه ی نرم افزار، نیومده که همه ی مشکلات رو یکجا حل کنه. به نوعی AOP چیزی نیست که بتونیم بگیم اگر وجود نداشته باشه و ازش استفاده نکنیم، مشکلات زیادی توی توسعه ی نرم افزار پیش میاد. همه میدونیم که کار اصلی ماها سر و کله زدن با مشکلات و چالش هاییه که پیش رومونه. مانند مثال هایی که در بالا گفته شد، اما باید بدانیم که AOP برای ما، تنها راه حلِ جواب دادن به این نوع از مشکلات نیست.
باید آگاه باشیم که AOP یک راه حلِ بسیار جذابه که شاید مناسبِ مواجهه با همه ی مشکلات نباشه. یعنی اگر بعنوان یک ابزار در نظر بگیریم، ما نباید زیاد Overuse کنیمش. مثل همه ی ابزارهای دیگه ای که در اختیار داریم.
یعنی Concern هایی که توی قسمت های مختلف اپلیکیشنِ ما پخش هستند. باید بدانیم که پیدا کردن نقاط Cross Cutting Concern بسیار مهمه. به این معنی که میتونیم توی سیستم ذکر کنیم که فرضا، تمام این نقاطی که ما تعریفشون میکنم، نقاطی هستند که ما قصد داریم یک تغییر یا یک مفهومی رو به اپلیکیشن خودمون اضافه کنیم. برای مثال تصور کنید که میخواهیم بگیم که قبل از تمامِ متدهایی که کارِ تغییر بر روی دیتابیس رو انجام می دهند، قصد داریم چیزی رو log کنیم.
در واقع Cross Cutting Concern نیازمندی هایی میشن که نقاط مختلفی از اپلیکیشنِ ما رو هدف میگیرند.
معمولا این نوع نیازمندی ها، نیازمندی های Non-functional یا غیر وظیفه مند هستند. برای مثال اگر ما یک نیازمندی رو برای یک اپلیکیشن بنویسیم، احتمالا این موارد درون Main Success Scenario پروژه مون نمیاد. مثلا یکیش اینه که میخواهیم هر وقت یک Controller اجرا شد، یک چیزی log بشه. این مورد چیزیه که اگر بخواهیم پیاده سازیش کنیم، همه ی Controller هایی که داخل سیستم داریم رو دستخوش تغییر میکنه. معمولا این موارد چیزهایی نیستند که توی نیازمندی های Product Owner بیاد. مثلا یک Product Specialist هیچوقت نمیاد به ما بگه که من میخوام فرضا فلان log توی برنامه اجرا بشه :)) این موارد چیزهایی هستند که ممکنه فقط تکنیکال باشند. یا مثلا همان بحث Security که بهش اشاره کردیم رو دوباره در نظر بگیرید، در اونجا گفتیم که نیاز بود تا Authenticate در جاهای مختلفی که با Resource سر و کار داشتیم، انجام بشه. در اون حالت میومدیم یک متد مینوشتیم فرضا با نام isUserAuthenticate و در همه جای برنامه ازش استفاده میکردیم.
بنابراین این مدل دغدغه ها که نقاط مختلفی از اپلیکیشن رو تحت تاثیر قرار میدن بهشون میگیم Cross Cutting Concern .
بطور کلی اون قطعه کد و Logic ای که ما میخواهیم در نقاط مختلف برنامه مورد استفاده قرار بگیره رو بهش میگیم یک Advice.
به اون نقاطی از اپلیکیشن میگیم که میخواهیم Advice مان در اونجا اجرا بشه.
به مجموعه ی Point Cut و Advice میگیم یک Aspect . به این معنا که اگر فرضا بگیم فلان قطعه کد قراره در فلان جاها اجرا بشه، به این مجموعه میگیم یک Aspect از برنامه مون.
فرض کنید که یک Aspect داریم که قراره بیاد با کد اصلیمون ترکیب بشه. یعنی فرضا ما خودمون تعریف کردیم که برای مثال قبل از اجرا شدن یک سری متد، کار X انجام بشه. حالا اتفاقی که میوفته اینه که درون یک پروسه ای، در یک قسمتی از اجرای نرم افزارمون، لازم میشه که این کدها با کدهای اپلیکیشنِ ما ترکیب بشه. اصطلاحا میگیم که عمل Weave اتفاق بیوفته. به نوعی این دو بخش از کد رو میبافیم توی همدیگه. حالا این مورد توی سیستم های مختلف و در زبان های مختلف میتونه به روش های مختلفی اتفاق بیوفته.
برای زبانی مثل جاوا که کامپایلری هستش، سه حالت ممکنه پیش بیاد:
مزیت Load time Weaving چیه؟
مزیتش اینه که دیگه با Overhead Runtime مواجه نمیشیم. یعنی دیگه در Runtime نیازی نیست که فرضا یک Coordinator وجود داشته باشه که بیاد و بررسی هاش رو انجام بده که الان متد X داره اجرا میشه، پس من برم Aspect فرضا Y رو اجرا کنم. در واقع کد، از قبل Generate شده و وجود داره.
عیب Load time Weaving چیه؟
عیبش اینه که باعث میشه Load time بره بالا. یعنی زمانی که داریم یک کلاس رو میخونیم و میبریمش توی حافظه، کارهای بیشتری توسط JVM در حال انجام شدنه. به همین دلیل ممکنه Startup time اپلیکیشنِ ما مقداری افزایش پیدا کنه. البته این زمان هم بستگی به این داره که چه تعدادی Aspect و چند تا Point cut مختلف داریم که داره از اونها استفاده میکنه.
مقایسه با Compile time Weaving :
کاری که عمل Weaving در زمان Compile time میکنه اینه که زمان کامپایلر رو ممکنه بیاره بالا و در نتیجه Compile time بیشتر بشه. اما نهایتا اون دو تا مشکلی که بهشون اشاره کردیم رو در مورد Runtime و Load time دیگه نداریم. یعنی اون کدی که در زمان Load time داریم و یا اون کدی که در زمان Runtime خواهیم داشت رو دیگه در این حالت نداریم.
مثلا فرض کنید که یک متدی نوشتیم و گفتیم که قبل از اجرا شدنش، متدِ Y هم باید اجرا بشه. میتونیم اینجوری در نظر بگیریم که در واقع این متدِ Y اومده توی کدِ ما تزریق شده و در نهایت اون مجموعه به شکل ترکیبی هستش که داره کامپایل و اجرا میشه.
در انتها یک مثال دیگه هم بررسی میکنیم که البته مربوط به AOP نمیشه ولی بیانش در اینجا خالی از لطف نیست. اون هم بحث Annotation Processor ها هستش که پس از اینکه در سیستم خوانده میشن، بر اساس اونها یکسری کد تولید میشه و بایت کدِ ما رو دستخوش تغییر میکنند. این مساله چیزی نیست که در زمان Load time یا Runtime اتفاق بیوفته. به نوعی حتی ما میتونیم این بخش رو از JAR File پروژه مون هم حذف کنیم. انگار که این بخش کارش رو کرده و تمام شده و رفته. دقیقا چیزی شبیه به کاتالیزورها در شیمی هستند :)) که توی ترکیب شیمیایی شرکت می کردند، اما تهش خودشون رو می کشیدند کنار و توی اون محصول نهایی وجود نداشتند. اینم دقیقا همونجوریه و کارش فقط در زمان کامپایله و در زمان اجرا کار خاصی انجام نمیده.
توی جاوا چند پیاده سازی معروف وجود داره. یکی از این پیاده سازی ها AspectJ هستش که به نوعی یک پیاده سازیِ پیچیده از AOP در جاواست که کامپایلرِ مخصوص به خودش رو داره. در واقع در این حالت یک AspectJ Compiler داریم که میتونه کدهای Aspect ما رو بگیره، کامپایل کنه و با کدهای اصلی Weave کنه. پس از این توالی، در نهایت بایت کدِ نهایی رو تولید میکنه. در واقع AspectJ به صورت پیش فرض معمولا در Compile time استفاده میشه (Compile time Weaving).
یک پیاده سازی دیگه هم داریم بنام Spring AOP که پیاده سازیش به مراتب ساده تره. اما در نهایت قدرت کمتری داره و تمام قابلیت های AspectJ رو نداره. ولی در نهایت برای ما اونقدری قابلیت داره که اکثر نیازمندی هامون رو برطرف کنه.
منبع:
OH! My Talks!