راهنمای Refactor یا بازسازی کد — به زبان ساده

۴۶۷ بازدید
آخرین به‌روزرسانی: ۱۲ مهر ۱۴۰۲
زمان مطالعه: ۸ دقیقه
راهنمای Refactor یا بازسازی کد — به زبان ساده

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

فرض کنید کدی مانند زیر داریم که در حالت نامرتب قرار دارد:

1function endpoint(service, version = '') {
2  let protocol
3  let domain
4
5  if (service === 'webclient') {
6    protocol = __CLIENT_PROTOCOL__
7    domain = `${__ENV__}-${service}.${__DOMAIN__}`
8    if (__SERVER__) {
9      protocol = 'http'
10    } else if (__ENV__ === 'production') {
11      domain = `www.${__DOMAIN__}`
12    }
13  } else {
14    if (__ENV__ !== 'local') {
15      if (__SERVER__) {
16        protocol = 'http'
17        domain = `${__ENV__}-${service}`
18        domain += `.${__DOMAIN__}`
19      } else {
20        protocol = __CLIENT_PROTOCOL__
21        domain = `${__ENV__}-api.${__DOMAIN__}`
22        if (service !== 'core') {
23          domain += `/${service}`
24        }
25        if (version) {
26          domain += `/${version}`
27        }
28      }
29    } else {
30      protocol = 'http'
31
32      if (service === 'core') {
33        if (__CLIENT__) {
34          domain = `api.${__DOMAIN__}`
35        } else {
36          domain = `api.${__DOMAIN__}:80`
37        }
38      } else {
39        if (__CLIENT__) {
40          domain = `api.${__DOMAIN__}/${service}/${version}`
41        } else {
42          domain = `api.${__DOMAIN__}:80/${service}/${version}`
43        }
44      }
45    }
46  }
47
48  const url = `${protocol}://${domain}`
49
50  return url
51}
52
53export default endpoint

منطق فوق، URL مربوط به «نقاط انتهایی» (endpoints) را مشخص می‌سازد و به چند چیز بستگی دارد. این موارد شامل سرویسی که استفاده می‌شود، رندر شدن روی سرور یا کلاینت و این که در محیط توسعه یا پروداکشن قرار داریم و غیره هستند. یکی از دلایل این که این قطعه کد می‌تواند چنین آشفته باشد، این است که ممکن است فراموش کنیم که تکرار کد بسیار کم‌هزینه‌تر از تجرید نادرست است.

اما خبر خوب این است که می‌توان برخی تکنیک‌های آسان برای ساده‌سازی گزاره‌های تودرتوی if-else اعمال کرد. خبر بد این است که این قطعه کد برای کارکرد اپلیکیشن ضروری است، چون همه درخواست‌ها به آن ارسال می‌شوند و همچنین تست نشده است. در ادامه روش بازسازی این کد را بررسی می‌کنیم.

تأمین پوشش تست 100% برای کد

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

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

مزیت اصلی TDD امکان بازسازی کد بدون ترس و بدون هزینه است.

1import endpoint from 'config/endpoint'
2
3describe('endpoint.js', () => {
4  global.__DOMAIN__ = 'gousto.local'
5  let service
6
7  describe('when the service is "webclient"', () => {
8    beforeEach(() => {
9      service = 'webclient'
10    })
11
12    describe('and being in the server side', () => {
13      beforeEach(() => {
14        global.__SERVER__ = true
15        global.__ENV__ = 'whateverenv'
16      })
17
18      test('an http address with the corresponding ENV, SERVICE and DOMAIN is returned', () => {
19        const url = endpoint(service)
20        expect(url).toBe(`http://${__ENV__}-${service}.${__DOMAIN__}`)
21      })
22    })
23
24    describe('and not being in the server side', () => {
25      ...
26      describe('and the environment is production', () => {
27        ...
28        test('an https address with "www" and without the service, but with the DOMAIN is returned', () => {...})
29      })
30
31      describe('and the environment is not production', () => {
32        ...
33        test('an https address with the corresponding ENV, SERVICE and DOMAIN is returned', () => {...})
34      })
35    })
36  })
37
38  describe('when the service is not "webclient"', () => {
39    ...
40    describe('and the env is not "local"', () => {
41      ...
42      describe('and being in the server side', () => {
43        ...
44        test('an http address with the corresponding ENV, SERVICE and DOMAIN is returned', () => {...})
45      })
46
47      describe('and not being in the server side', () => {
48        ...
49        describe('and the service is core', () => {
50          ...
51          test('an https API address with the corresponding ENV and DOMAIN is returned', () => {...})
52
53          describe('and a version was passed', () => {
54            test('an https API address with the corresponding ENV, DOMAIN, SERVICE and VERSION is returned', () => {...})
55          })
56        })
57
58        describe('and a version was passed', () => {
59          test('an https API address with the corresponding ENV, DOMAIN, SERVICE and VERSION is returned', () => {...})
60        })
61
62        describe('and a version was not passed', () => {
63          test('an https API address with the corresponding ENV, DOMAIN and SERVICE is returned', () => {...})
64        })
65      })
66    })
67
68    describe('and the env is "local"', () => {
69      ...
70      describe('and the service is core', () => {
71        ...
72        describe('and being in the client side', () => {
73          ...
74          test('an http API address with the corresponding DOMAIN is returned', () => {...})
75        })
76
77        describe('and not being in the client side', () => {
78          ...
79          test('an http API address with the corresponding DOMAIN and port 80 is returned', () => {...})
80        })
81      })
82
83      describe('and the service is not core', () => {
84        ...
85        describe('and being in the client side', () => {
86          ...
87          test('an http API address with the corresponding DOMAIN, SERVICE and VERSION is returned', () => {...})
88        })
89
90        describe('and not being in the client side', () => {
91          ...
92          test('an http API address with the corresponding DOMAIN, port 80, SERVICE and VERSION is returned', () => {...})
93        })
94      })
95    })
96  })
97})

همچنین می‌خواهیم مطمئن شویم که پوشش تست 100% است، ‌بنابراین از فلگ Jest به صورت coverage– استفاده می‌کنیم که خروجی زیر را در اختیار ما قرار می‌دهد:

-------------------|----------|----------|----------|----------|-------------------|
File               |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-------------------|----------|----------|----------|----------|-------------------|
  ...              |      ... |      ... |      ... |      ... |                ...|
  endpoint.js      |      100 |      100 |      100 |      100 |                   |
  ...              |      ... |      ... |      ... |      ... |                ...|
-------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       12 passed, 12 total

کارکرد دقیق

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

const url = `${protocol}://${domain}`

بنابراین می‌توانیم کدی که پروتکل را تعیین می‌کند در تابع خاص خود قرار دهیم و آن را صرفاً یک بار فرا بخوانیم:

1const getProtocol = (service, isServerSide, environment) => {
2  if (service === 'webclient') {
3    if (isServerSide) {
4      return 'http'
5    }
6
7    return 'https'
8  } else {
9    if (environment === 'local') {
10      return 'http'
11    } else {
12      if (isServerSide) {
13        return 'http'
14      } else {
15        return 'https'
16      }
17    }
18  }
19}
20
21function endpoint(service, version = '') {
22  const protocol = getProtocol(service, __SERVER__, __ENV__)
23  
24  // Rest of the mess here
25  ...
26  
27  const url = `${protocol}://${domain}`
28
29  return url
30}
31
32export default endpoint

همین کار که در مورد ()getProtocol انجام دادیم، روی باقی بخش‌های URL نیز قابل اجرا است. هر چه کارکردها بیشتر تجزیه شوند، بخش‌های if-else بیشتر ساده‌سازی می‌شوند و امکان جداسازی بقیه موارد آسان‌تر می‌شود. از این رو توصیه ما این است که کار را از بخشی آغاز کنید که پیچیدگی کمتری دارد، ‌چون باعث می‌شود پیچیدگی بقیه بخش‌ها هم کاهش یابد.

استخراج این تابع‌ها کار چندان پیچیده‌ای نیست، اما دلیل این امر آن است که کابوس if-else را جدا کرده‌ایم. بنابراین آشفتگی زیادی وجود ندارد، اما هنوز مقداری آشفتگی برجا مانده که غیر قابل قبول است و لذا در بخش بعد آن را نیز حذف می‌کنیم.

ساده‌سازی

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

این نوع ساده‌سازی به صورت چشمی قابل اجرا است و همچنین می‌توانید از یک «جدول ارزش» (truth table) نیز کمک بگیرید. در مورد تابع ()getProtocol جدول ارزش به صورت زیر است:

Service is WebclientServer SideEnv is LocalReturns
TRUETRUETRUEhttp
TRUETRUEFALSEhttp
TRUEFALSETRUEhttps
TRUEFALSEFALSEhttps
FALSETRUETRUEhttp
FALSEFALSETRUEhttp
FALSETRUEFALSEhttp
FALSEFALSEFALSEhttps

اما آن را می‌توان کمی ساده‌تر نیز نوشت. علامت (-) به این معنی است که مقدار مربوطه اهمیتی ندارد.

Service is WebclientServer SideEnv is LocalReturns
TRUETRUE-http
TRUEFALSE-https
FALSE-TRUEhttp
FALSETRUEFALSEhttp
FALSEFALSEFALSEhttps

ما صرفاً دو مقدار قابل قبول داریم که شامل http و https است. بنابراین می‌توانیم یکی که ردیف‌های کمتری دارد (https) را انتخاب کنیم و شرایط را برای آن بررسی کنیم و سپس دیگری (http) را بازگشت دهیم:

1const getProtocol = (service, isServerSide, environment) => {
2  if (service === 'webclient' && !isServerSide) {
3    return 'https'
4  }
5  
6  if (service !== 'webclient' && !isServerSide && environment !== 'local') {
7   return 'https' 
8  }
9  
10  return 'http'
11}

این وضعیت در عمل بهتر از چیزی است که در بخش قبل داشتیم. اما می‌توانیم باز هم آن را بهبود ببخشیم. با بررسی دو شرط نخست متوجه می‌شویم که تنها زمانی https می‌گیریم که در سمت سرور نباشیم. بنابراین می‌توانیم شرط اول را برای http بنویسیم و در بقیه بخش‌های تابع نیز isServerSide را حذف کنیم.

1const getProtocol = (service, isServerSide, environment) => {
2  if (isServerSide) {
3    return 'http'
4  }
5  
6  if (service === 'webclient') {
7    return 'https'
8  }
9  
10  if (service !== 'webclient' && environment !== 'local') {
11   return 'https' 
12  }
13  
14  return 'http'
15}

همچنان جا برای بهبود وجود دارد. می‌توان شرط دوم و سوم را با هم ادغام کرد تا کوچک‌تر شوند:

1const getProtocol = (service, isServerSide, environment) => {
2  ...
3  if (service === 'webclient' || 
4      (service !== 'webclient' && environment !== 'local')) {
5    return 'https'
6  }
7  ...
8}

اما کد فوق کمی احمقانه به نظر می‌رسد. اگر سرویس webclient باشد، شرط باید True باشد. در غیر این صورت به بخش دوم OR می‌رویم، اما چرا باید اصولاً بررسی کنیم که webclient نباشد؟ اگر در سمت صحیحِ OR باشیم، در این صورت مطمئن هستیم که webclient نیست. نتیجه نهایی این فرایند کدی کاملاً ساده است، به خصوص زمانی که با کد اولیه بررسی می‌کنیم، میزان تفاوت را بهتر متوجه می‌شویم:

1const getProtocol = (service, isServerSide, environment) => {
2  ...
3  if (service === 'webclient' || 
4      (service !== 'webclient' && environment !== 'local')) {
5    return 'https'
6  }
7  ...
8}

نتیجه

به لطف استخراج کارکردها در نهایت یک تابع endpoint()‎ به دست می‌آید که نوشتن آن لذت‌بخش است:

1...
2
3function endpoint(service, version = '') {
4  const protocol = getProtocol(service, __SERVER__, __ENV__)
5  const subdomain = getSubdomain(service, __SERVER__, __ENV__)
6  const path = getPath(service, __SERVER__, __ENV__, version)
7  const port = getPort(service, __ENV__, __CLIENT__)
8
9  return `${protocol}://${subdomain}.${__DOMAIN__}${port}${path}`
10}
11
12export default endpoint

تابع get در کد فوق به قدر کافی کوچک است تا به راحتی متوجه بشوید. البته همه موارد به این سادگی نیستند، ‌اما بسیار بهتر از کد اصلی شده‌اند. فایل ما در نهایت به صورت زیر در می‌آید:

1const getProtocol = (service, isServerSide, environment) => {
2  if (isServerSide) {
3    return 'http'
4  }
5
6  if (service === 'webclient' || environment !== 'local') {
7    return 'https'
8  }
9
10  return 'http'
11}
12
13const getPath = (service, isServerSide, environment, version) => {
14  const isCore = service === 'core'
15  let path = ''
16
17  if (service === 'webclient') {
18    return path
19  }
20
21  if (environment === 'local') {
22    if (!isCore) {
23      return `/${service}/${version}`
24    }
25  }
26
27  if (isServerSide) {
28    return path
29  }
30
31  if (!isCore) {
32    path += `/${service}`
33  }
34  if (version) {
35    path += `/${version}`
36  }
37
38  return path
39}
40
41const getPort = (service, environment, isClientSide) => {
42  if (service !== 'webclient' && environment === 'local' && !isClientSide) {
43    return ':80'
44  }
45
46  return ''
47}
48
49const getSubdomain = (service, isServerSide, environment) => {
50  if (service === 'webclient') {
51    if (!isServerSide && environment === 'production') {
52      return 'www'
53    }
54
55    return `${environment}-${service}`
56  }
57
58  if (environment === 'local') {
59    return 'api'
60  }
61
62  if (isServerSide) {
63    return `${environment}-${service}`
64  }
65
66  return `${environment}-api`
67}
68
69function endpoint(service, version = '') {
70  const protocol = getProtocol(service, __SERVER__, __ENV__)
71  const subdomain = getSubdomain(service, __SERVER__, __ENV__)
72  const path = getPath(service, __SERVER__, __ENV__, version)
73  const port = getPort(service, __ENV__, __CLIENT__)
74
75  return `${protocol}://${subdomain}.${__DOMAIN__}${port}${path}`
76}
77
78export default endpoint

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

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

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
gousto-engineering-techbrunch
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *