آموزش برنامه نویسی اندروید: اصول ۵ گانه SOLID

آبان 27, 1399| سنا عبادی
اصول سالید solid در برنامه نویسی اندروید | وبلاگ مارکت اندروید ریور

در این مقاله به پنج اصل SOLID در برنامه نویسی اندروید می پردازیم تا با استفاده از آنها بتوانید اصول و استانداردهای نگارش کد رو توی توسعه سیستم عامل اندروید رعایت کنید و پروژه ای اصطلاحا تمیز و استاندارد تولید کنید. درادامه با این مقاله با اندروید ریور همراه باشید..

واژه ی SOLID بیانگر 5 اصل مهم برای طراحی شی گرا یا object-oriented  هست که صورت کامل آنها به شرح ذیل خواهد بود :

  • Single Responsibility Principle 
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

پیش زمینه در SOLID :

SOLID در اوایل سال 2000 توسط رابرت مارتین (Uncle Bob) معرفی شد و این اصطلاح توسط Michael Feathers ابداع شد. هنگامی که این پنج اصل طراحی شی گرا با هم به کار گرفته می شوند ، آنها به توسعه دهندگان کمک می کنند تا توسعه سیستم های قابل حفظ و توسعه را تسهیل کنند.

توجه : در سایت گیت هاب پروژه ایی برای این مقاله تدارک دیدم که با مراجعه به آن می توانید ادامه ی پست را بهتر متوجه شوید !

اصل اول : The Single Responsibility Principle

اصل تک وظيفه اي با به اختصار SRP بیانگر جمله ی زیر است :

یک کلاس باید تنها یک وظیفه داشته باشد (یک کلاس باید تنها یک دلیل برای تغییر داشته باشد نه بیشتر) !

برای داشتن یک مثال بیاید کمی راجع به آداپتور RecyclerView صحبت کنیم . وظیفه ی Adapter برای ریسایکلر ویو چیست ؟! تبدیل دیتا به فرمت صحیح برای نمایش ui است.

عملی ترین قسمت کلاس آداپتور ریسایکلر ویو متد onBindViewHolder هست که باید مجموعه ای از دیتا را به فرمت مورد نیاز تبدیل کند.

 یعنی اگر این کلاس  آداپتور قرار باشید وظیفه ی دیگری با به عهده بگیرید ما اصل SRP را نقض کردیم .

مثال برای SRP :

فرض کنید این کلاس آداپتور ریسایکلر ما است :

در کد بالا اگر به متد onBindViewHolder توجه کنید متوجه میشوید که علاوه بر وظیفه ی خود , دستورات دیگری را نیز اجرا میکند :

همانطور که اشاره کردیم این متد فقط باید دیتا را هندل کند ولی در اینجا تک وظیفه ایی عمل نمی کند ! و این ناقض اصل اول سالید است .

مشکل چیست ؟!

گنجاندن مسئولیت های متعدد در یک کلاس میتواند مشکلات مختلفی ایجاد کند. ابتدا ، منطق محاسبه Order اکنون با آداپتور همراه است. اگر لازم است که کل سفارش را در جای دیگر (به احتمال زیاد انجام دهید) نمایش دهید ، باید آن منطق را تکرار کنید. پس از این اتفاق ، برنامه شما در معرض مسائل تکراری منطق نرم افزار قرار دارد که همه با آنها آشنا هستیم. شما کد را در یک مکان به روز می کنید و فراموش می کنید که آن را در یک مکان دیگر و غیره به روز کنید.

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

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

برای Refactoring این متد به این صورت عمل می کنیم یعنی عمل تبدیل ارز را از این متد خارج می کنیم :

نقل قولی از عمو باب در این رابطه را خواهیم داشت :

In the context of the Single Responsibility Principle (SRP) we define a responsibility as “a reason for change”. If you can think of more than one motive for changing a class, then that class has more than one responsibility.

چالش این است که بدانید چه موقع SRP را بکار بگیرید و چه موقع نه. با توجه به مثال آداپتور ، اگر دوباره به كد نگاه كنیم ، شاهد اتفاقات مختلفی هستیم که می تواند به دلایل مختلف نیاز به تغییر در مناطق مختلف داشته باشد:

و حتما دو دیتا کلاس Order و LineItem را به عنوان کلاس جداگانه داشته باشید.

نتیجه گیری :

A class should have only one reason to change.

یک کلاس فقط باید یک دلیل برای تغییر داشته باشد.

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

سرانجام ، با تغییر برنامه شما ، متوجه میشوید که ممکن است لازم باشد SRP را در مناطقی که قبلاً نیازی به آن نبودید ، اعمال کنید. این کاملاً خوب است و توصیه می شود.

اصل دوم : The Open/Closed Principle

اصل دوم اصول پنچ گانه سالید که به حرف O اشاره دارد , به اختصار OCP خوانده می شود .

اصل باز- بسته ; اجزای نرم افزار باید نسبت به توسعه باز (یعنی پذیرای توسعه باشد) و نسبت به اصلاح بسته باشند (یعنی پیرای اصلاح نباشد) . مثلا برای افزودن یک ویژگی جدید به نرم افزار نیاز نباشد که بعضی از قسمت های کد را بازنویسی کرد , بلکه بتوان آن ویژگی را مانند پلاگین به راحتی به نرم افزار افزود .

همچنین بخوانید :  آموزش خروجی گرفتن apk از سورس اندروید

مثال برای OCP :

مثال زیر نمونه ای از اصل Open / Closed را بیان می کند .

فرض کنیم که شما برنامه ای دارید که نیاز به محاسبه مساحت برای هر شکل خاص دارد. برنامه باید قادر به محاسبه مساحت داده شده از تمام محصولات برای یک بیمه نامه باشد.

در این مثال محاسبه منطقه را به یک کلاس با نام AreaManager انتزاعی می کنیم. کلاس AreaManager یک مسئولیت واحد دارد – محاسبه مساحت کل اشکال موجود .

بیایید فرض کنیم که هم اکنون ما فقط با محصولات مستطیل کار می کنیم بنابراین یک کلاس مستطیل داریم که این را نمایان می کند. آنچه در این کلاسها به نظر می رسد:

Rectangle.java

AreaManager.java

تا به اینجا همه چیز به نظر خوب می رسد تا اینکه قرار باشد ما شکل دیگری را برای محاسبه مساحت به کلاس AreaManager خواهیم فرستاد . کلاس Circle را به آن صورت خواهیم داشت :

Circle.java

پس باید کلاس AreaManager را به صورت زیر ویرایش کنیم :

این کد مشکل دارد !

اگر قرار باشد شکل هندسی دیگری مانند مثلث را به این پروژه اضافه کنیم , مجبور خواهیم بود کلاس AreaManager را تغییر دهیم .

این کلاس اصل Open / Closed را نقض می کند. برای اصلاح بسته نشده است و برای توسعه باز نیست. هربار که شکل جدیدی به وجود بیاید ، باید AreaManager را اصلاح کنیم. ما می خواهیم از این امر جلوگیری کنیم.

برای Refactoring این کد به این صورت عمل میکنیم :

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

اما این باعث میشود که AreaManager از تمامی کلاس های اشکال مطلع باشد درست است ؟!  پس بهترین روش استفاده از رابط ها یا Interface است .

اینترفیسی به نام Shape خواهیم داشت :

Shape.java

و کلاس های دیگر از این اینترفیس به این صورت استفاده کنند :

سپس کلاس AreaManager را به کمک OCP به این صورت می نویسم :

ما در AreaManager تغییراتی ایجاد کرده ایم که اجازه می دهد تا برای اصلاح بسته شود اما برای گسترش باز است. اگر ما نیاز به شکل جدیدی مانند هشت ضلعی داشته باشیم ، AreaManager دیگر نیازی به تغییر نخواهد داشت زیرا از طریق رابط Shape برای گسترش باز است.

نتیجه گیری :

شما می توانید کدی بنویسید که طبق اصل Open/Close  باشد. با کمی برنامه ریزی و انتزاع می توانید کدی را ایجاد کنید که به راحتی حفظ و توسعه یابد ، و نیازی به اصلاح برای اضافه کردن قابلیت جدید , ‌نداشته باشد.

اصل سوم – The Liskov Substitution Principle

حرف سوم SOLID عبارت L است ، که برای اصل جایگزینی لیسکاو است. اصل جانشینی لیسکوف توسط باربارا لیسکوف در سال 1987 در یک سخنرانی اصلی در یک کنفرانس معرفی شد. اصل جایگزینی لیسکوف موارد زیر را بیان می کند:

 اشیا یک برنامه که از یک کلاس والد هستند , باید به راحتی و بدون نیاز به تغییر در برنامه , قابل جایگزینی با کلاس والد باشند.

مثال برای LSP :

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

فرض کنیم که شما در حال نوشتن کد Android هستید که به شما امکان می دهد با نوع List در Java کار کنید:

فرض کنید کدی به این صورت داریم :

در نمونه کد بالا ، customer repository  به لیستی از customer  احتیاج دارد تا بتواند آن مشتریان را بدست آورد. مخزن مشتری فقط نیاز دارد که آن لیست از شناسه مشتری از لیست نوع <Integer> باشد. هنگامی که customer repository را کال میکنیم ، ArrayList <Integer> را مانند آن ارائه می دهیم:

یک لحظه صبر کنید … customer repository به List<Integer> نیاز دارد نه یک ArrayList<Integer> ! پس چگونه هنوز کار می کند ؟!

این اصل جایگزینی لیسکوف است. از آنجا که ArrayList <Integer> یک زیر گروه از List<Integer> است ، پس مشکلی نخواهد بود ; ما نمونه نمونه درخواستی (List <Integer>) را با نمونه ای از زیرگروه آن جایگزین می کنیم (ArrayList <Integer>).

به عبارت دیگر ، در کد بالا ، ما به انتزاع بستگی داریم (List<Integer>) ، و به همین دلیل می توانیم یک زیرگروه تهیه کنیم (ArrayList <Integer>) و برنامه همچنان بدون مشکل اجرا می شود. چرا اینطور است؟

دلیل این امر آن است که customer repository بسته به قرارداد ارائه شده توسط رابط List بستگی دارد. ArrayList اجرای رابط List است ، بنابراین ، هنگامی که برنامه اجرا می شود ، customer repository نمی بیند که نوع ArrayList است ، ,ولی به عنوان نمونه ای از List آن را استفاده می کند. بخش اصلی مقاله LSP ویکی پدیا این موضوع را خیلی خوب توضیح می دهد ، بنابراین می خواهم آن را در اینجا نقل کنم:

Liskov’s notion of a behavioral subtype defines a notion of substitutability for mutable […] objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).

به طور خلاصه ، ما می توانیم هر چیزی را که List را گسترش میدهد جایگزین کنیم و برنامه را خراب نکنیم .

همچنین بخوانید :  ویژگی های یک اپلیکیشن موفق موبایل چیست ؟

من مطمئن هستم که همچین کدی را بارها در برنامه ی خود استفاده کردید ! یعنی شما اصل لیسکوف را رعایت کردید .

نتیجه گیری :

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

اصل چهارم – The Interface Segregation Principle

اصل تفکیک (تجزیه) رابط ; استفاده از چند رابط که هر کدام , فقط یک وظیفه را بر عهده دارد بهتر از استفاده از یک رابط چند منظوره است.

درک آن بسیار آسان است. بیایید با مثالی شروع کنیم که همه در Android با آن آشنا هستیم – ویو در اندروید.

همانطور که میدانید کلاس Android View  , سوپر کلاس superclass اصلی برای تمام ویو های Android است. View ریشه و بیس TextView ، Button ، LinearLayout ، CheckBox ، Switch ، و غیره است. 

حال بیایید فرض کنیم که شما یکی از برنامه نویسان هستید که سیستم عامل Android را اواسط اواخر سال 2000 دوباره می نویسید. شما می دانید که به احتمال زیاد روی هر کامپوننتی باید کلیک شود. بنابراین ، به عنوان یک برنامه نویس خوب جاوا ، رابطی به نام OnClickListener ایجاد می کنید که در کلاس View قرار گرفته است و به این شکل ظاهر می شود:

بعد از مدتی شما نیاز به کلیک طولانی مدت برای کامپوننت را نیاز خواهید داشت پس اینترفیس بالا به این صورت بازنویسی می شود :

طبق نیاز شما نیاز به لمس اسکرین توسط کاربر پیدا خواهید کرد پس بعد از بازنویسی شما با این کد رو به رو خواهید بود :

در این لحظه تصمیم میگیرید نام این اینترفیس را به ViewInteractions یا همچین چیزی تغییر دهید چرا ؟! چرا که با اضافه کردن آخرین متد , این اینترفیس از حالت کلیک کردن خارج شده است .

با استفاده از همان OnClickListener از بالا ، تصور کنیم که ما برنامه ای را با استفاده از SDK Android ساختیم.

من می خواهم برای یک button یک click listener بنویسیم :

این دو فانکشن آخر ، onLongClick و onTouch هیچ کاری انجام نمی دهند. مطمئناً می توانیم برخی از کد ها را در آنجا قرار دهیم ، اما اگر به آن احتیاج نداشته باشیم چه می شود؟ 

رابط کاربری بسیار وابسته است زیرا به مشتری نیاز دارد تا تمام روشها را پیاده سازی کند ، حتی اگر به آنها نیازی نداشته باشد.بیایید دوباره تعریف کوتاه ISP را مرور کنیم:

Make fine grained interfaces that are client-specific.

با این کار ،شما یک رابط تمیزی نخواهید داشت چرا که به کار بردن دو فانکشن بدون نیاز به آنها مشکل ساز خواهد بود . در مثال بالا ، کاربر به onLongClick و onTouch احتیاج ندارد.

به عنوان مثال ، TextView دارای متد addTextChangedListener است. رابط TextWatcher سه متد ارائه می دهد:

نتیجه گیری :

اصل تفکیک رابط به شما در نگهداری ، به روز رسانی پروژه کمک میکند.

تصور کنید که یک اینترفیس خیلی بزرگ دارید . و اگر نیاز به تغییر داشته باشید , حالا شما باید آن را تغییر دهید. ولی به کمک اصل ISP توسعه و نگهداری کد برای شما آسان می شود .

اصل پنجم -Dependency Inversion Principle

اصل وارونگی وابستگی ; بهتر است که برنامه به انتزاع یا تجرید (abstraction) وابسته باشد نه به پیاده سازی .

در این قسمت با آخرین قاعده SOLID، یعنی Dependency Inversion آشنا می شویم. زمانی که شما مبتنی بر تکنیک های شئ گرایی برنامه می نویسید، به طور حتم، کلاس هایی خواهید داشت که وابسته به کلاس های دیگر هستند. قاعده SRP رو به یاد دارید؟ گفتیم هر کلاس باید تنها و تنها یک وظیفه خاص را انجام دهد و سایر وظایف را به کلاس های مربوطه محول کند. اما نباید ارتباط مستقیمی بین کلاس ها وجود داشته باشد! اصطلاحاً گفته میشه که باید ارتباط بین کلاس ها Loosely Coupled باشد. به مثال زیر دقت کنید: 

کلاسی داریم با نام DatabaseManager که با فراخوانی هر یک از متدهای آن، ایمیلی برای یک آدرس مشخص ارسال می شود. در کد بالا وظایف تقسیم بندی شده، یعنی قاعده SRP در نظر گرفته شده، اما ارتباطی که میان کلاس DatabaseManager و کلاس EmailNotification وجود دارد، مستقیم است. فرض کنید بخواهیم به جای ارسال رویداد بوسیله Email از پیامک استفاده کنیم، باید کلاس جدیدی تعریف شود و کلاس DatabaseManager تغییر کند تا رویدادها بوسیله پیامک ارسال شوند. اما با پیاده سازی مبتنی بر قاعده Dependency Inversion، این کار به راحتی امکان پذیر خواهد بود، برای این کار ابتدا یک interface با نام INotification تعریف می کنیم: 

حال، کلاس هر کلاسی که عملیات ارسال رویداد را انجام می دهد، می بایست interface ای که تعریف کردیم را پیاده سازی کند، در زیر دو کلاس EmailNotification و SMSNotification را تعریف میکنیم: 

حال کلاس DatbaseManager را جوری تغییر می دهیم تا وابستگی آن نسبت به یک کلاس از بین رفته و وابسته به interface تعریف شده باشد: 

با تغییر بالا، کلاس DatabaseManager هیچ وابستگی به کلاس خاصی ندارد و می توان زمان ساخت شئ از روی آن، Dependecy مربوطه را برای آن مشخص کرد: 

و در صورتی که بخواهیم از سرویس ایمیل استفاده کنیم: 

با تغییرات انجام شده، قاعده DIP  را در کد خود اعمال کردیم. 

موفق و پایدار باشید.

  تخفیف ها و اخبار ویژه رو در تلگراممون دنبال کن :)
سنا عبادی نویسنده مقاله

توسعه دهنده موبایل به ویژه سیستم عامل اندروید ، در تلاش برای تحقق یک رویا..



می تونی سنا عبادی رو توی شبکه های اجتماعی هم دنبال کنی ...

مقالات مرتبط را بخوانید :


سورس های اندروید شامل تخفیف رو ببین !

به این مقاله امتیاز دهید :
4.5/5 (2 Reviews)
  خرید سورس های حرفه ای بازی و اپلیکیشن اندروید

دسته‌ها: آموزش برنامه نویسی اندروید

دیدگاهتان را بنویسید

راهنما : برای نوشتن موارد مختلف در دیدگاه می توانید از راهنمای نگارش اندروید ریور استفاده کنید : نگارش کد کوتاه `your code`
نگارش کد بلند یا نگارش بخش عمده یک سورس کد :
[sourcecode lang="your code language"] your code here [/sourcecode]