آبجکت های شما، سرورهای شما

فهرست مطالب

گواهی رفع مسئولیت :)

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

مقدمه

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

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

شی‌گرایی یعنی وجود class

خیر. شی‌گرایی یعنی برنامه نویسی مبتنی بر آبجکت‌ها. و class فقط یک روش ساخت آبجکت‌ هاست. فقط «یک» روش، و نه تنها روش!

وراثت جزیی از شی‌گرایی است

خیر. وراثت یکی از چندین مکانیزم در دسترس برای تشریح رابطه‌ی بین تایپ‌ها برای «تایپ سیستم» است.

پلی مورفیسم جزیی از شی‌گرایی است

خیر. پلی مورفیسم یکی از مکانیزم های «تایپ سیستم» است که می‌توان با اتکا به آن، در موقعیت‌های یکسان از رفتارهای متفاوتی بهره برد. (توضیح کامل آن در مقاله‌ی دیگری داده شده.)

مجوز‌های دسترسی مثل public یا private جزیی از شی گرایی است

خیر. جلوگیری از دسترسی به State داخلی یک آبجکت از اصول اساسی شی گرایی است؛ و مجوزهایی مثل private و public تنها یکی از روش های پیاده سازی چنین ایده ای هستند. و نه تنها روش!

پس شی گرایی چیست؟

برنامه نویسی مبتنی بر آبجکت‌ها!

آبجکت چیست؟

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

  • فعال، به این معنی که این واحد ها قادر به انجام کار می‌باشند. کارهایی که یک آبجکت قادر به انجام آن است در متدهایش تعریف می‌شود.
  • مستقل، به این معنی که هر آبجکت مانند یه جعبه‌ی دربسته است و آبجکت‌های دیگر نباید قادر به ویرایش State درونی اش باشند.

زبان شی‌گرا چیست؟

زبانیست که تمام اجزای آن تشکیل شده از آبجکت‌ها باشند:

  • توابع، آبجکت هستند.
  • متغیرها، آبجکت هستند.
  • مقادیر مانند 23 یا "hi" آبجکت هستند.

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

داستان شی گرایی

عبارت object-oriented programming توسط یکی از دانشمندان معروف دنیای کامپیوتر آقای Alan Kay به سر زبان ها انداخته شد. تا قبل از آن مفهومی تحت عنوان Object در زبان Simula وجود داشت؛ حتی مفاهیمی مانند‌ class یا inheritance از زبان Simula آمده‌اند، ولی اینطور نبود که افراد مشخصا مبتنی بر آبجکت‌ها برنامه نویسی کنند. (مانند فانکشن که در همه زبان‌ها هست، ولی همه‌ی زبان ها فانکشنال نیستند!)

Alan Kay ایده‌هایش را جمع کرد و به کمک دوستانش زبان Smalltalk را ساخت. Simula و Smalltalk دو دید متفاوت به شی‌گرایی دارند.

Simula شی‌گرایی را بیشتر به عنوان «ساختار کدها» و در هارمونی با «تایپ سیستم» در نظر می‌گیرد، و Smalltalk شی‌گرایی را به عنوان شیوه ای برای ساخت یک سیستم زنده، داینامیک، و مقیاس پذیر تصور می‌کند. یکی توجه اصلی اش مدل نوشتن کدهاست، و دیگری مدل اجرای کدها! برای همین است که Smalltalk تنها یک زبان نیست و در واقع به عنوان یک Environment نرم افزاری عرضه می‌شود که بیشتر شبیه یک OS کامل است! در حال حاضر این دو زبان به عنوان مرجع تمام زبان‌های OOP شناخته می‌شوند.

پ.ن: Alan Kay ، و Ole-Johan Dahl و Kristen Nygaard (سازندگان Simula) همگی جزو برندگان جایزه Turing می‌باشند.

فعلا از این‌ها بگذریم…

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

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

خوشبختانه چند روز پیش از نگارش این مطلب، مشغول تویتر گردی بودم که ناگهان یک توییت نظرم رو جلب کرد. یک برنامه نویسی، آبجکت‌ها را به «وب سرویس‌» ها تشبیه کرده بود! آنجا بود که گفتم لعنتی، این همان چیزی بود که من در فکرم داشتم ولی نمی‌توانستم به درستی ابرازش کنم. و تازه یادم افتاد که چرا زودتر این قضیه به فکرم نرسیده بود، چون Alan Kay خودش به طور مشخص چنین مثالی را درباره شی‌گرایی زده بود: اینکه آبجکت‌ها را باید مثل کامپیوترهای مستقل‌ای تصور کرد که در یک شبکه قرار دارند!

I thought of objects being like biological cells and/or individual
computers on a network, only able to communicate with messages.

یک سیستم شی‌گرا، به نوعی مانند اینترنت است! کامپیوتر هایی که در یک شبکه عظیم قرار گرفته اند و با هم «تبادل پیام» می‌کنند! در حقیقت ARPAnet (پروژه‌ای که بعد ها تبدیل به اینترنت شد) جزو پروژه‌هایی بوده که Alan Kay از آن الهام گرفته بود!

Alan Kay می‌گوید ایده‌ای که براش اهمیت داشته فقط «ارتباط از طریق انتقال پیام بوده» (Message passing) و از اینکه از کلمه Object برای توصیف چنین موضوعی استفاده کرده پشیمان هست چون این کلمه باعث گمراهی برنامه نویسان شده!

حالا با تمام این توصیفات، اولین زبانی که اسمش به ذهن تان میرسد چیست؟ متوجه شدید چرا من شی‌گرایی را از ارلنگ یاد گرفتم؟ زبانی که برنامه شما در قالب اجزای زنده ای توصیف می‌شوند که تنها از طریق انتقال پیام با یکدیگر در ارتباط اند!

ایده

چیزی که من می‌خوام پیشنهاد بدهم برای تمام زبان‌های شی ‌گرا قابل اجراست. در هر زبان شی‌گرایی که دوست دارید می‌توانید از این ایده استفاده کنید. آن ایده این است که بیاییم «دیدگاه» خودمان درباره آبجکت‌ها را عوض کنیم!

بیاییم اینطور تصور کنیم که هر شی، یک وب سرور مستقل است. یا یک مایکرو سرویس است!

  • سرور = آبجکت
  • کلاینت = کدی که قرار است از آبجکت استفاده کند (ممکن است خودش یک آبجکت باشد)
  • فراخانی های http = فراخانی متد
  • پارامترهای GET یا POST = پارامترهای متد
  • API وب سرویس = اینترفیس‌ها

تصور کنید که هر آبجکت، مانند سروری است که به طور مستقل فعالیت می‌کند و IP خودش را دارد.

اینطور تصور کنید که هر متد، همانند یک route در API سرور شماست! و پارامترهای GET یا POST که در آن route پذیرش می‌شوند، همان پارامترهایی هستند که شما در تعریف متد خود مشخص کرده‌اید.

آیا یک سرور، می‌تواند به حافظه‌ی رم سرور دیگر دسترسی داشته باشد و آن را تغییر دهد؟

یا حتی تفکیک وظایف سرور ها را در نظر بگیرید؛ یکی وب سرور است. یکی سرور DNS است. یکی سرور دیتابیس است… هر کدام وظیفه مشخصی دارند. آبجکت‌ها هم باید با وظایف مشخصی طراحی شوند.

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

در طول خواندن ادامه این نوشته، به طور مداوم این موضوع را در مغز خود تکرار کنید: هر آبجکت مانند سروری است که در یک کامپیوتر مستقل میزبانی می‌شود!

پینشهادات

بعد از Constructor دیگر چیزی را تغییر ندهید

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

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

اگر بعد از مقدار دهی اولیه، دیگر State داخلی آبجکت را تغییر ندهیم، می توانیم با خیال راحت و بدون ترس از Race condition از آن آبجکت در سناریوهای همروند استفاده کنیم.

پیشنهاد من این است که به جای این کد:

class Person {
    private String name;
    private int age;

    public Person() { }

    public void setName(String name) {
        this.name = name == null? "" : name;
    }

    public void setAge(int age) {
        this.age = age;
    }
    
    public String getInfo() {
        return String.format("%s is %d years old.", this.name, this.age);
    }
}

// ------------------------------

var person = new Person();

person.setName("Amir");
person.setAge(30);
System.out.println(person.getInfo());

person.setName("Sara");
person.setAge(25);
System.out.println(person.getInfo());

سعی کنید از کدی مثل کد زیر استفاده کنید:

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name == null? "" : name;
        this.age = age;
    }

    public String getInfo() {
        return String.format("%s is %d years old.", this.name, this.age);
    }
}

// ------------------------------

var amir = new Person("Amir", 30);
System.out.println(amir.getInfo());

var sara = new Person("Sara", 25);
System.out.println(sara.getInfo());

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

هر آبجکت، هویت ثابتی دارد. از یک آبجکت برای بیان چندین موجودیت استفاده نکنید. اگر کلاس Person دارید و می خواهید ۵ نفر را با آن ابراز کنید، ۵ نمونه از روی آن بسازید. اگر کلاس Cat دارید و می خواهید ۲ گربه را با آن ابراز کنید، ۲ نمونه از روی Cat بسازید. موجودیت‌های برنامه را شناسایی کنید، و ببینید به چند مورد از آن‌ها نیاز دارید و به همان اندازه نمونه‌های مجزا تولید کنید. از ساخت نمونه‌هایی که چندین موجودیت را ابراز می‌کنند بپرهیزید.

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

همچنین یادتان باشد که Constructor قرار است state آبجکت را مقدار دهی اولیه کند؛ نباید درون Constructor از کدهای عملیاتی/الگوریتمی استفاده کرد. کار Constructor فقط مقدار دهی اولیه است و تمام.

آبجکت‌ها را تک وظیفه‌ای کنید

به نظر من، کلاس ایده آل کلاسی است که دو متد بیشتر نداشته باشه! یک Constructor که State آبجکت را مقدار دهی می‌کند، و یک متد دیگر که عمل اصلی پردازش را به عهده دارد! یعنی هر کلاس، مترادف با یک «تابع» شود! مسلما نمی‌توانیم همیشه چنین آبجکت کوچکی بسازیم. ولی خوب است که حداقل سعی کنیم آبجکت‌ها را کوچیک طراحی کنیم. درست مثل مایکروسرویس ‌ها که هر کدام تلاش دارند فقط یک کار مشخص را انجام دهند.

منظورم از کوچک کردن آبجکت‌ها این نیست که به زور تعداد متدها را کم کنیم. خیر. در عوض باید سعی کنیم آبجکت را به سمت «تک وظیفه‌ای» شدن پیش ببریم. هر چه لیست وظایفی که آبجکت به عهده دارد کمتر باشد، خود به خود کوچک خواهد شد! هر چه آبجکت تک وظیفه‌ای تر شود، مستقل تر خواهد شد و تست کردن آن نیز راحت تر خواهد بود.

از Dependency Injection استفاده کنید

کلاس بهتر است در بدنه‌ی خودش از new استفاده نکند! به زبانی دیگر، کلاس نباید خودش به طور مستقیم آبجکت‌هایی که به آن‌ها نیاز دارد را نمونه‌سازی کند. این نمونه‌ها باید از بیرون کلاس به آن تزریق شوند! در غیر این صورت این دو آبجکت به شدت به هم وابسته خواهند شد. یک راه حل ساده این است که این نمونه‌ها از طریق Constructor وارد کلاس شوند.

به کمک DI، آبجکت‌ها می‌توانند به طور مستقل تست شوند. اگر آبجکت A درون بدنه‌ی خودش نمونه‌ای از آبجکت B ساخته باشد، آنوقت هنگام تست کردن A باید ابتدا میرفتیم و آن شی ‌B را تست میکردیم!

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

پس می‌توانیم اینطور بگوییم که DI می‌تواند به شما کمک کند که وابستگی بین آبجکت‌ها را کم کنید! و این جزو اهدافی است که باید به دنبال آن باشید.

پ.ن: زیاد خودتان را با فریم‌ورک های DI درگیر نکنید. شما براحتی می‌توانید برای DI از Constructor ها استفاده کنید. لازم نیست خودتان را به هیچ فریم ورکی وابسته کنید. ما در دنیای امروز برنامه نویسی به اندازه کافی به فریم ورک های مختلف وابسته شده ایم! ۴ خط کد بیشتر بنویسید، منت قصاب را نکشید!

State را از خارج آبجکت به آن تزریق کنید

درست مانند DI که با آن نمونه‌های مورد نیاز آبجکت را از بیرون به آن تزریق می‌کنید، من پیشنهاد می‌کنم تا حد امکان State را هم از بیرون به آبجکت تزریق کنید و در داخل آبجکت State کمتری نگه داری نمایید!

فرضا در مثال محاسبه BMI، پارامترهای مورد نیاز را از بیرون کلاس به آن پاس دهید. آن هم در همان لحظه‌ای که می خواهید روی آن‌ها کاری انجام دهید. مثلا به جای این کار:

obj.setAge(20);
obj.setHeight(180);
obj.setWeight(90);
obj.calculateBMI();

از چنین روشی استفاده کنید:

obj.calculateBMI(20, 180, 90);

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

به public و private اکتفا نکنید

من فکر میکنم تمام فیلدها باید private باشند، و تمام متدها public…

امیررضا، این مسخره‌ترین پیشنهادی بود که ازت شنیدیم! تو اصلی ترین موضوع شی‌گرایی را زیر سوال بردی! تو هیچی از شی‌گرایی حالیت نیست!

همممم… بذارید در این باره کمی در زبان‌های برنامه نویسی به گشت و گذار بپردازیم:

در Smalltalk که به نوعی مرجعی برای تمام زبان‌های OOP تلقی می‌شود، اصلا کلید واژه‌هایی تحت عنوان private یا public وجود ندارد! زبان به طور پیشفرض اینطور تصور می‌کند که همه‌ی فیلدها حالت private دارند و همه متدها حالت public. جالب تر می دانید چیست؟ اینکه حتی اعضای private هم واقعا private نیستند و اگر بخواهید می توانید براحتی به آن‌ها دسترسی داشته باشید! این مدل طراحی Smalltalk تصادفی نبوده و از قصد به این شکل ساخته شده است!

در زبان‌هایی مثل پایتون یا جاوا اسکریپت یا کاتلین هم متدها به صورت پیشفرض public هستند. و چیزی که خیلی‌ها نمی دانند این است که تقریبا در بیشتر زبان‌ها، حتی اگر اعضا را به شکل private تعریف کنید باز هم می‌شود به طور غیر مستقیم به آن اعضا دسترسی داشت!

سوتفاهم در اینجا به موضوع Data hiding برمی‌گردد و برداشت اشتباه برنامه نویسان از آن! Data hiding طوری به برنامه نویسان تدریس شده که فکر می‌کنند بحث اصلی سر قایم کردن اعضای کلاس است! چه چیزی را قایم کنید؟ از دست چه کسی قایم کنید؟ مگر کدی می‌تواند به طور خودسر بیاید از اعضای یک کلاس استفاده کند؟ شما اصلا تمام اعضای کلاس را public کن، تا زمانی که کدی در جایی نوشته نشود و مشخصا این اعضا را فراخانی نکند هیچ مشکلی پیش نخواهد آمد.

آقای Martin Fowler می‌گوید ایده Data hiding این نیست که اعضای یک آبجکت را قایم کنید. واژه‌هایی مثل private یا public صرفا برای این هستند که اگر بقیه برنامه نویس‌ها کدهای شما را مطالعه کردند، بدانند که نباید روی اعضای private حساب باز کنند چون آن‌ها ممکن است هر لحظه دچار تغییر شوند. ایده اصلی Data hiding این است که آبجکت‌ها به State داخلی همدیگر کاری نداشته باشند. همین. (در حقیقت ایده اصلی Encapsulation است).

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

حالا چرا پیشنهاد میکنم که متدها را به طور public طراحی کنید؟ مسلما اینکه همینطور بیخود و بی جهت هر متدی که قرار است بنویسیم را به طور public طراحی کنیم، هیچ سودی به حال ما نخواهد داشت. پینشهاد من بخاطر این است که این قضیه بتواند «محرک» ای باشد تا استایل کد نویسی شما را تغییر دهد! منظورم چیست؟

  • وقتی یک متد private می‌نویسیم، به احتمال زیاد به دلیل این بوده که آن متد State رو تغییر می داده و ما نمی خواستیم که از بیرون در دسترس باشد.
  • وقتی یک متد private می‌نویسیم، تست کردن آن متد غیر ممکن یا سخت خواهد شد. کدی که نشود آن را تست کرد، یک کد غیر مطمئن است.
  • وقتی یک متد public می‌نویسیم، می‌دانیم که نباید طوری باشد که State داخلی را تغییر دهد. این قضیه «محرک»ای می‌شود برای اینکه State را تغییر ندهیم.
  • وقتی یک متد public می‌نویسیم، می‌دانیم که باید آن را تست کنیم. و این خودش «محرک» ایست برای اینکه متدها را قابل تست شدن طراحی کنیم.

آبجکت، از دو بخش اساسی تشکیل شده است: State و رفتار! State قرار نیست جایی نمایان باشد، ولی رفتار آبجکت قرار است برای دیگران نمایان و در دسترس باشد. اگر رفتار آبجکت برای بقیه در دسترس نباشد، یعنی آبجکت دارای رفتارهای پنهان است! یعنی آبجکت به غیر از ویژگی‌هایی که بقیه از اون انتظار دارند، دارای وظایف جانبی و مخفی می‌باشد؛ و یکی از اصول سودمند آبجکت‌ها «تک وظیفه» ای بودن آن‌هاست!

هر چه آبجکت‌ها را «تک وظیفه‌» ای تر کنید، متوجه می‌شوید که دیگر آنقدر متدی در آن وجود نخواهد داشت که بخواهید برای private یا public بودن آن‌ها سردرگم شوید!

پس این پیشنهاد برای ترغیب شما به:

  • ساخت متدهایی که State را تغییر نمی دهند.
  • ساخت متدهایی که مناسب تست شدن طراحی شده‌اند.
  • ساخت آبجکت‌هایی که «تک وظیفه» ای هستند و کلا متدهای زیادی ندارند.

وگرنه اگر قرار باشد همینطور بیخود متدها را public کنید، به هیچ دردی نخواهد.

اگر متد شما public بود، و با اینحال باز هم State داخلی آبجکت را برای دیگر آبجکت‌ها نمایان نمی‌کرد، آن وقت است که شما یه کد خوب نوشته‌اید! برای همین است که می‌گوییم Data hiding باید از طریق معماری کدهای‌تان به وقوع بپیوندد، و نه از طریق چیزهایی مثل public و private (هرچند که این‌ها می توانند شما را در این امر یاری کنند).

متدهای private به هیچ دردی نمی‌خورند؟

چرا. پیشنهاد من بر این اساس بود که اگر متد شما قصد تغییر State را داشت، و شما به این دلیل آن را private کرده اید، بهتر است بجای private کردن آن سعی کنید راهی پیدا کنید تا دیگر نیازی به تغییر State نباشد! به نوعی صورت مساله را عوض کنید!

اما متدهای private برای مخفی کردن «پیاده سازی» همچنان سودمند هستند. فرض کنید متدی دارید که اصلا State را تغییر نمی دهد. اما شما مطمئن نیستید که پیاده سازی آن تا ابد به همین شکل باقی بماند. آن کد جزو چیزهایی است که فکر می‌کنید احتمالا در آینده به دلایل مختلف مثل تغییر الگوریتم یا پیدا کردن روشی برای سرعت بالاتر، تغییر خواهد کرد. چنین متدی را می‌توانید به طور private تعریف کنید.

دقت کنید که private کردن با هدف تغییر Stete ، و private کردن با هدف منعطف بودن پیاده سازی، دو چیز متفاوت هستند.

اگر متدها را به شکل public تعریف کنیم، آیا ممکن نیست بقیه آبجکت‌ها با توجه به اینکه به آن متدها دسترسی دارند به آنها وابسته شوند؟ مگر قرار نبود وابستگی بین آبجکت‌ها را کم کنیم؟

برای جواب این سوال، بخش بعدی را مطالعه فرمایید…

اینترفیس، اینترفیس، اینترفیس

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

اگر طراحی را از پیاده سازی جدا کنیم، دست ما باز خواهد بود تا با خیال راحت‌تری پیاده سازی را تغییر دهیم! همانطور که می دانید طراحی به ندرت تغییر می‌کند، ولی پیاده‌سازی همیشه دستخوش تغییر خواهد شد.

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

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

جواب: اینترفیس ها!

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

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

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

  • به این صورت این موضوع مهم می‌شود: این آبجکت چیست، و چه کارهایی میتواند بکند.
  • و این موضوع از اهمیت می‌افتد: این آبجکت چیست، و چگونه کار میکند!

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

زبان مورد علاقه‌ی من دارای مکانیزم اینترفیس نیست؛ چه راه‌کاری پینشهاد می دهید؟

جواب: برخی زبان‌ها مانند پایتون دارای اینترفیس نمی‌باشند. موضوع این است که این وسط اینترفیس‌ها برای ما مهم نیستند. مهم در واقع «ایده»ی جدا سازی طراحی از پیاده‌سازی است. مثلا یک راهش می‌تواند این باشد که شما چنین چیزی را با وراثت شبیه سازی کنید:

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

آیا متدهای public به نوعی همان API خارجی یک آبجکت محسوب نمی‌شوند؟

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

ویژگی‌های کلاس من با یک اینترفیس قابل بیان نیست. ایرادی ندارد اگر از چندین اینترفیس استفاده کند؟

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

از Inheritance استفاده نکنید

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

پس چرا این همه کتابخونه از Inheritance استفاده میکنن؟

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

آیا میشود از Trait استفاده کرد؟

بله. اتفاقا Trait ها جزو مکانیزم‌های مورد علاقه‌ی من هستند! Trait در هر زبانی اسم متفاوتی دارد. فرضا در جاوا Trait با همان اینترفیس ها تعریف می‌شود. (از جاوا ۸ به بعد). مهم ایده ایست که Trait با خود به همراه دارد، و آن گردآوری نکات مثبت Inheritance و interface در یک مکانیزم واحد است.

حذف متدهای set، و استفاده از دیتابیس!

وقتی برای یک آبجکت متدهای set تعریف می‌کنید، در واقع اجازه‌ی ویرایش State داخلی آبجکت را برای اشیای دیگر فراهم کرده اید. به محض اینکه از متدهای set استفاده کردید، آبجکت شما از حالت آبجکت خارج میگردد و به یک «دیتا استراکچر» تبدیل می‌شود!

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

یکی از عناصر اصلی در بیشتر برنامه‌های سرور، دیتابیس می‌باشد. دیتابیس یک موجودیت متغیر است. فیلدها اضافه می‌شوند، پاک می‌شوند، ویرایش می‌شوند… در طراحی شی‌گرا هم می توانید آبجکت‌هایی داشته باشید که منحصرا به عنوان یک «دیتابیس» موجود در رم از آن استفاده کنید! بیایید در این نوشته آن‌ها را با اسم «دیتابیس آبجکت» صدا کنیم :)

همانطور که دیتای مورد نیاز یک سرویس را در خود سرویس نگه داری نمی‌کنید و به جایش از یک پایگاه داده برای نگه داری داده ها بهره می‌گیرید، من پیشنهاد میکنم مواردی که نیاز به تغییر دارند را در قالب یک «دیتابیس آبجکت» جداگانه پیاده سازی کنید. و این «دیتابیس آبجکت» را به کمک DI به آبجکت اصلی خودتان تزریق کنید!

در آن «دیتابیس آبجکت»، فقط متدهای get و set را به عنوان متدهای public داشته باشید. به حالتی که مشخص باشد این آبجکت فقط و فقط به منظور ذخیره و بازیابی داده هاست. همچنین اکیدا به این موضوع توجه کنید که درست همانند یک دیتابیس واقعی، باید مساله کانکشن‌های همروند را در آن پیش بینی کنید! زبان‌های مختلف مکانیزم‌های گوناگونی را برای‌تان ارائه کرده‌اند تا خیال‌تان از دسترسی همروند به دیتا راحت شود. از آن مکانیزم‌ها استفاده کنید. (مثلا lock ها…)

در این حالت در آبجکت اصلی شما هیچ متد get یا set ای وجود نخواهد داشت. در آبجکت اصلی شما هیچ چیزی تغییر نمی‌کند. تمام چیزهای قابل تغییر در یک محدوده‌ی مشخص جمع شده‌اند و بقیه آبجکت‌ها به آن‌ها دسترسی ندارند. همچنین هیچ مکانیزمی برای همروندی در آبجکت اصلی شما نیاز نیست چون این قضیه از طرف «دیتابیس آبجکت» ای که از آن در پشت صحنه استفاده می‌کنید پیش بینی شده است!

سخن آخر

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

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

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

در نهایت، توصیه من این است که برای شی گرایی بهتر، این جمله را مدام در مغز خود تکرار کنید:

«هر آبجکت مانند سروری است که در یک کامپیوتر مستقل میزبانی شود!»

نظرات

comments powered by Disqus