آموزش برنامه نویسی سوئیفت (Swift): ساختار، خوانایی و اصول کدنویسی – بخش دهم
در بخش قبلی از این سری مقالات آموزش سوئیفت، در مورد پروتکلهای پایه، اکستنشنها و زیرنویسها صحبت کردیم. پروتکلها خود توان زیادی دارند و با بهره گرفتن از اکستنشنها توانایی آنها دوچندان میشود؛ اما بررسی این موضوع را به مقالههای آتی موکول میکنیم. با فرض این که شما اینک با مطالعه 9 بخش قبلی این سری مطالب آموزشی با نهادهای سطح بالا در سوئیفت آشنا هستید، در این بخش تصمیم گرفتیم در مورد ساختار کد و خوانایی آن صحبت کنیم.
اگر تاکنون به توصیههای مکرر ما در مورد تمرین کردن کدنویسی گوش کرده باشید، اینک احتمالاً توانستهاید یک برنامه نسبتاً خوب بسازید؛ اما اگر مدت زمانی از نوشتن کد بگذرد و دوباره به آن مراجعه کنید، ممکن است متوجه شوید که برخی بخشهای آن و منطقش را درک نمیکنید.
این وضعیت زمانی که تازه شروع به برنامهنویسی کردهاید، کاملاً طبیعی است. ممکن است بارها از این که کدی که 5 دقیقه پیش نوشتید را فراموش کردهاید، دچار ترس بشوید؛ اما جای نگرانی نیست، این اتفاق برای اغلب برنامه نویسان میافتد. خبر خوب این است این وضعیت در طی زمان بهبود مییابد و میزان طول کشیدن آن به میزان تمرین کدنویسی شما بستگی دارد.
برای مثال برخی برنامه نویسان به مدتی در حدود 6 ماه کدنویسی مداوم به صورت صبح تا شب نیاز دارند تا به این روند عادت کنند. بنابراین یک بار دیگر تأکید میکنیم که تمرین مداوم کدنویسی و مطالعه و آموزش، کلید حل مشکلات هستند.
ساختار کد
برخی افراد تصور میکنند که در زبان برنامهنویسی سوئیفت نکته خاصی در مورد ساختار کد نمیتوان گفت. هر کسی عادتها و سبک کدنویسی خاص خود را دارد و هیچ چیز استانداردی وجود ندارد. ما نیز قصد نداریم در این مطلب به استانداردسازی ساختار کد سوئیفت بپردازیم، بلکه میخواهیم این ساختار را روشنتر کنیم. بدین ترتیب هر آن چه را تاکنون آموختهایم کنار هم قرار میدهیم تا یک نقطه آغاز برای درک شیوه سازماندهی کد و درکم آسانتر مسائل مختلف در زمان مراجعات بعدی به کد خودمان بیابیم.
در ادامه اجزای مختلف یک اپلیکیشن را که در فایلهای سوئیفت نمایش مییابد، از دیدی سطح بالا مورد بررسی قرار میدهیم.
کلاسها، پروتکلها و اکستنشنها
این موارد به طور انحصاری نهادهایی سطح بالا هستند. منظور از سطح بالا این است که در آکولادها محصور نشدهاند و به طور مستقیم در خود فایل قرار میگیرند.
struct-ها و enum-ها
این موارد تقریباً نهادهای سطح بالا محسوب میشوند، اما آنها را میتوان درون struct-ها و enum-های دیگر نیز قرار داد.
تابعها
تابعها میتوانند شیءهای سطح بالا باشند؛ اما این وضعیت صرفاً در برنامههای کنسول توصیه میشود. در مورد برنامههای macOS ،iOS و watchOS بهتر است که همواره تابعها را درون کلاسها نگهداری کنیم.
ثابتها و متغیرها
این موارد باید در پایینترین سطح ممکن اعلان شوند و معنی این حرف آن است که اگر یک متغیر قرار است درون یک گزاره if استفاده شود، باید صرفاً درون آن اعلان شده باشد. اگر یک متغیر قرار است در کل فایل استفاده شود، آن را میتوان در ابتدای فایل اعلان کرد تا کنترل دسترسی مناسبی داشته باشد.
در ادامه مثالی ارائه میکنیم تا همه آنچه گفته شد را به وسیله روش عملی طرحبندی موارد مختلف در یک فایل نمایش دهیم. این مثال بر مبنای یک قطعه کد View Controller است که در همه فایلهای جدید کنترل نما قرار میگیرد.
1// Comments regarding author, application, date of creation
2// copyright info and a brief synopsis of what this Swift file does
3//MARK: My Global and File Constants
4public static let myGlobalConstant: String = "Place constants here"
5fileprivate var myFileConstantCounter: Int = 1
6
7
8//MARK: - Enums
9enum myEnum { }
10enum myOtherEnum {}
11
12
13//MARK: - Protocols
14protocol myProtocol {}
15protocol myOtherProtocol {}
16
17
18//MARK: - Home Details
19struct myHouse {
20 // myHouse properties and functions for later use
21}
22
23
24//MARK: - View Controller
25class ViewController: UIViewController {
26
27 //MARK: - Properties
28
29 @IBOutlet weak var myLabel: UILabel!
30
31 let myClassConstant = "Default Text"
32 let myReasoningForTheSpaces = "Spaces help separate variables logically by use, if I need to change an initial value I know where to look"
33
34 var myClassVariable: String = "I declare constants before variables, but keep them grouped together"
35 var session: URLSession = URLSession.default
36
37
38 //MARK: - View Life Cycle
39
40 override func viewDidLoad() {
41 // set up the session variable here
42 }
43
44 override func viewWillAppear() {
45 // start drawing objects needed for view here
46 // also set up animations that should load with the view
47 }
48
49
50 //MARK: - Helper Methods
51
52 func getUserInfo() {
53 ...
54 }
55
56
57 //MARK: - Private Methods
58
59 private func updateInfo() {
60 myClassVariable = "I use private methods to perform work
61 that would only be used by the class it is declared in"
62 }
63
64
65 //MARK: Actions
66
67 @IBAction func buttonPressed(_ sender: UIButton) {
68 // handle the event initiated by the user
69 }
70}
71
72
73//MARK: - Extensions
74//MARK: URLSession
75extension ViewController: URLSessionDataDelegate {
76 // ... URLSession Data Delegate Methods
77}
78
79extension ViewController: URLSessionTaskDelegate {
80 // ... URLSession Task Delegate methods
81}
82
83
84//MARK: - View Controller Protocols
85extension ViewController: myProtocol {
86 // add default implementation specific to ViewController
87}
88
89extension ViewController: myOtherProtocol {
90 // add default implementation specific to ViewController
91}
92
93
94//MARK:- Protocols
95extension myProtocol {
96 //add default implementation where used
97}
98
99extension myOtherProtocol {
100 //add default implementation where used
101}
خوانایی
خوانایی هم به منظور کمک به درک کد از سوی خود توسعهدهنده و هم افراد دیگر بسیار مهم است. شما باید نهایت تلاش خود را بکنید تا خوانایی مناسبی در همه کدهایتان داشته باشید.
تقسیم کردن همه چیز
همان طور که میبینید وقتی قرار است کارکردهای مختلفی را ایجاد کنیم، فایلهای برنامه کاملاً طولانی میشوند. بنابراین باید آنها را در یک گروه پوشه به فایلهای مختلف تقسیم کنیم. قرارداد نامگذاری که معمولاً به این منظور استفاده میشود به صورت زیر است:
ViewController+NameOfDelegate.swift
یا
ViewController+NameOfDataSource.swift
بدین ترتیب میتوانیم متوجه بشویم که چه اجزایی را باید در کدام فایلها قرار دهیم و میتوانیم مطمئن باشیم که کد صرفاً شامل آن جزئی است که قرار است باشد.
با استفاده از مثالی که در بخش فوق مطرح کردیم، در ادامه تلاش میکنیم که enum-ها را جدا کرده و آنها را در فایل مخصوص خود قرار دهیم. اگر تلاش کنیم فایلی به نام Enums.swift برای enum-های سراسری بسازیم، در ادامه میتوانیم با استفاده از //MARK: آن را به بخشهای مختلف تقسیم کنیم تا در بهروزرسانیهای بعدی به راحتی بتوانیم به آن بازگردیم. اگر enum-ها در فایل تنها به این کنترلر ویو مرتبط باشند، کنترل دسترسی را طوری تنظیم میکنیم که به صورت داخلی باشد؛ اما فایلی را تحت همان گروه به نام کلاس ViewController ایجاد میکنیم.
ما میتوانیم پروتکلها و اکستنشنهای آنها را نیز انتخاب کرده و در فایلهای جداگانه خود قرار دهیم؛ مگر این که پروتکل در مدل تعریف شده باشد و در این صورت در کلاسهای دیگر قابل استفاده نیست. اگر پروتکل دارای یک اکستنشن خاص کلاس یا struct باشد باید از //MARK: برای جداسازی منطقی آنها در فایل Protocols.swift بکنیم. در این مورد نیز فایلها را به همان ترتیبی که در مورد Enums.swift عمل کردیم، قرار میدهیم.
در ادامه struct را نیز جدا میکنیم و آن را به عنوان یک مدل به نام House در فایلی به نام House.swift قرار میدهیم. همچنین آن را در یک گروه متفاوت به نام Models قرا میدهیم، زیرا یک مدل برای خانه محسوب میشود.
کلاس و همه متدهای آن نیز میتوانند کنار هم بمانند؛ اما باید اکستنشنهای خاص کلاس را جدا کرده و در فایل جدیدی به نام ViewController+Extensions.swift قرار دهیم. در این مورد کافی است مطمئن شویم که همه متدهای ما در ViewController.swift که باید از سوی یک اکستنشن فراخوانی شوند به صورت internal تنظیم شدهاند، چون وضعیتهای private و fileprivate جلوی دیدن متدهایی که در یک اکستنشن فایل دیگر قرار دارند را میگیرند.
ثابتها وضعیت خاصی دارند. یک ترفندی که اغلب افراد استفاده میکنند، این است که یک فایل Constants.swift میسازند و همه ثابتهایی را که در سراسر اپلیکیشن استفاده خواهد شد در آن قرار میدهند. بدین ترتیب اگر یک URL که مورد نیاز اپلیکیشن است تغییر یابد، تنها کافی است آن را در یک جا تغییر دهید و بدین ترتیب مکان آن را بیدرنگ میشناسیم. در این صورت اگر از آن در 5 جای مختلف استفاده کرده باشید، این محلها میتوانند همچنان به فایل ثابتها ارجاع داشته باشند و میتوانید مطمئن باشید که همه چیز بهروزرسانی شده است. یک تغییر در برابر پنج تغییر، معامله خوبی به نظر میرسد.
1//Contants.swift
2// This file contains all constants used by my app
3struct SomeWebAPI {
4 static let myURL: String = "http://api.contoso.com"
5}
6
7// Call it later like this
8let url = URL(string: SomeWebAPI.myURL)!
9
10// or safely using
11if let url = URL(string: SomeWebAPI.myURL) {
12 // do stuff with URL
13}
استفاده از نامهای گویا برای متغیرها
اغلب افراد با توجه به سابقه کدنویسی در زبانهای C و ++C معمولاً از نامهای تکحرفی برای متغیرها استفاده میکنند که این وضعیت منجر به درهمریختگی کد و ناخوانایی آن میشود. این یک رویه نامناسب محسوب میشود و نباید از آن استفاده کرد. در طی سالها، حروف i ،j ،k ،l ،m ،n ،t ،x ،y و z چنان معانی مختلف در محیطهای برنامهنویسی داشتهاند که اینک تقریباً هیچ معنی خاصی به ذهن متبادر نمیکنند. فرض کنید برنامهای نوشتهاید که با استفاده از سرعت میانگین در طی دوره زمانی خاص، مسافت پیموده شده را با متغیرهای j ،k و m محاسبه میکند. اسامی این متغیرها هیچ سرنخی به ما نمیدهد و مگر معجزهای رخ بدهد که بتوانیم بفهمیم k به معنی متغیر سرعت است.
در واقع بدون بررسی دقیق کد و روش نوشتن فرمولها، امکان این که بفهمیم متغیر سرعت با استفاده از k مورد ارجاع قرار گرفته امکانپذیر نیست. بدین ترتیب دیگر نمیتوانیم با نگاهی سریع به یک قطعه کد متوجه شویم که سرعت کجا تعیین شده است و در این وضعیت خوانایی کد هم برای خود ما و هم برنامهنویسان دیگر کاهش مییابد.
با مراجعه به تاریخچه استفاده از متغیرهای با نام تکحرفی متوجه میشویم که دلیل استفاده برنامهنویسان از چنین نامهایی این بوده است که چیزی به نام امکان «تکمیل خودکار» (autocomplete) وجود نداشته است و توسعهدهندگان نمیخواستهاند کد زیادی را تایپ کنند. بدین ترتیب این اطمینان نیز حاصل میشده است که نام یک متغیر اشتباه درج نشده است. این وضعیت شبیه نوشتن SMS روی گوشیهای تلفن همراه در ابتدای دهه 2000 بوده است که از ترکیبهایی مانند lol ،c u l8r ، < 3 u و ttyl استفاده میشده است. اما اینک همه چیز تغییر یافته است و همه محیطهای برنامهنویسی گزینه تکمیل خودکار را دارند که موجب میشود زحمت نوشتن نامهای طولانی و گویا برای متغیرها تا حدود زیادی کاهش یابد.
البته در برخی موارد مثلاً زمانی که از متغیرهای موقت در گزارههای if استفاده میکنیم و اعلان متغیر صرفاً پنج خط با محل استفاده آن فاصله دارد، استفاده از نامهای تکحرفی و کوتاه برای متغیرها اشکالی نخواهد داشت. اما وقتی که از یک متغیر به نام a در سراسر یک کلاس یا struct استفاده میکنید، به طور جدی به خوانایی کد خودتان آسیب میزنید. این وضعیت شبیه این است که امروزه ببینیم فردی در آخرین مدل از گوشی آیفون به جای «سلام، چطوری؟» عبارت «سلم. چطری؟» را تایپ کرده باشد! همین موضوع در مورد متغیرهای سراسری تکحرفی نیز صدق میکند. زمانی که پس از 8 ماه بخواهید بخشی از کد را تغییر دهید، ابتدا باید بررسی کنید که این متغیر به چه منظور نوشته شده است تا بتوانید آن را تغییر دهید و گاهی حتی بخش اول به زمان و انرژی بیشتری نسبت به بخش دوم نیاز خواهد داشت.
اگر همه گفتههای فوق را هم فراموش کردید، صرفاً این نکته را همواره به خاطر داشته باشید که امروزه در زبان برنامهنویسی سوئیفت باید از نامهای گویایی برای متغیرها و ثابتها استفاده کنید. ما در این سری مطالب آموزش سوئیفت از نخستین قسمت، همه نامهای متغیرها را به صورت طولانی و حتی بدون دسترسی به گزینه تکمیل خودکار با کمی کپی و چسباندن نوشتهایم. با استفاده از گزینه تکمیل خودکار این کار به زحمت چندانی نیاز نخواهد داشت.
فاصله خالی
رعایت فاصله خالی همواره به منظور افزایش خوانایی کد مفید است. زمانی که تلاش میکنید همه چیز را بهم بچسبانید وقتی متعاقبا بخواهید آنرا بخوانید دچار مشکل خواهید شد. در عوض میتوانید از فاصله خالی استفاده کنید. این فواصل موجب میشوند که مشکل خوانایی کد کاهش یابد و کد به بخشهای فرعی منطقی تقسیم شود. همان طور که در مثال کد فوق دیدیم، ما از فواصل خالی برای معنیدار ساختن کد خود استفاده کردهایم.
- ما با استفاده از دو خط خالی روی استفاده از فواصل خالی بین هر نشانه تأکید کردهایم.
- بین هر تابع یک جداسازی قرار دادهایم.
- از فاصله خالی بین متغیرهایی که نقشهای متفاوتی در کد دارند استفاده کردهایم.
- فاصله خالی بین اعلان کلاس و تابع و همچنین آکولاد آغازین هر یک از بدنههای مرتبط قرار دادهایم.
- از فاصله خالی پیش از انتهای هر کلاس به عنوان سرنخ ظریفی برای این که بدنه کلاس پایان مییابد استفاده کردهایم.
همه این موارد به افزایش خوانایی کد کمک میکنند. «دیوید هاینمایر هنسن» (David Heinemeier Hansson) خالق «روبی آن ریلز» (Ruby on Rails) یک سخنرانی در RailsConf دارد که در آن اشاره میکند برنامهنویسان بسیار کمی دانشمند علوم کامپیوتر هستند. در عوض بهتر است برنامه نویسان را نویسندگان نرمافزار بدانیم، زیرا این همان کاری است که انجام میدهیم. ما نرمافزار را مینویسیم. شما ممکن است با این گفته موافق نباشید و همچنان به عنوان «دانشمند کامپیوتر» علاقه داشته باشید. مشکلی وجود ندارد؛ اما پیشنهاد میکنیم ویدئوی این سخنرانی (+) را ببینید و سپس در این مورد تصمیمگیری کنید. در ادامه ویدئو در مورد «توسعه مبتنی بر تست» (TDD) صحبت میشود که نشانه خوبی برای اشاره به آن در بخشهای آتی این سری مقالات آموزشی است؛ اما این کار را به بخشهای انتهایی این دوره آموزشی موکول میکنیم.
نکته بعدی این است که باید همه تابعها را در کلاس ابتدا بر اساس «پدیداری» (Visibility) و سپس بر اساس ترتیبی که فراخوانی میشوند قرار دهید. متأسفانه در صورتی که کد شما درون یک کلاس کپسولهسازی نشده باشد، لازم خواهد بود که همه تابعها را پیش از کدی که هر تابع مورد ارجاع قرار میگیرد قرار دهید. بهترین راهنمایی به این منظور آن است که تابعهای خود را در ابتدا قرار دهید و در این مورد نیز تابعهایی که به تابعهای دیگر وابسته نیستند را ابتدا بنویسید و سپس تابعهای دیگر را در ادامه آنها قرار دهید. در واقع اگر تابع a تابع b را فراخوانی میکند، و تابع b به تابع c و d وابسته است، ترتیب آنها از بالا به پایین باید به صورت c ،d ،b و a باشد.
آخرین نکته در مورد خوانایی این است که کدهای مرتبط با هم را باید در مجاورت هم بنویسیم. این بدان معنی نیست که باید همه کدهایی که به هم مرتبط هستند را به صورت پشت سر هم اجرا کنیم؛ بلکه این گفته بدان معنی است که اگر از دادههایی از وب استفاده میکنید، همه متدهای شبکه خود را کنار هم قرار دهید، همه متغیرهای با موضوع وب نیز باید در کنار هم باشند و همه منطق مورد نیاز برای بازیابی دادهها با استفاده از متغیرها باید در مجاورت هم واقع شده باشند. حتی اگر متغیرهایی در ابتدا هستند، منطق در میانه فایل قرار دارد و کد وبسرویس در انتهای فایل نوشته شده، باز هم باید حالتی که ذکر کردیم وجود داشته باشد.
اصول
ما در بخشهای قبلی (+) این مقالات آموزشی در مورد اصول DRY (عدم تکرار کد) جداسازی دغدغهها، و اصل مسئولیت منفرد صحبت کردیم. در این بخش این اصول را بیشتر توضیح میدهیم و به موارد دیگری نیز اشاره میکنیم.