کدنویسی به روش صحیح در کاتلین – از صفر تا صد
آیا تاکنون پیش آماده که در زمان خواندن یک قطعه کد با خود بگویید من مشغول خواندن یک قطعه کد نیستم بلکه یک داستان را میخوانم. کدی که به روش صحیح نوشته شده باشد، شبیه یک رمان است، چون داستانی را که کدنویس قصد داشته بازگو کند، روایت میکند و شبیه به زبان معمولی است. در این حالت کد مانند آب روان جاری میشود. هر زمان که چنین قطعه کدی را میخوانید، آگاه باشید که نویسنده کد به جای شما این زحمت را متقبل شده است. در ادامه این راهنما با کدنویسی به روش صحیح در کاتلین آشنا خواهیم شد.
هر کدنویسی باید سه اصل مهم را در خاطر خود داشته باشد: «کدی بنویس که کار کند، کد زیبا بنویس و این کار را در کمترین زمان ممکن انجام بده». یکی از مهمترین مؤلفههای زیبایی کد، روان بودن آن است. هنگامی که کدی را مینویسید یا مرور میکنید، غالباً از برخی ترجیحهای استایلبندی بهره میگیرید تا خوانا باشد. با این حال خوانایی یک قطعه کد تا حدود زیادی به ترجیحهای شما و قواعدی که در محیط برنامهنویسی برقرار است، بستگی خواهد داشت.
پیشنهادهایی که در طول این راهنما ارائه میشوند، نظر شخصی نگارنده است و ممکن است از دیدگاه عمومی به عنوان «بهترین رویه» (best practice) تلقی نشوند.
از کامنت استفاده نکنید یا کم استفاده کنید
کامنتها بخشی از کد محسوب نمیشوند. آنها از سوی کامپایلر نادیده گرفته میشوند و تنها برای خواندن کدنویسها نوشته میشوند. از این رو یک موضوع نگهداری کد محسوب میشوند و ممکن است همزمان با کد، قدیمی شوند.
کتاب «کد تمیز» نوشته Uncle Bob این موضوع را به زیبایی چنین خلاصه کرده است: «مقصود خودتان را در کد توضیح دهید». بنابراین ما باید منظورهای خودمان را در کدی که می نویسیم، تشریح کنیم و این کار از طریق نامگذاری روشن و دیگر ساختارهای اشاره شده در ادامه این راهنما صورت میگیرد که موجب افزایش خوانایی کد میشود. اگر فکر میکنید که برخی از کدها نیاز به توضیح دارند، به جای نوشتن یک کامنت، تلاش کنید کد را طوری ریفکتور کنید که خواننده کد آن توضیح شما را از خواندن خود کد به دست آورد.
به آخرین کامنتی که نوشتهاید فکر کنید. آیا مفید بوده است؟ آیا دقیق بوده است؟ آیا میتوانید کد را طوری تغییر دهید که آن اطلاعات را در خود جای دهد؟
با این حال یک استثنا در این زمینه وجود دارد. اگر یک کتابخانه مینویسیم که افراد دیگر مورد استفاده قرار خواهند داد، افزودن کامنت به API-های عرضه شده عمومی با روش مناسب میتواند برای تولید مستندات مفید باشد.
از استثناها برای نمایش رفتار غیر نرمال استفاده کنید
اگر بخواهیم رفتار غیر نرمال را نشان دهیم، باید به جای بازگشت یک مقدار خطا، یک «استثنا» (Exception) صادر کنیم. دلایل این کار به شرح زیر هستند:
- رفتار غیر نرمال بدون عوارض سپری نخواهد شد، بلکه انتشار مییابد تا این که به نخستین «دستگیره خطا» (Error Handler) برسد که عموماً آن چیزی است که میخواهیم.
- فراخوانی کننده لازم نیست به خاطر بسپارد که باید سناریوهای خطا را بررسی کند و از این رو کد قالبی کمتری برای مدیریت خطا نوشته میشود.
- بسیاری از سازندههای کتابخانه استاندارد کاتلین روشهای مناسبی برای مدیریت سیگنالرسانی مبتنی بر استثنا (مانند Result و Preconditions) ارائه کردهاند.
- کاتلین الزامی برای اعلان / مدیریت استثناهای بررسی شده مانند کاری که جاوا انجام میدهد، ندارد و از این رو این کد قالبی تکراری در کاتلین وجود ندارد.
برای سیگنالرسانی رفتار برنامه نرمال نباید از Exceptions استفاده کنیم، بلکه باید به جای آن از دو گزینه زیر بهره بگیریم:
- برای تمییز بین حالتهای پردازش از «کلاسهای مهروموم» (Sealed Classes) استفاده کنید که کارکرد مناسبی با سازنده when داشته باشد. در ادامه در این خصوص بیشتر توضیح خواهیم داد.
- برای مدیریت شکستهای قابل انتظار (مانند اعتبارسنجی ورودی کاربر) از مقادیر بازگشتی null استفاده کنید. برای نمونه میتوانید از متدهای ()toFooOrNull کاتلین بهره بگیرید.
از require/check برای اعتبارسنجی شرایطی که نباید رخ دهند بهره بگیرید
کاتلین یک مجموعه اعتبارسنجی آماده به نام Preconditions.kt دارد. به این ترتیب به جای شرط if از require یا check برای افزودن معنا به کد و کاهش حجم کد استفاده میشود.
- در صورتی که یک ورودی یا آرگومان را بررسی میکنیم، بهتر است از require استفاده کنیم. به این ترتیب یک استثنا به نام IllegalArgumentException صادر میشود.
- در مورد سناریوهای دیگر از check استفاده میکنیم که استثنایی به نام IllegalStateException ایجاد میکند.
اگر از شیء تهیپذیر کاتلین یا نوع پلتفرمی استفاده میکنیم و انتظار نداریم که در این نقطه تهی باشد، باید از یکی از موارد requireNotNull یا checkNotNull استفاده کنیم.
1require(arg.length < 10) {
2 "message"
3}
4
5val result = checkNotNull(bar(arg)) {
6 "message"
7}
8/////////////// instead of ///////////////
9if (arg.length < 10) {
10 throw IllegalArgumentException("message")
11}
12val result = bar(arg)
13 ?: throw IllegalStateException("message")
از تابعهای اکستنشن برای افزودن معنا و قابلیت زنجیرهسازی استفاده کنید
اگر قطعه کدی داشته باشیم که نیاز به یک کامنت داشته باشد، این قطعه کد باید به یک متد تبدیل شود.
1val user = getUser(id)
2validate(user)
3activate(user)
4private fun validate(user: User) {
5 // validate
6}
7private fun activate(user: User) {
8 // activate
9}
10/////////////// instead of ///////////////
11val user = getUser(id)
12/*
13 * Validate user
14 */
15// validate
16/*
17 * Activate user
18 */
19// activate
زمانی که متدهایی مانند فوق ایجاد میکنیم، در واقع نوعی معنا را کپسولهسازی کردهایم، اما میتوانیم گام را فراتر از این گذاشته و آن را به روش روانتری بنویسیم. بخش عمده کدنویسی صحیح شامل طراحی اینترفیسهای روان است و بخش عمده آن نیز قابلیت زنجیرهسازی عملیات مرتبط با هم است. اگر کلاس User را کنترل بکنیم، میتوانیم موارد validate و activate را نیز به عنوان متد اضافه کنیم، اما اگر هر دو متد تنها در این کلاس معنا داشته باشند یا اگر مالک User نباشیم، تابعهای اکستنشن به کمک ما میآیند.
در واقع به طور رسمی توصیه شده که تا حد امکان از اکستنشنها استفاده کنیم. شما میتوانید به دلخواه از تابعهای اکستنشن بهره بگیرید. هر بار که تابعی داشته باشید که به طور عمده روی یک شیء کار میکند، میتوانید آن را به یک تابع اکستنشن تبدیل کنید که آن شیء را به عنوان یک گیرنده دریافت میکند.
1private fun User.validate(): User {
2 // validate
3 return this
4}
5private fun User.activate(): User {
6 // activate
7 return this
8}
9...
10val user = getUser(id)
11 .validate()
12 .activate()
تابعهای infix را برای کاهش حجم کد ایجاد یا استفاده کنید
تابعهای «میانوندی» (infix) زیادی در کتابخانه استاندارد کاتلین وجود دارند که هدف آنها کاهش پرانتزهای تودرتو است.
1val x = mapOf(1 to "a")
2val range = 1 until 10
3val loop = listOf(...) zip listOf(...)
4/////////////// instead of ///////////////
5val x = mapOf(1.to("a"))
6val range = 1.until(10)
7val loop = listOf(...).zip(listOf(...))
ما میتوانیم تابعهای میانوندی خودمان را ایجاد کنیم. انجام این کار تنها در صورتی که شرایط زیر برقرار باشد، توصیه میشود:
- عدم وجود عوارض جانبی
- منطق ساده
- نام کوتاه
- کاربرد در جایی که وجود پرانتزهای کم مفید است.
نکته آخر فهرست فوق اهمیت زیادی دارد و سناریویی را نشان میدهد که نباید از تابعهای میانوندی استفاده کنیم. تابعهای میانوندی به دلیل عدم وجود پرانتز ممکن است به روش خوانایی زنجیرهسازی نشوند. برای نمونه اگر 2 تابع داشته باشیم که قرار است به صورت زنجیری مانند زیر استفاده شوند:
1private fun process(arg: String) = // some string
2private fun String.foo(x: String) = // some string
3val bar = process("a")
4 .foo("b")
5 .min()
اگر foo به صورت infix بود، استفاده از آن کاملاً غریب میبود، زیرا به پرانتزهای اضافی برای رسیدن به رفتار مورد نظر نیاز میداشتیم. در این حالت، بهتر است از حالت میانوندی استفاده نکنیم.
1val bar = (process("a") foo "b").min()// bad
از تابعهای دامنهای برای کاهش حجم کد بهره بگیرید
کلیدواژه with به ایجاد یک بخش کد با منطقی که به یک شیء مربوط است، کمک میکند.
1...some code...
2with(foo.id) {
3 LOGGER.info("id is $this")
4 doSomething() // method of id
5 doSomethingElse(this)
6}
7...some code...
8/////////////// instead of ///////////////
9...some code...
10val id = foo.id
11LOGGER.info("id is $id")
12id.doSomething()
13doSomethingElse(id)
14...some code...
کلیدواژه apply در زمان تعامل با اشیایی که تنها setter دارند، عملکرد عجیبی دارد.
1val foo = Foo().apply {
2 field1 = 1
3 field2 = "a"
4}
5/////////////// instead of ///////////////
6val foo = Foo()
7foo.field1 = 1
8foo.field2 = "a"
کلیدواژه also به ما امکان میدهد که از یک شیء برای افزودن جلوههای بیشتر مجدداً استفاده کنیم.
1requireNotNull(foo) {
2 "message with ${foo.id}"
3 .also { LOGGER.error(it) }
4}
5/////////////// instead of ///////////////
6requireNotNull(foo) {
7 val message = "message with ${foo.id}"
8 LOGGER.error(message)
9 message
10}
11/////////////// or worse ///////////////
12requireNotNull(foo) {
13 LOGGER.error("message with ${foo.id}")
14 "message with ${foo.id}"
15}
اطلاعات نوع را به صورت پیشفرض خاموش کنید
اینترفیس نوع کاتلین و IDE اطلاعات نوع را به عنوان یک سرنخ نمایش میدهند، بنابراین تقریباً همواره میخواهیم آن را در زمان کدنویسی حذف کنیم به جز زمانی که از سوی ساختار کد الزام شوند.
1val x = "a"
2override fun foo() = 1
3/////////////// instead of ///////////////
4val x: String = "a"
5override fun foo(): Int = 1
با این حال برخی سناریوها وجود دارند که اطلاعات نوع میتوانند مفید باشند:
- زمانی که نوع بازگشتی یک تابع یا فیلد مانند Map<Int, Map<String, String>> آن قدر پیچیده است که در یک نظر قابل تشخیص نیست.
- زمانی که نوع پلتفرم بازگشت مییابد.
از ساختار expression در صورتی استفاده کنید که تابع یک عبارت داشته باشد
تا زمانی که تابع شامل یک عبارت باشد، مهم نیست که چه طوری دارد و بهتر است از ساختار expression استفاده کنیم.
1fun foo(id: Int) = getFoo(id)
2 .chain1()
3 .chain2()
4 .chain3()
5 .chain4 {
6 // some lambda
7 }
8/////////////// instead of ///////////////
9fun foo(id: Int): Bar {
10 return getFoo(id)
11 .chain1()
12 .chain2()
13 .chain3()
14 .chain4 {
15 // some lambda
16 }
17}
این حالت در مورد تابعهای دامنه کارکرد بسیار مناسبی دارد.
1fun foo(arg: Int) = Foo().apply {
2 field1 = arg
3 field2 = "a"
4 field3 = true
5}
6/////////////// instead of ///////////////
7fun foo(arg: Int): Foo {
8 return Foo().apply {
9 field1 = arg
10 field2 = "a"
11 field3 = true
12 }
13}
تنها استثنا زمانی است که نوع بازگشتی به صورت Unit است و نباید از ساختار عبارت استفاده کنید، حتی اگر متد فراخوانی شده نیز مقدار Unit یا Void بازگشت دهد، نباید این کار را انجام دهید. به این ترتیب در یک نگاه میتوان دریافت که تابع هیچ چیزی بازگشت نمیدهد.
1fun foo() {
2 barThatReturnsUnitOrVoid()
3}
4/////////////// instead of ///////////////
5fun foo() = barThatReturnsUnitOrVoid()
از typealias یا کلاسهای درون خطی برای افزودن معنا به انواع رایج استفاده کنید
برخی انواع ژنریک هستند. در این موارد میتوانیم از typealias یا کلاسهای inline برای افزودن معنا به کد استفاده کنیم، چون در غیر این صورت درک این نوعها دشوار خواهد بود.
1typealias CustomerId = Int
2typealias PurchaseId = String
3typealias StoreName = String
4typealias Report = Map<CustomerId, Map<PurchaseId, StoreName>>
5fun(report: Report) = // ...
6/////////////// instead of ///////////////
7fun(report: Map<Int, Map<String, String>>) = // ...
کلاسهای درون خطی مشابه این هستند که یک کلاس واقعی را اعلان کنیم تا نوع اصلی را در خود قرار دهد. مزیت آن نسبت به typealias این است که گرچه اسم مستعار نوع CustomerId هر مقدار Int را قبول میکند، اما یک کلاس درون خطی CustomerId تنها CustomerId دیگر را میپذیرد.
از تگهای دقت برای تعیین دقت لفظهای عددی استفاده کنید
کاتلین دارای تگهای دقت برای تمییز بین لفظهای Double/Float و Int/Long است. از آنها برای تعیین نوع استفاده کنید.
1val x = 1L
2val y = 1.2f
3/////////////// instead of ///////////////
4val x: Long = 1
5val y: Float = 1.2
از زیرخط برای گروهبندی بصری لفظهای عددی استفاده کنید
هر جا که امکان دارد از زیرخط برای نمایش آسانتر لفظهای عددی بهره بگیرید:
1val x = 1_000_000
2/////////////// instead of ///////////////
3val x = 1000000
از قالبهای رشته و رشتههای خام استفاده کنید
قالبهای رشته (یا میانیابی رشته) نسبت به «الحاق» (concatenation)، String.format یا MessageFormat در اغلب موارد ترجیح بیشتری دارد. رشتههای خام (Raw strings) در زمان کار با متن چندخطی یا متنی با کاراکترهای خاص که در غیر این صورت نیاز به escape کردن دارند، نیز مفید واقع میشوند.
1val x = "customer $id bought ${purchases.count()} items"
2val y = """He said "I'm tired""""
3/////////////// instead of ///////////////
4val x = "customer " + id + " bought " + purchases.count() + " items"
5val y = "He said \"I'm tired\""
از عملگر Elvis برای بازگشت در صورت تهی بودن استفاده کنید
یک سناریوی رایج در زمان مدیریت نوعهای تهیپذیر، بازگشت دادن برخی مقادیر است که انتظار میرود null باشند. در این موارد باید از الگوی if-null-then-return استفاده کنیم. عملگر الویس در این حالت مفید واقع میشود.
1val user = getUser()
2 ?: return 0
3/////////////// instead of ///////////////
4val user = getUser()
5if (user == null) {
6 return 0
7}
با این حال، اگر user از یک آرگومان متد باشد چطور؟ در این حالت هیچ ()getUser برای الصاق ?: وجود ندارد. برای حفظ انسجام کد بهتر است همچنان استفاده از ?: را نسبت به if ترجیح بدهیم.
1fun foo(user: User?): Int {
2 user ?: return 0
3 // ...
4}
5/////////////// instead of ///////////////
6fun foo(user: User?): Int {
7 if(user == null) {
8 return 0
9 }
10 // ...
11}
از Stream و Sequence به طرز صحیحی استفاده کنید
زمانی که سابقه کدنویسی در جاوا را داشته باشید، ممکن است در زمان تبدیل کردن کالکشنها برحسب عادت از ()stream. استفاده کنید. اگر این کار را انجام دهید، در عمل از API استریم جاوا استفاده کردهاید. کاتلین متدهای خاص استریم خود را دارد که روی Iterable تعریف میشوند.
1listOf(1).map { ... }
2/////////////// instead of ///////////////
3listOf(1).stream().map { ... }.collect(...)
متدهای stream کاتلین چابک هستند، در حالی که متدهای جاوا کُند هستند، از این رو به جای آن میتوان از متدهای Sequence کاتلین استفاده کرد. برای مدیریت کالکشنهای بزرگ یا تبدیلهای چندمرحلهای، استفاده از متدهای Sequence موجب ایجاد عملکرد بهتر بدون قربانی کردن چندان خوانایی کد میشود.
1listOf(1).asSequence()
2 .filter { ... }
3 .map { ... }
4 .maxBy { ... }
5/////////////// instead of ///////////////
6listOf(1)
7 .filter { ... }
8 .map { ... }
9 .maxBy { ... }
از الگوی جنبه-محور برای الصاق عوارض جانبی استفاده کنید
الگوی «جنبه محور» (Aspect-Oriented) به ما امکان میدهد که عوارض جانبی را بدون افزایش حجم کد اضافه کنیم. به عنوان یک مثال ساده اگر تابعی مانند زیر داشته باشیم:
1fun getAddress(
2 customer: Customer
3): String {
4 return customer.address
5}
میتوانیم به سادگی با تعریف کردن تابع cachedBy با استفاده از این الگو و تغییر بخش کوچکی از کد، امکان کش کردن را اضافه کنیم:
1fun getAddress(
2 customer: Customer
3): String = cachedBy(customer.id) {
4 customer.address
5}
6/////////////// instead of ///////////////
7fun getAddress(
8 customer: Customer
9): String {
10 val cachedValue = cache.get(customer.id)
11 if (cachedValue != null) {
12 return cachedValue
13 }
14
15 val address = customer.address
16 cache.put(customer.id, address)
17 return address
18}
از کلاسهای مهروموم برای مدیریت حالتهای پردازش استفاده کنید
«کلاسهای مهروموم» (sealed classes) به طور کلی به ما امکان میدهند که حجم کد لازم برای ارزیابی حالتها را کاهش داده و دیگر لزومی به نگهداری از enum-های حالت اضافی وجود ندارد.
از بکتیک برای تست نامهای متد استفاده کنید
اگر نامهای طولانی برای تست متد مینویسید، غالباً بهتر است که از بکتیک برای خوانایی بیشتر استفاده کنید. بدین ترتیب امکان استفاده از فاصله و برخی کاراکترهای خاص نیز ایجاد میشود:
1fun `test foo - when foo increases by 3% - returns true`() { ... }
2/////////////// instead of ///////////////
3fun testFoo_whenFooIncreasesBy3Percent_returnsTrue() { ... }
به جای کد عمیق، کد گسترده بنویسید
این پیشنهاد خاص زبان کاتلین است، اما با توجه به مقصود ما از این راهنما که بررسی روشهای کدنویسی صحیح و روان در کاتلین است، اشاره به آن خالی از لطف نخواهد بود.
کد با عمق زیاد به کدی میگوییم که لایههای زیادی دارد , fh متدهای ناآشنا فراخوانی میکند. این یک تعریف فنی نیست، بلکه یک تعریف شناختی محسوب میَشود. به طور کلی ما «حافظه کاری» (Working Memory) محدودی داریم و زمانی که کد را میخوانیم، هر فراخوانی به متد ناآشنای دیگر موجب میشود که بر چارچوب حافظه کاری ما افزوده شود که موجب افزایش «بار شناختی» (Cognitive Load) ما میشود.
این موضوع را با بررسی یک مثال بیشتر توضیح میدهیم. فرض کنید تابع fun زیر را داریم:
1fun foo(): Foo {
2 val foo = getFoo()
3 return foo1(foo)
4}
5private fun foo1(foo: Foo): Foo {
6 ...something with foo...
7 return foo2(foo)
8}
9private fun foo2(foo: Foo): Foo {
10 ...something with foo...
11 return foo3(foo)
12}
13private fun foo3(foo: Foo): Foo {
14 ...something with foo...
15 return foo
16}
زمانی که یک فراخوانی به ()foo را میخوانیم، یک فراخوانی به ()foo1 میبینیم. اکنون ()foo را در پشت پشته حافظه کاری خود قرار میدهیم و ()foo1 را میخوانیم. به این ترتیب رفتهرفته به ()foo3 میرسیم. دوباره شروع به حرکت به سمت ()foo میکنیم. این بدان معنی است که برای درک ()foo باید آیتمهای زیادی را به حافظه کاری خود اضافه کنیم که هر آیتم به چارچوب تابعی که فراخوانی میکند، افزوده میشود.
مثال فوق را با مثال زیر که معادل گسترده آن است مقایسه کنید:
1fun foo(): Foo =
2 getFoo()
3 .foo1()
4 .foo2()
5 .foo3()
6private fun Foo.foo1(): Int {
7 ...something with this (which is foo)...
8 return this
9}
10private fun Foo.foo2(): Foo {
11 ...something with this (which is foo)...
12 return this
13}
14private fun Foo.foo3(): Foo {
15 ...something with this (which is foo)...
16 return this
17}
در رویکرد دوم، ما تنها یک چارچوب را در پشته حفظ میکنیم که ()foo است. هر بار که fun دیگری فراخوانی شود، به همان چارچوب بازمیگردد و از این رو نیازی به حرکت عمقی نداریم.
در مثال زیر نیز یک نمونه کد با لایههای زیاد را میبینید:
1return Foo(
2 bar.doSomething(
3 getId(SomeEnum.ENUM_1),
4 "string"
5 )
6)
در حالی که با کمک برخی تابعهای اکستنشن و دامنه، این کد میتواند به صورت زیر از حالت تودرتو خارج شود:
1return SomeEnum.ENUM_1
2 .getId()
3 .let {
4 bar.doSomething(it, "string")
5 }.let {
6 Foo(it)
7 }
مثالهای فوق بزرگنمایی شدهاند و این قاعده سرانگشتی در همه سناریوها کار نمیکند. با این حال، استفاده از رویکرد «کد گسترده» در اغلب موارد خوانایی بهتری به دست میدهد. این وضعیت در جاوا نیز قابل حصول است، اما کاتلین آن را کمی آسانتر ساخته است، زیرا برخی سازهها مانند تابعهای اکستنشن و تابعهای دامنه را ارائه کرده است.
به طور خلاصه کاتلین قابلیتهای سنتتیکی را ارائه میکند که به حذف حالت تودرتو در کد کمک میکنند. زمانی که با کد دارای لایههای زیاد کار میکنیم، بهتر است با برخی سازهها بازی کنید تا تعداد لایهها را کاهش دهید و رویکردی را انتخاب کنید که برای مخاطب خواناتر باشد.
در بخش فوق در مورد متدهای ناآشنا صحبت کردیم. منظور از متدهای ناآشنا، متدهایی هستند که انتظار نمیرود خواننده در یک نگاه تشخیص دهد، از این رو شامل غالب متدهای داخلی کاتلین از قبیل String.split و همچنین متدهای با کاربرد رایج در کد که گرچه ممکن است پیچیده باشند، اما به دلیل فراوانی کاربرد از سوی خواننده به سرعت تشخیص داده میشوند، نیز نمیشود.
جداسازی کد غیر روان
گاهی اوقات مهم نیست چه قدر تلاش کنیم، در نهایت یک بخش از کد ممکن است به یکی از دلایل زیر ناخوانا به نظر برسد:
- عملیات عجیبی در کد استفاده شده یا از API-های نادری استفاده شده که هیچ کدام از پیشنهادهای فوق نمیتوانند اصلاح کنند.
- برخی ملاحظات عملکردی وجود دارند که ساختار خاصی را برای کد الزام میکنند.
- همچنین ممکن است زمان کافی برای صرف کردن و اصلاح کد نداشته باشیم.
در این حالت بهترین کار این است که کد را جداسازی کنیم، به طوری که بخشهای دیگر کدبیس لزومی به تعامل مستقیم با آن نداشته باشند. صرفنظر از این که از یک کلاس مستقل یا یک متد مجزا استفاده میکنیم، باید زمانی را به ساخت امضاهای متد خوانا و معنیدار اختصار دهیم.
جداسازی کد غالباً موجب میشود که قابلیت تستپذیری کد افزایش یابد که در زمانی که کد زیرین چندان خوانا نیست، امری حیاتی است، زیرا به عنوان مستندات زنده برای رفتار مورد انتظار عمل میکند.
1... some fluent code ...
2val fee = activateCustomerAndCalculateFee(
3 userId,
4 FeeType.SIMPLE,
5 DEFAULT_CUSTOMER_TYPE
6)
7... other fluent code ...
8fun calculateFeeForCustomer(
9 userId: String,
10 feeType: FeeType,
11 customerType: Int
12): Double {
13 ... some complicated or not-so-readable code
14}
سخن پایانی
وقتی در مواردی که بین چند روش برای نوشتن یک قطعه کد مردد باشیم، باید از قاعده سرانگشتی زیر برای انتخاب یک روش استفاده کنیم:
- منظور کد را روشن کنید. در آینده با خوانش مجدد کد، به خاطر انجام این کار، از خودتان قدردانی خواهید کرد.
- کدی بنویسید که اسکن آن آسان باشد. آن سازههای کد را انتخاب کنید که خواننده را به نقطه صحیح کد هدایت کند.
- حجم کدنویسی را کاهش دهید. هر چه کد کمتری نوشته شود، کمتر موجب بر هم خوردن حواس میشود.