مفاهیم شی گرایی در زبان برنامه نویسی Dart (بخش دوم) — به زبان ساده
این مقاله بخش دوم از سری مطالب راهنمای زبان برنامهنویسی Dart است. در بخش قبلی این مجموعه به معرفی مفاهیم مقدماتی این زبان نوظهور پرداختیم. در این مقاله تلاش خواهیم کرد تا با معرفی مفاهیم شی گرایی از جمله مفهوم کلاس در برنامه نویسی به زبان Dart و همچنین بررسی ژنریک و فایل سیستم، درک خود را از این زبان بسط دهیم.
مبانی شی گرایی در زبان Dart
تا پیش از معرفی زبانهای برنامهنویسی شیءگرا، برنامهها به طور معمول به صورت یک سری دستورهای طولانی نوشته میشدند که از ابتدا به انتها اجرا میشدند و نگهداری آنها بسیار دشوار بود. زبان C، فرترن و کوبول نمونههایی از این زبانها هستند که زبانهای «برنامهنویسی رویهای» (Procedural Programming) نیز نامیده میشوند. همانند مثال دستور آشپزی نودل که در بخش قبلی بررسی کردیم، در این زبانها یک توالی از مراحل باید پیگیری میشد. در هر حال این فرایند برای افراد مبتدی یادگیری آسانی دارد و در مورد برنامههای کوچک نیز موجب میشود که نگهداری برنامه ساده باشد.
با این حال، میخواهیم مثال دستور پخت نودل خود را تقسیم کنیم و تلاش کنیم با استفاده از مفاهیم شیءگرایی آن را مجدداً بسازیم. به جای توصیف مراحل مورد نیاز برای پخت نودل، به بررسی تکتک اشیا در آشپزخانه میپردازیم و کاری که با آنها میتوان انجام داد را در نظر میگیریم. این شیءها شامل قابلمه، بسته نودل، اجاق گاز موارد دیگر هستند. اینک این اشیا مانند برنامههای کوچکی هستند که برای خود دادهها و تابعهایی دارند و با هم دیگر تعامل پیدا میکنند تا برخی وظایف ترکیبی مانند پختن نودل را اجرا کنند. همان طور که میبینید هدف نهایی هر دو مفهوم برنامهنویسی رویهای و شیءگرا یکسان است؛ اما روشی که برای حل مسئله انتخاب میکنند و روشی که به مسئله فکر میکنند، متفاوت است. به علاوه، روش دوم تفکر موجب میشود که کد ما قابلیت استفاده مجدد بیشتری داشته باشد.
برای نمونه اگر بخواهیم فردا نودل خاصی را بخوریم، آیا میتوانیم از همان دستور آشپزی نودل قبلی خود استفاده کنیم؟ البته جزئیات دو نودل مختلف ممکن است متفاوت باشند، اما مسائل کلیتر که با اشیایی مانند قابلمه و اجاق گاز مربوط هستند مانند هم هستند.
امیدواریم مثال فوق به روشنتر شدن مفهوم برنامهنویسی شیءگرا در ذهن شما کمک کرده باشد. همان طور که متوجه شدیم مفهومی که میخواهیم بررسی کنیم مستقل از زبان برنامهنویسی است و مفهوم دیگری است که به نام پارادایم برنامهنویسی میشناسیم. پارادایمهای برنامهنویسی دیگری مانند «برنامهنویسی تابعی» (functional paradigm) نیز وجود دارند. اگر چه این پارادایمهای متفاوت کاربردهای خاصی دارند و شما همواره با شیءگرایی در مسیر خود در حوزه برنامهنویسی وب، برنامهنویسی موبایل و حتی برنامهنویسی بازی برخورد خواهید داشت. اینک نوبت آن رسیده است که وارد دنیای شیءگرایی شویم و به معرفی اشیا و کلاسها بپردازیم.
شیءها و کلاسها
مقصود اصلی از طراحی شیءگرا این بوده است که تفکر در مورد برنامهنویسی از طریق نزدیک ساختن آن به دنیای واقعی تسهیل شود. اینک باید برنامهنویسی کامپیوتر را برای مدتی فراموش کنیم و در مورد این که یک شیء در زندگی واقعی دقیقاً به چه معنا است فکر کنیم؟ یک بسته نودل یا قابلمهای که در آن پخت و پز میکنیم یا حتی اجاق گازی که روشن میشود یا کارخانه نودل همگی قطعاً شیء محسوب میشوند. این شیءها هویت و کاربردهای خاص خود را دارند. آنها متمایز از هم هستند و هر یک مشخصات خاص خود را دارند.
این مشخصات متفاوت به ما کمک میکند که آنها را بر اساس خصوصیاتی مانند رنگ و اندازه از هم تمیز دهیم. این اشیای مختلف رفتارهای متفاوتی نیز دارند مثلاً اجاق گاز میتواند روشن یا خاموش شود. بنابراین دو وجه تمایز یافتیم: خصوصیت و رفتار. این دو وجه به تعریف اشیا در برنامهنویسی شیءگرا میپردازند.
شیء در برنامهنویسی رایانه میتواند شبیه به شیءهای دنیای واقعی باشد، چنان که در بخش قبل توضیح دادیم؛ اما میتواند شیء انتزاعیتری مانند داده، یک تقویم و یا حتی زمان نیز باشد. به این مثال عملی توجه کنید. فرض کنید مشغول نوشتن یک وب اپلیکیشن ساده هستید که از کاربر میخواهد وارد اپلیکیشن شده و ثبتنام کند. سپس یک شیء کاربر از کلاس user ایجاد میکنید (در مورد کلاسها در ادامه صحبت خواهیم کرد ولی فعلاً برای این که مفهوم شیء را بدانیم باید به آنها اشاره کنیم). این کاربر خصوصیتهای مشخصی مانند نام خانوادگی، نام، ایمیل، و رمز عبور دارد و میتواند وارد اپلیکیشن شده و یا ثبتنام کند.
ما نمیتوانیم شیء را بدون صحبت کردن در مورد کلاسها معرفی کنیم، زیرا برای ساخت آنها به کلاسها نیاز داریم. یک کلاس به سادگی نقشه اولیهای از یک قالب یا توصیف چگونگی ساخت یک شیء است. کلاس تا حدودی شبیه به نقشه ساخت یک خانه است. این نقشه خود خانه نیست؛ اما با استفاده از آن میتوان یک و یا حتی چند میلیون خانه ساخت. در مورد کلاسها و شیءها نیز همین تمثیل را میتوان استفاده کرد. ما یک بار به تعریف کلاس میپردازیم و سپس اشیای زیادی را از روی آن کلاس میسازیم. یک کلاس صرفاً به توصیف خصوصیتها و رفتارها میپردازد. در ادامه تعریف یک کلاس را مشاهده میکنید و با استفاده از این کلاس یک شیء در Dart ساخته میشود.
1main() {
2 void user1 = new User();
3 var user2 = User();
4
5 user1.id = 121;
6 user1.email = "sharadghimire5551@gmail.com"
7 print("${user1.id} and ${user1.email}");
8
9 user1.register();
10 user1.login(email);
11}
12class User {
13 int id;
14 String lastname;
15 String firstname;
16 String email;
17 String password;
18 void login(email, password){
19 print('Welcome! Your email is ${email}');
20 }
21 void register() => print('Thanks for registering');
22}
در این کد {...} class User به تعریف حالتها (مشخصات) و رفتار یک User میپردازد. کلیدواژه new برای ایجاد یک شیء از یک کلاس استفاده میشود. در Dart میتوانیم مستقیماً یک شیء را بدون new نیز بسازیم.
به id ،lastname ،firstname ،email و password متغیرهای «وهلهای» (instance) میگوییم که مقدار پیشفرض آنها null است. ()login و ()register متد نام دارند. ()user1.register روش فراخوانی متد یک شیء را نشان میدهد.
چهار مفهوم بنیادی شیءگرایی
در این بخش در مورد چهار مفهوم بنیادی شیءگرایی که تجرید یا انتزاع، کپسولهسازی، وراثت و چندریختی هستند، توضیح میدهیم.
تجرید
اگر به مثال نودل خود بازگردیم، تصور کنید از شنیدن کلمه نودل چه چیزی درک میکنید؟ چه چیزی به ذهن شما میآید؟ یک رشته بلند و دراز که میتوان خورد؟ در واقع از این تصویر نمیتوان نوع خاص نودل را استنباط کرد. همچنین به ما گفته نشده که نودل پخته شده یا خام است. ما صرفاً مفهوم مقدماتی نودل را درک کردهایم و دیگر هیچ خصوصیت مشخصی به ما داده نشده است. از آنجا که قبلاً یک ایده واقعی از نودل داریم میتوانیم آن را درک کنیم. ما قبلاً نودل خوردهایم و از این رو میتوانیم مفهوم نودل را از همه نودلهای واقعی مختلف دنیا تجرید یا انتزاع بکنیم.
این معنی تجرید است، یعنی میتوانیم به جای توجه به گونه خاصی از یک شیء، روی کیفیتهای اساسی آن تمرکز کنیم. بنابراین ایدهای داریم که کاملاً از یک وهله خاص از آن شیء متمایز است و آن را تجرید مینامیم. این همان چیزی است که در برنامهنویسی شیءگرا در زمان ایجاد کلاس استفاده میکنیم. ما برای همه انواع مختلف نودل یک کلاس جداگانه نمیسازیم؛ بلکه یک کلاس برای نودل میسازیم و سپس شیء نودلهای مختلف را از روی آن میسازیم.
کپسولهسازی
خصوصیت و رفتارهای خاصی را انتخاب کنید و آنها را با هم در یک کلاس قرار دهید و دسترسی به آن را از بیرون محدود کنید. یک شیء نباید اطلاعات مربوط به خود را به جز در مواردی که مطلقاً ضروری است افشا کند، و باید دادههایی را که میتوانند از درون کلاس مورد دسترسی قرار گیرند، از دنیای بیرون پنهان کند. برای نمونه ما از دکمههای ریموت کنترل استفاده میکنیم و نتیجه آن را روی نمایشگر تلویزیون میبینیم؛ اما نمیدانیم که درون ریموت چه اتفاقی میافتد و چگونه کار میکند.
این کار «طراحی جعبه سیاه» (black boxing) نام دارد. بدین ترتیب میتوانیم روش انجام کارها را به صورت امنی تغییر دهیم. ما به جای این که در مورد کل برنامه نگران باشیم، صرفاً توجه خود را معطوف به یک کلاس خاص میکنیم. این وضعیت به طور کامل به یک کلاس خاص مربوط است و نه کل برنامه و باید وابستگی درون اپلیکیشن را کاهش دهیم تا با یک تغییر ساده در یک کلاس تأثیری روی بخشهای دیگر نداشته باشد.
وراثت
ما میتوانیم به سادگی یک کلاس جدید بسازیم؛ اما به جای نوشتن همه محتوای آن کلاس، آن را بر مبنای یک کلاس موجود طراحی میکنیم. برای نمونه تصور کنید یک کلاس User در اپلیکیشن خود داریم و در ادامه میخواهیم یک کلاس Admin User جداگانه نیز بسازیم. نکته اینجا است که کلاس User و کلاس Admin User دقیقاً یکسان هستند؛ به جز این که خصوصیتها و رفتارهای بیشتری در کلاس Admin User وجود دارد. به این ترتیب از تجرید استفاده میکنیم و تلاش میکنیم روی پیادهسازی جنبههای مهم کلاس تمرکز کنیم، چون همه کاربران، ادمین نیستند. بنابراین در اینجا دو راهکار وجود دارد که در یکی میتوانیم یک کلاس جدید بسازیم و در دیگری یک کلاس جدید را بر مبنای کلاس User موجود طراحی کنیم. در برنامهنویسی شیءگرا بهتر است کلاسهای جدید را بر مبنای کلاسهای موجود یا به اصطلاح با ارثبری از کلاسهای موجود طراحی کنیم.
معنی وراثت نیز همین است. اینک کلاس جدید admin به طور خودکار همه مشخصهها و رفتارهای کلاس User را دارد و میتوانیم مشخصهها و رفتارهای خاصی را که میخواهیم به آن بیفزاییم. بر اساس اصطلاحات برنامهنویسی شیءگرا اینک کلاس User سوپرکلاس (یا کلاس والد) و کلاس Admin به نام کلاس فرعی (یا کلاس فرزند) نامید میشود. به علاوه میتوانیم کلاسهای زیاد دیگری را نیز بسازیم که از کلاس User مبنای ما ارثبری کنند. اکنون میتوانیم یک شیء User بسازیم یا شیء Admin یا به سادگی هر شیء دیگری را با استفاده از کلاس مبنا بسازیم.
چندریختی
چندریختی یا «پلیمورفیسم» (Polymorphism) به بیان ساده به معنی داشتن شکلهای مختلف است. یعنی میتوانیم از سازه واحدی در موقعیتهای مختلف استفاده کنیم و بر اساس چارچوب خاص آن موقعیت رفتار متناسبی از آن انتظار داشته باشیم. برای نمونه میتوانیم یک کلاس User داشته باشیم و سپس کلاس Admin از این کلاس بر اساس خصوصیتهای و رفتارهای خود ارثبری کند. فرض کنید کلاس User یک متد login دارد که درون آن تعریف شده و کاربر با استفاده از آن وارد اپلیکیشن شده و نام کاربر روی صفحه نمایش مییابد. از آنجا که کلاس Admin فرزند کلاس User است، به طور خودکار این متد را به ارث میبرد.
بنابراین ما درون کلاس Admin به جای متد نمایش نام کاربر یک متد به نام Welcome Admin اضافه میکنیم که متد قبلی را باطل (Override) میکند. اینک اگر متد login را برای کاربر معمولی فراخوانی کنیم، نام وی روی صفحه نمایش مییابد و اگر آن را برای کاربر admin فراخوانی کنیم، پیام خوشامدگویی مدیر نمایش مییابد. تابع Login در چارچوبهای مختلف رفتار متفاوتی دارد و این وضعیت چندریختی نام دارد.
بدین ترتیب با مبانی برنامهنویسی شیءگرا آشنا شدیم و گرچه این بخش طولانی بود؛ اما جای نگرانی نیست چون اینک به پایان رسیده است و شما با مفاهیم بنیادی برنامهنویسی شیءگرا آشنا شدهاید. البته ممکن است قبلاً از آنها استفاده کرده باشید.
سازندهها
بدین ترتیب در بخش قبل با مفهوم شیء آشنا شدیم. شیءهایی که میسازیم چیزی به نام متد چرخه عمر دارند، یعنی متدهایی که از زمان حیات یافتن شیء آغاز میشوند. یکی از این متدها به نام متد «سازنده» (Constructors) شناخته میشود. متد سازنده متد خاصی برای یک کلاس است که مسئول مقداردهی اولیه یک شیء از آن کلاس است. نام آن همان نام کلاس است و در اغلب موارد برای تعیین مقادیر اعضا به صورت تعریف شده از سوی کاربر یا مقادیر پیشفرض استفاده میشود.
در برخی زبانهای دیگر مانند ++C مفهومی به نام deconstructor نیز داریم که به صورت خودکار زمانی که یک شیء که از سوی سازنده ایجاد شده تخریب شود، اجرا میشود. Dart متد deconstructor ندارد، زیرا از مفهومی به نام garbage collector استفاده میکند که مداوماً اجرا شده و همه اشیای ناخواسته را از حافظه پاک میکند. انواع مختلفی از متد سازنده در زبان دارت وجود دارند.
1void main(){
2 var user1 = User();
3 var user2 = User(id: 1, name: "Sharad");
4 var user3 = User.createNewOne();
5 var user4 = User.createNewOne(12, "Sharad");
6 user1._marks = 200.0; // Error because its private field
7}
8class User {
9 int id;
10 String name;
11 double _marks = 100.0;
12 User(){} // Default constructor
13 User(int id, String name){ // Parameterized constructor
14 this.id = id;
15 this.name = name;
16 }
17
18 User({this.id, this.name}); //Shortcut for param. constructor
19 User.createNewOne() {} //Named constructor
20 User.createNewOne(this.id, this.name){} //Named param con.
21 // Other methods below.
22 // Private functions
23 void _printMarks() => print("You marks: ${this._marks}");
24}
- در کد فوق id_ به این معنی است که فیلد خصوصی این کلاس است و کلاسهای دیگر نمیتوانند به آن دسترسی داشته باشند یعنی نمیتوانیم آن را از بیرون تغییر دهیم. به طور مشابه user._marks = 200.0 در ()main متدی است که تولید خطا میکند. برخی مفاهیم در مورد متدها نیز اعمال میشوند.
- سازندهها برای ارسال اطلاعات و دریافت آنها از کلاس بسیار مفید هستند. در سازنده پارامتری، میتوانیم مقادیر را دریافت کرده و به متغیرهای فیلد کلاس انتساب دهیم.
- پیش از ایجاد یک شیء از کلاس، کد درون سازنده اجرا خواهد شد.
- استفاده از هر دو سازنده پیشفرض و «نامگذاری شده» (Named) در یک کلاس مجاز نیست. ما میتوانیم هر تعداد سازنده نامگذاری شده که دوست داریم داشته باشیم.
- کلیدواژه this به تعریف متغیر فیلدی میپردازد که بخشی از یک شیء است و نه بخشی از تابع.
مفاهیم مهم شیءگرایی در دارت
در این بخش برخی از مفاهیم مهم پارادایم برنامهنویسی شیءگرا در زبان دارت را مورد بررسی قرار میدهیم.
دامنه (Scope)
دامنه در واقع به قابلیت مشاهده متغیرها گفته میشود. هر متغیر بین {...} دامنهای درون آن بلوک کد دارد که به نام دامنه بلوکی شناخته میشود. روش دامنهبندی دارت به نام «دامنهبندی واژگانی» (Lexically scoped) شناخته میشود. منظور از دامنهبندی واژگانی این است که دامنههای فرزند به اغلب متغیرهای اخیراً تعریف شده با نام مشابه دسترسی خواهند داشت. درونیترین دامنه ابتدا مورد جستجو قرار میگیرد و سپس به سمت بیرون عملیات جستجو در دامنههای بعدی تکرار میشود. شکی نیست که جهت ممانعت از بروز مشکل نباید از متغیرهای با نام مشابه استفاده کرد.
1{
2 // Search outermost block last
3 String myName = "Sharad Ghimire"";
4 {
5 // Search innermost block first
6 String myName = "Ghimire Sharad";
7 print(myName); // prints => Ghimire Sharad
8 }
9}
Getter و Setter
اینک که با دامنهبندی و متغیرهای فیلد خصوصی با استفاده از _ آشنا شدیم، نوبت آن رسیده است که با مفهوم Getter و Setter آشنا شویم. در کد زیر برخی متغیرهای خصوصی داریم که برای دسترسی از بیرون کلاس به آنها باید روش مناسبی در اختیار داشته باشیم. این روش به نام getter شناخته میشود. جهت تغییر مقادیر خصوصی نیز از متد setter استفاده میشود.
1void main(){
2 var user = User();
3 user.name = "Sharad"; // Calling setter
4 print(user.name); // Calling getter
5 print(user.age); // Prints 0
6 user.age = 10;
7 print(user.age); // Prints 100;
8}
9class User {
10 String _name;
11 String _emailId;
12 int _age = 0; //Default value
13 String get name => _name;
14 String set name(String name) => _name = name;
15 int get age => _age;
16 void set age(int age) => _age = age * 10;
17}
اعضای استاتیک
اعضای استاتیک کلاس هیچ کاری با شیء یا وهله کلاس ندارند و در میان همه وهلههای کلاس مشترک هستند. ما میتوانیم آنها را به صورت مستقیم با استفاده از نام کلاس به جای مقداردهی شیء به دست بیاوریم. متغیرهای استاتیک به نام متغیرهای کلاس و متدهای استاتیک به نام متدهای کلاس نیز شناخته میشوند. متغیرهای استاتیک دارای مقداردهی کُندی هستند، یعنی تا زمانی که در برنامه استفاده نشوند مقداردهی نمیشوند. بنابراین مصرف حافظه آنها بهینه است و به جز در موارد ضروری حافظه را اشغال نمیکنند.
1void main(){
2 var circle = Circle();
3 //circle.pi; // Error
4 //circle.calculateArea(); // Error
5 print(Circle.pi); //Directly call using class name
6 Circle.calculateArea();
7}
8class Circle {
9 static const double pi = 3.14;
10 static int maxRadius = 5;
11 String color;
12 static void calculateArea(){
13 print("Some code");
14 anotherFunction(); //Not allowed to call instance functions
15 this.color = "Some"; // Error
16 }
17 void anotherFunction(){
18 print("Some code");
19 Circle.calculateArea();
20 this.color = "Red";
21 print(pi);
22 print(Circle.maxRadius);
23 }
24}
ما از یک متد استاتیک میتوانیم تنها به متد استاتیک و متغیر استاتیک دسترسی داشته باشیم. اما نمیتوانیم به متغیرهای وهلهای نرمال و متدهای کلاس دسترسی پیدا کنیم.
وراثت
همان طور که میدانیم وراثت به سازوکاری گفته میشود که به وسیله آن یک شیء، مشخصههای کلاس والد خود را به دست میآورد. سوپرکلاس هر کلاس یک Object است که پیادهسازی پیشفرض برخی تابعهای از پیش تعریف شده مانند ()toString و getter مربوط به hasCode را ارائه میکند که به نوبه خود کد hash یک شیء را بازگشت میدهد. به طور معمول سه نوع وراثت در دارت ممکن است. «وراثت منفرد» که در آن یک کلاس از کلاس دیگر ارث میبرد، «وراثت چند سطحی» که در آن یک کلاس از کلاس دیگر ارث میبرد که آن کلاس نیز به نوبه خود از کلاس دیگری ارثبری کرده است، و «وراثت سلسلهمراتبی» که در آن دو یا چند کلاس از یک کلاس والد ارث میبرند.
به علاوه همان طور که قبلاً دیدیم با استفاده از overriding متد میتوانیم در یک کلاس فرزند، متدی را که از کلاس والد به ارث رسیده بازتعریف کنیم و کامپایلر نیز به متد override شده اولویت بالاتری میدهد.
1void main(){
2 var livingThing = LivingThing();
3 var human = Human();
4 var animal = Animal();
5 print(livingThing.canReproduce()); //"Yes they can"
6 print(human.canReproduce()); //"Yes, Human can reproduce"
7 print(animal.canReproduce());
8 //"Yes they can" and " Animal can also reproduce"
9}
10class LivingThing {
11 bool isAlive = true;
12 bool canBreadth = true;
13 void canReproduce() => print("Yes they can");
14}
15class Human extends LivingThing {
16 String name;
17 bool work = true;
18
19 // method overriding
20 void canReproduce() => print("Yes, Human can reproduce!");
21}
22class Animal extends LivingThing {
23 String breed;
24 String name;
25 void canReproduce() {
26 super.canReproduce(); // Also execute parent's method
27 print("Animal can also reproduce");
28 }
29}
- در کد فوق کلیدواژه extends برای ارثبری از کلاس والد استفاده میشود.
- کلیدواژه super در صورتی استفاده میشود که بخواهیم متد یا متغیر والد را از کلاس فرزند فراخوانی کنیم.
- در این کد متد ()canReproduce چندین بار override شده است و اگر آن متد را از شیء متفاوتی فراخوانی کنیم، به دلیل چندریختی نتیجه مختلفی دریافت میکنیم.
Mixin
Mixin زمانی استفاده میشود که میخواهیم وراثتهای چندگانه داشته باشیم. این یکی از ویژگیهای خاص دارت محسوب میشود. Mixin-ها روشی برای استفاده مجدد از کد کلاس در سلسلهمراتبهای چندگانه کلاس هستند. این یک موضوع پیشرفته است که در مقالات آتی مجله فرادرس در مورد آن بیشتر صحبت خواهیم کرد. فعلاً به کد زیر توجه کنید تا با مفاهیم مقدماتی Mixin آشنا شویم.
1main(){
2 Lizard liz = Lizard();
3 liz.test(); // Testing in Lizard, Animal, and Reptile
4 liz.crawl(); // Now we can call crawl() from Reptile class
5}
6class Animal {
7 void test() => print("Testing in Animal");
8}
9class Reptile {
10 bool canCrawl = true;
11 void crawl() => print("Crawl");
12 void test() => print("Testing in Reptile");
13}
14
15class Cat extends Animal {
16 bool hasTwoPointyEars = true;
17 void meow() => print("Meow!!");
18
19 @override
20 void test() {
21 print("Testing in Cat");
22 super.test();
23 }
24}
25class Lizard extends Animal with Reptile{
26 bool hasTail = true;
27 @override
28 void test(){
29 print("Testing in Lizard");
30 super.test();
31 }
32}
یکی از الزامات mixin این است که کلاس باید مستقل باشد. برای مثال در کد فوق کلاس Reptile چنین وضعیتی دارد و از کلاس دیگری به ارث نرسیده است. بنابراین وقتی از mixin صحبت میکنیم، یعنی میتوانیم متد کلاس دیگری را بدون ارثبری از آن مورد استفاده قرار دهیم.
اینترفیسها
یک اینترفیس به سادگی به تماس بین دو کلاس گفته میشود. یک اینترفیس زمانی استفاده میشود که یک پیادهسازی مستحکمی از همه تابعهای آن، درون کلاس فرعیاش وجود داشته باشد. این الزام وجود دارد که همه متدها در کلاس پیادهسازی override شوند. ضمناً میتوانیم کلاسهای چندگانهای را implement کنیم؛ اما نمیتوانیم در طی وراثت کلاسهای چندگانه را extend کنیم. به بیان سادهتر وقتی یک کلاس را ارثبری میکنیم، همه مشخصههای کلاس مبنا را به ارث میبریم، اما وقتی کلاسی را پیادهسازی میکنیم باید آن مشخصهها را خودمان پیادهسازی کنیم.
1void main(){
2 Manager srd = new Manager();
3 srd.printSomething();
4}
5class Worker {
6 String name = "";
7 void printSomething() => print("Worker Printing");
8}
9class Manager implements Worker {
10 String name = "Sharad";
11 void printSomething() => print("Manager Printing");
12}
تجرید
در «تجرید» (abstraction) ما در عمل یک کلاس را ایجاد نمیکنیم؛ بلکه ایده یا مفهوم آن را میسازیم. این وضعت شبیه به اینترفیس است؛ اما در عمل از آنها ارثبری میکنیم. برای ایجاد یک متد مجرد به جای بدنه متد باید از نقطهویرگول استفاده کنیم. یک متد مجرد تنها در یک کلاس مجرد میتواند تعریف شود. ما باید متدهای مجرد را در کلاسهای فرعی override کنیم. برای یک کلاس مجرد از کلیدواژه abstract برای اعلان کلاس استفاده میکنیم. این کلاسها میتوانند متدهای مجرد، متدهای معمولی و متغیرهای وهلهای داشته باشند. کلاس مجرد نمیتواند وهلهسازی شود، یعنی نمیتوان از روی آن شیئی ساخت.
1void main() {
2 var rect = Rectangle();
3 rect.draw();
4}
5
6abstract class Shape {
7 String x; // Can also define instance variable
8 void draw(); // abstract method
9 // We can also define normal functions as well
10}
11
12// Whenever we extend a abstract class, then its mendatory to override that class's abstract methods
13class Rectangle extends Shape {
14 String x = "yo!";
15 void draw() => print("Overrided!!");
16}
ژنریکها
اگر بخش اول این راهنما را مطالعه کرده باشید، پس قبلاً از «ژنریکها» (Generics) استفاده کردهاید.
1List values = [1, 2, 5]; // Dart is smart to figure out datatype
2List<String> name = new List<String>();
3name.addAll(["Sharad", "Ghimire"]);
4List<int> numbers = new List<int>();
5numbers.addAll([1, 2]);
ما لیستی داریم که یک کلاس است و این کلاس یک کلاس ژنریک است و از این رو باید در خط اول آن یک نوع به آن بدهیم که از نوع String استفاده کردهایم. در این مورد نیز از همان ساختار استفاده میکنیم؛ اما به جای آن یک نوع int ارسال میکنیم. همان طور که میبینید یک کلاس یکسان میتواند به عنوان int و همچنین String استفاده شود. این کار ژنریک نام دارد. یعنی کد میتواند انواع مختلف را بر اساس این که چه کاری باید انجام دهد مدیریت کند.
1void main(){
2 add<int>(1, 2); // prints 3
3 add<double>(1.0, 2.0); // prints 3.0
4 add<String>("Sharad", "Ghimire"); // prints SharadGhimire
5 addNumbers<String>("a", "b"); // Gives error but still work??
6 }
7void add<T>(T a, T b) { // T is shorthand for type
8 print(a + b); // But, for String, the operator + isn't defined for the class 'Object'
9}
10void addNumbers<T extends num>(T a, T b){ // num includes int and doubles so it does not work for String
11 print(a +b);
12}
در کد فوق T اختصاری برای Type یعنی نوع است. ما میتوانیم هر حرفی را در خط زیر بنویسیم:
1void add<K>(K a، K b){ }
در ادامه یک مثال از برنامهنویسی ژنریک را مشاهده میکنید. در این مثال میتوانیم مقدار int را به double تغییر دهیم و تغییری در کارکرد برنامه ایجاد نمیشود.
1main() {
2 List values = [1, 2, 4, 5];
3 print(subtract(15, values));
4}
5T subtract<T extends num>(T value, List<T> items){
6 T x = values;
7 items.forEach((values) {
8 x = x - value;
9 });
10 return x;
11}
مثالی از کلاس ژنریک
در ادامه میخواهیم یک کلاس ژنریک به نام {.. }<Counter<T extends num ایجاد کنیم که گروهی از عملیات مانند addAll و total را اجرا میکند.
1void main() {
2 Counter<double> doules = new Counter<double>();
3 doubles.addAll([1.0, 2.0, 3.0]);
4 double.total();
5}
6class Counter<T extends num> {
7 List<T> _items = new List<T>();
8 void addAll(Iterable<T> iterable) => _items.add(iterable);
9 void add(T value) => _items.add(value);
10
11 T elementAt(int index) => _items.elementAt(index);
12
13 void total() {
14 num value = 0;
15 _items.forEach((item){
16 values = values + item;
17 });
18 print(value);
19 }
20}
فایل سیستم
برای اجرای اقداماتی مانند خواندن و نوشتن فایلها باید بستهای جداگانهای به نام dart:io را ایمپورت کنیم.
1import 'dart:io';
2main(){
3 String myPath = '/';
4 Directory myDirectory = Directory(path);
5 Directory dir = Directory.systemTemp.createTempSync();
6 if(myDirectory.existsSync()){
7 print("File Exists");
8 } else {
9 print("File not found");
10 }
11 if(dir.existsSync()){
12 print("Deleting ${dir.path}");
13 dir.deleteSync();
14 } else {
15 print("Could not delete!!!");
16 }
17}
در کد فوق (/) دایرکتوری ریشه لینوکس است. در مورد ویندوز باید از c:\ استفاده کنیم.
در مورد Sync و Async که به برنامهنویسی همگام و ناهمگام مرتبط هستند در بخش بعدی این مقاله صحبت خواهیم کرد. فعلاً کافی است بدانیم که عملیات I/O میتواند هم به صورت همگام و هم ناهمگام اجرا شود. منظور از همگام این است که همه چیز به صورت یکباره رخ میدهد و ناهمگام نیز به این معنی است که کارها میتوانند به صورت هر بار یکی اجرا شوند.
()dir.existSync به این معنی است که باید مدتی منتظر بمانیم و پس از این که این دستور به پایان رسید، دستور بعدی اجرا خواهد شد.
()Directory.systemTemp.createTempSync به این معنی است که یک عضو استاتیک کلاس Directory فراخوانی شده است و از این رو باید یک دایرکتوری موقت را به صورت همگام ایجاد کنیم. در صورت وجود دایرکتوری آن را پاک میکنیم.
1import 'dart:io';
2main(){
3 Directory dir = Directory.current;
4 List<FileSystemEntity> list = dir.listSync(recursive: true);
5 print(${list.length});
6 list.forEach((FileSystemEntity value){
7 FileStat stat = value.statSync();
8 print('Path: ${value.path}');
9 print('Type: ${stat.type}');
10 print('Modified: ${stat.modified}');
11 print('Size: ${stat.size}');
12 });
13}
کد فوق کاملاً گویا است. در این کد دایرکتوری جاری را دریافت کرده و همه فایلها را به صورت بازگشتی مرتبسازی کرده و وضعیت هر فایل مانند اندازه و نوع را نمایش میدهیم.
1import 'dart:io';
2void main() {
3 Directory d = Directory.current;
4 File file = File(dir.path + '/file.txt');
5 writeToThatFile(file);
6 readThatFile(file);
7}
8void writeToThatFile(File file){
9 RandomAccessFile f = file.openSync(mode: FileMode.APPEND);
10 f.writeStringSync("Yeah I wrote that programmatically");
11 f.closeSync();
12}
13void readThatFile(File file){
14 if(file.exitsSync){
15 print("I am reading that file now");
16 print(file.readAsStringSync());
17 } else {
18 print("File not found :( ");
19 return; // returns nothing
20 }
21}
زمانی که میخواهیم چیزی را در یک فایل بنویسیم میتوانیم آن چیز را به فایل الحاق کنیم (یعنی به انتهای فایل اضافه کنیم) و یا آن را به طور کامل بنویسیم یعنی کل محتوای فایل را پاک کرده و از نو آن را بنویسیم.
RandomAccessFile به این معنی است که میتوانیم از هر جایی به صورت تصادفی به فایل دسترسی داشته باشیم. ما این فایل با دسترسی تصادفی را ایجاد میکنیم و سپس آن را به صورت ناهمگام باز میکنیم و یک حالت خاص که در اینجا APPEND است به آن میدهیم. سپس رشته مورد نظر را به آن الحاق میکنیم.
()closeSync صرفاً به منظور بستن فایل استفاده میشود. ممکن است بپرسید که چرا باید فایل را ببندیم؟ دلیل این مسئله آن است که ما اطلاعات را در یک فایل قرار دادهایم و ممکن است بیدرنگ روی دیسک نوشته نشود. از این رو باید فایل را ببندیم تا این فرایند به نوبه خود تابع ()flushSync را فراخوانی کند که همه چیز را روی دیسک مینویسد. برای خواندن اطلاعات از یک فایل میتوانیم تابع ()readAsStringSync را روی شیء آن فایل فراخوانی کنیم.
نتیجهگیری
بدین ترتیب به پایان این مقاله نسبتاً بلند میرسیم. اگر به قدر کافی تمرکز کنید و مفاهیم مطرح شده را تمرین کنید، به راحتی میتوانید بر آنها تسلط پیدا کنید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزش های برنامه نویسی
- مفاهیم مقدماتی زبان برنامه نویسی دارت (Dart) – بخش اول
- مجموعه آموزش های مهندسی نرم افزار
- آموزش گوگل فلاتر (Flutter ): ساخت اپلیکیشن دستورهای آشپزی
- مفاهیم مقدماتی فلاتر (Flutter) — به زبان ساده
- آشنایی با Garbage Collector در فلاتر (Flutter) — راهنمای پیشرفته
- مفهوم کلاس در برنامه نویسی — همراه با نمونه مثال عملی
==
بسیار عالی ممنون از توضیحات خوبتون
آینده زبان دارت چگونه پیش بینی میکنید؟