حالت تاریک
فصل ۱۰ — بازآراییهای متد (Method Refactorings)
این فصل بازآراییهای کدی را که برای stored procedure، stored function و trigger قابل اعمال یافتهایم خلاصه میکند. برای سادگی، این سه نوع عملکرد را فقط «متد» مینامیم — اصطلاحی که Martin Fowler در کتاب Refactoring (Fowler 1999) به کار برد. همانطور که از نامش پیداست، بازآرایی متد تغییری در stored procedure است که کیفیت آن را بهبود میدهد. هدف ما ارائهٔ نمای کلی این بازآراییهاست. توضیحات مفصل را در Refactoring، اثر بنیادین این موضوع، مییابید.
بین دو دستهٔ بازآرایی متد تمایز قائل میشویم: آنهایی که رابط (interface) پایگاه دادهٔ ارائهشده به برنامههای بیرونی را تغییر میدهند و آنهایی که درون متد را تغییر میدهند. مثلاً بازآرایی Add Parameter (صفحهٔ ۲۷۸) رابط متد را تغییر میدهد، در حالی که Consolidate Conditional Expression (صفحهٔ ۲۸۳) این کار را نمیکند — مقایسهٔ پیچیده را با فراخوانی متد جدیدی که فقط مقایسه را پیادهسازی میکند جایگزین میکند. بین دو دسته تمایز قائل میشویم چون بازآراییهایی که رابط را تغییر میدهند نیاز به بازآرایی برنامههای بیرونی دارند، در حالی که آنهایی که فقط درون متد را تغییر میدهند اینطور نیستند.
۱۰.۱. بازآراییهای تغییردهندهٔ رابط (Interface Changing Refactorings)
هر یک از این بازآراییها دورهٔ گذاری (transition period) دارد تا به تیمهای توسعه زمان کافی برای بهروزرسانی برنامههای بیرونی که متدها را فراخوانی میکنند بدهد. بخشی از بازآرایی بازکار نسخهٔ اصلی متد(ها) برای فراخوانی مناسب نسخه(های) جدید است و سپس پس از دورهٔ گذار، نسخهٔ قدیمی بهطور مناسب فراخوانی میشود.
۱۰.۱.۱. Add Parameter — افزودن پارامتر
همانطور که از نامش پیداست، این بازآرایی پارامتر جدیدی به متد موجود اضافه میکند. در شکل ۱۰.۱، مثالی میبینید که MiddleName بهعنوان پارامتر سوم به ReadCustomer اضافه شده است. امنترین راه افزودن پارامتر، افزودن آن به انتهای فهرست پارامترهاست؛ به این ترتیب هنگام بازآرایی فراخوانیهای متد، خطر مرتبسازی نادرست پارامترها را ندارید. اگر بعداً تصمیم گرفتید پارامترها در ترتیب اشتباه هستند، میتوانید بازآرایی Reorder Parameters (صفحهٔ ۲۸۱) را اعمال کنید.
شکل ۱۰.۱. افزودن پارامتر.
۱۰.۱.۲. Parameterize Method — پارامتردهی متد
گاهی دو یا چند متد اساساً کار یکسانی انجام میدهند. مثلاً در شکل ۱۰.۲، stored procedureهای GetAmericanCustomers، GetCanadianCustomers و GetBrazilianCustomers بهترتیب فهرستی از مشتریان آمریکایی، کانادایی و برزیلی تولید میکنند. اگرچه این procedureهای جداگانه مزیت اختصاصی بودن را دارند و اطمینان میدهند پرسوجوهای درست روی پایگاه داده فراخوانی میشوند، نگهداری آنها سختتر از یک stored procedure واحد است. این stored procedureها با stored procedure GetCustomerByCountry جایگزین میشوند که شناسهٔ کشور را بهعنوان پارامتر میگیرد. در نتیجه کد کمتری برای نگهداری وجود دارد و افزودن پشتیبانی برای کشورهای جدید فقط با افزودن کدهای کشور جدید آسان است.
شکل ۱۰.۲. پارامتردهی stored procedure.
۱۰.۱.۳. Remove Parameter — حذف پارامتر
گاهی متدی پارامتر اضافی دارد — شاید چون در ابتدا با این باور اضافه شده که در آینده لازم خواهد بود، یا شاید چون کسبوکار تغییر کرده و دیگر لازم نیست، یا شاید مقدار پارامتر را میتوان از راه دیگری — مانند خواندن از جدول lookup — به دست آورد. در شکل ۱۰.۳، میبینید GetAccountList پارامتر اضافی AsOfDate دارد که برای انعکاس استفادهٔ واقعی stored procedure حذف میشود.
شکل ۱۰.۳. حذف پارامتر.
۱۰.۱.۴. Rename Method — تغییر نام متد
گاهی متدی نام ضعیفی دارد، یا صرفاً قراردادهای نامگذاری سازمانی شما را رعایت نمیکند. مثلاً در شکل ۱۰.۴، stored procedure GetAccountList به GetAccountsForCustomer تغییر نام میدهد تا قراردادهای نامگذاری استاندارد شما را منعکس کند.
شکل ۱۰.۴. تغییر نام stored procedure.
۱۰.۱.۵. Reorder Parameters — مرتبسازی مجدد پارامترها
رایج است کشف کنید ترتیب پارامترهای متد منطقی نیست — شاید چون پارامترهای جدید از طریق Add Parameter (صفحهٔ ۲۷۸) در ترتیب اشتباه اضافه شدهاند یا شاید چون ترتیب نیازهای کسبوکار فعلی را منعکس نمیکند. در هر صورت، میتوانید stored procedure را با مرتبسازی مناسب پارامترها آسانتر برای فهم کنید (شکل ۱۰.۵ را ببینید).
شکل ۱۰.۵. مرتبسازی مجدد پارامترهای متد.
همانطور که در شکل ۱۰.۶ میبینید، پارامترهای GetCustomers برای انعکاس قراردادهای رایج مرتبسازی کسبوکار مرتب شدند. توجه کنید این مثال بسیار پرخطر است — در این مورد نمیتوانید دورهٔ گذار داشته باشید چون نوع هر سه پارامتر یکسان است و بنابراین نمیتوانید ساده متد را override کنید. پیامد این است که میتوانید پارامترها را مرتب کنید اما فراموش کنید در کد بیرونی که متد را فراخوانی میکند آنها را مرتب کنید — متد همچنان اجرا میشود، اما با مقادیر اشتباه برای پارامترها؛ نقصی که بدون test caseهای خوب پیدا کردنش دشوار است. اگر پارامترها از انواع مختلف بودند، برنامههای بیرونی که برای ارسال پارامترها به ترتیب جدید بازآرایی نشدهاند میشکستند و یافتن کد تأثیرپذیر در نتیجه بسیار آسان میشد.
شکل ۱۰.۶. مرتبسازی مجدد پارامترهای متد بدون دورهٔ گذار.
۱۰.۱.۶. Replace Parameter with Explicit Methods — جایگزینی پارامتر با متدهای صریح
گاهی یک متد واحد یکی از چند کار را بر اساس مقدار پارامتری که به آن ارسال میشود انجام میدهد. مثلاً در شکل ۱۰.۷، میبینید GetAccountValue — که اساساً getter عمومی است و مقدار ستونی در یک ردیف را برمیگرداند — با stored procedureهای مشخصتر GetAccountBalance، GetAccountCustomerID و GetAccountOpeningDate جایگزین میشود.
شکل ۱۰.۷. جایگزینی پارامتر با stored procedureهای صریح.
۱۰.۲. بازآراییهای درونی (Internal Refactorings)
بازآراییهای درونی کیفیت پیادهسازی متد را بدون تغییر رابط آن بهبود میدهند.
۱۰.۲.۱. Consolidate Conditional Expression — ادغام عبارت شرطی
درون منطق متد، اغلب سریای از آزمونهای شرطی دارید که نتیجهٔ یکسانی تولید میکنند. باید کد را بازآرایی کنید تا آن شرطها را ترکیب کنید و کد واضحتر شود، همانطور که در مثال زیر میبینید که سه شرط در یکی ادغام شدهاند. این بازآرایی شما را برای اعمال Extract Method (صفحهٔ ۲۸۵) برای ایجاد متدی که شرط را پیادهسازی میکند آماده میکند:
sql
-- Before code
CREATE OR REPLACE FUNCTION GetAccountAverageBalance
( inAccountID IN NUMBER)
RETURN NUMBER;
AS
averageBalance := 0;
BEGIN
IF inAccountID = 10000 THEN
RETURN 0;
END IF;
IF inAccountID = 123456 THEN
RETURN 0;
END IF;
IF inAccountID = 987654 THEN
RETURN 0;
END IF;
Code to calculate the average balance
RETURN averageBalance;
END;
-- After code
CREATE OR REPLACE FUNCTION GetAccountAverageBalance
( inAccountID IN NUMBER)
RETURN NUMBER;
AS
averageBalance := 0;
BEGIN
IF inAccountID = 10000 || inAccountID = 123456 || inAccountID = 987654 THEN
RETURN 0;
END IF;
Code to calculate the average balance
RETURN averageBalance;
END;۱۰.۲.۲. Decompose Conditional — تجزیهٔ شرط
عبارت شرطی IF-THEN-ELSE پیچیدهای دارید که در نتیجه فهم آن دشوار است. شرط، بخش then و بخش else را با فراخوانی متدهایی که منطق عبارت را پیادهسازی میکنند جایگزین کنید. در نسخهٔ اول کد زیر، منطق تعیین نرخ بهرهٔ اعمالی بر اساس اینکه مقدار inBalance زیر، درون یا بالاتر از آستانهٔ مشخص است تعیین میشود. در نسخهٔ دوم، کد با افزودن فراخوانیهای BalanceIsSufficient، CalculateLowInterest و CalculateHighInterest ساده شده است. (این توابع نشان داده نشدهاند.)
sql
-- Before code
CREATE OR REPLACE FUNCTION CalculateInterest
( inBalance IN NUMBER )
RETURN NUMBER;
AS
lowBalance NUMBER;
highBalance NUMBER;
lowInterestRate NUMBER;
highInterestRate NUMBER;
BEGIN
lowBalance := GetLowBalance();
highBalance := GetHighBalance();
lowInterestRate := GetLowInterestRate();
highInterestRate := GetHighInterestRate();
IF inBalance < lowBalance THEN
RETURN 0;
END IF
IF inBalance >= lowBalance && inBalance <= highBalance THEN
RETURN inBalance * lowInterestRate;
ELSE
RETURN inBalance * highInterestRate;
END IF;
END;
-- After code
CREATE OR REPLACE FUNCTION CalculateInterest
( inBalance IN NUMBER )
RETURN NUMBER;
AS
BEGIN
IF BalanceIsInsufficient( inBalance ) THEN
RETURN 0;
END IF
IF IsLowInterestBalance( inBalance ) THEN
RETURN CalculateLowInterest( inBalance );
ELSE
RETURN CalculateHighInterest( inBalance );
END IF;
END;۱۰.۲.۳. Extract Method — استخراج متد
درون متد موجود، قطعهٔ کدی دارید که میتوان آن را گروهبندی و به متد خودش استخراج کرد — نامی که توضیح میدهد چه میکند. قطعهٔ کد معمولاً در جای دیگر تکرار شده و/یا عملکرد معناداری پیادهسازی میکند؛ بنابراین میتوانید این بازآرایی را برای سادهسازی کد اعمال کنید. عملیاتهای کوتاه و خوشنام آسانتر برای فهم و در نتیجه نگهداری هستند و احتمال بیشتری برای استفادهٔ مجدد دارند. همانطور که در کد زیر میبینید، نسخهٔ اول با جایگزینی منطق تعیین موجودی روزانهٔ آغازین با فراخوانی stored function GetDailyBalance ساده شده است. در نسخهٔ نهایی کد، این بازآرایی چند بار دیگر برای سادهسازی بیشتر اعمال شده است. اثر جانبی اعمال این بازآرایی این است که stored procedureهای سطحبالا و عمومی شما مانند کد خوشنویس و بسیار خوانا خوانده میشوند:
sql
-- Initial version of the code
CREATE OR REPLACE FUNCTION CalculateAccountInterest
( inAccountID IN NUMBER,
inStart IN DATE,
inEnd IN DATE )
RETURN NUMBER;
AS
medianBalance NUMBER;
startBalance NUMBER;
endBalance NUMBER;
interest := 0;
BEGIN
BEGIN
Determine the starting balance
SELECT Balance INTO startBalance
FROM DailyEndBalance
WHERE AccountID = inAccountID && PostingDate = inStart;
EXCEPTION WHEN NO_DATA_FOUND THEN
startBalance := 0;
Determine the ending balance
SELECT Balance INTO endBalance
FROM DailyEndBalance
WHERE AccountID = inAccountID && PostingDate = inEnd;
EXCEPTION WHEN NO_DATA_FOUND THEN
endBalance := 0;
END;
medianBalance := ( startBalance + endBalance ) / 2;
IF medianBalance < 0 THEN
medianBalance := 0;
END IF;
IF medianBalance >= 500 THEN
interest := medianBalance * 0.01;
END IF;
RETURN interest;
END;
-- Intermediate version of the code
CREATE OR REPLACE Function CalculateAccountInterest
( inAccountID IN NUMBER,
inStart IN DATE,
inEnd IN DATE )
RETURN NUMBER;
AS
medianBalance NUMBER;
startBalance NUMBER;
endBalance NUMBER;
interest := 0;
BEGIN
startBalance := GetDailyEndBalance ( inAccountID, inStart );
BEGIN
Determine the ending balance
SELECT Balance INTO endBalance
FROM DailyEndBalance
WHERE AccountID = inAccountID && PostingDate = inEnd;
EXCEPTION WHEN NO_DATA_FOUND THEN
endBalance := 0;
END;
medianBalance := ( startBalance + endBalance ) / 2;
IF medianBalance < 0 THEN
medianBalance := 0;
END IF;
IF medianBalance >= 500 THEN
interest := medianBalance * 0.01;
END IF;
RETURN interest;
END;
CREATE OR REPLACE Function GetDailyBalance
( inAccountID IN NUMBER,
inDate IN DATE )
RETURN NUMBER;
AS
endbalance NUMBER;
BEGIN
BEGIN
SELECT Balance INTO endBalance
FROM DailyEndBalance
WHERE AccountID = inAccountID < PostingDate = inDate;
EXCEPTION WHEN NO_DATA_FOUND THEN
endBalance := 0;
END;
RETURN endBalance;
END;
-- Final version of the code
CREATE OR REPLACE FUNCTION CalculateAccountInterest
( inAccountID IN NUMBER,
inStart IN DATE,
inEnd IN DATE )
RETURN NUMBER;
AS
medianBalance NUMBER;
startBalance NUMBER;
endBalance NUMBER;
BEGIN
startBalance := GetDailyEndBalance ( inAccountID, inStart );
endBalance:= GetDailyEndBalance ( inAccountID, inEnd );
medianBalance := CalculateMedianBalance ( startBalance, endBalance );
RETURN CalculateInterest ( medianBalance );
END;
-- Code for GetDailyEndBalance, CalculateMedianBalance, & CalculateInterest goes here۱۰.۲.۴. Introduce Variable — معرفی متغیر
کد متد شما عبارت پیچیدهای دارد که خواندنش دشوار است. میتوانید متغیرهای خوشنام در کد معرفی کنید که بخشهایی از عبارت را پیادهسازی میکنند و سپس برای تشکیل عبارت اصلی ترکیب میشوند و در نتیجه متد آسانتر برای فهم میشود. این بازآرایی معادل Introduce Explaining Variable مارتین فاولر (Fowler 1999) است. پس از صحبت با او، پیشنهاد کرد از نام سادهتر استفاده کنیم؛ در retrospect، فهمید که در ابتدا باید این بازآرایی را Introduce Variable مینامید و بیشتر فروشندگان ابزار هم از نام سادهتر استفاده میکنند:
sql
-- Before code
CREATE OR REPLACE FUNCTION DetermineAccountStatus
( inAccountID IN NUMBER,
inStart IN DATE,
inEnd IN DATE )
RETURN VARCHAR;
AS
lastAccessedDate DATE;
BEGIN
Some code to calculate lastAccessDate
IF ( inDate < lastAccessDate && outdate > lastAccessDate )
&& ( inAccountID > 10000 )
&& ( inAccountID != 123456 && inAcountID != 987654) THEN
do something
END IF;
do another thing
END;
-- After code
CREATE OR REPLACE FUNCTION DetermineAccountStatus
( inAccountID IN NUMBER,
inStart IN DATE,
inEnd IN DATE )
RETURN VARCHAR;
AS
lastAccessedDate DATE;
isBetweenDates BOOLEAN;
isValidAccountID BOOLEAN;
isNotTestAccount BOOLEAN
BEGIN
Some code to calculate lastAccessDate
isBetweenDates := inDate < lastAccessDate && outdate > lastAccessDate;
isValidAccountID := inAccountID > 100000;
isNotTestAccount := inAccountID != 123456 && inAcountID != 987654;
IF isBetweenDates && isValidAccountID &&
isNotTestAccount THEN
do something
END IF;
do another thing
END;۱۰.۲.۵. Remove Control Flag — حذف پرچم کنترل
متغیری دارید که بهعنوان پرچم کنترل برای خروج از ساختار کنترلی مانند حلقه عمل میکند. میتوانید کد را با جایگزینی استفاده از پرچم کنترل با بررسی عبارت درون ساختار کنترلی ساده کنید. همانطور که در مثال زیر میبینید، وقتی منطق controlFlag حذف میشود، کد سادهتر میشود:
sql
-- Before code
DECLARE
controlFlag := 0;
anotherVariable := 0;
BEGIN
WHILE controlFlag = 0 LOOP
Do something
IF anotherVariable > 20 THEN
controlFlag = 1;
ELSE
Do something else
END IF;
END LOOP;
END;
-- After code
DECLARE
anotherVariable := 0;
BEGIN
WHILE anotherVariable <= 20 LOOP
Do something
Do something else
END LOOP;
END;۱۰.۲.۶. Remove Middle Man — حذف واسطه
گاهی متدی فقط بهعنوان واسطه (pass through) یا «میانی» برای متدهای دیگر عمل میکند. این میتواند وقتی رخ دهد که stored procedure تغییر نام داده و سپس stored procedureای با نام اصلی معرفی شده که فقط stored procedure با نام جدید را فراخوانی میکند. همچنین میتواند وقتی رخ دهد که متوجه شوید دو stored procedure کار یکسانی انجام میدهند؛ بنابراین همانطور که میبینید، یکی از آنها بازنویسی شد تا ساده دیگری را فراخوانی کند. در هر صورت، وقتی کدی مانند مثال زیر میبینید، باید هر چیزی که AProcedure را فراخوانی میکند بازآرایی کنید تا اکنون AnotherProcedure را فراخوانی کند (و بنابراین AProcedure را حذف کنید):
sql
CREATE OR REPLACE PROCEDURE AProcedure
parameter1 IN NUMBER;
...
parameterN IN VARCHAR;
AS
BEGIN
EXECUTE AnotherProcedure ( parameter1, ..., parameterN );
END;۱۰.۲.۷. Rename Parameter — تغییر نام پارامتر
رایج است کشف کنید نام پارامتر موجود دشوار برای فهم است — یا نام پارامتر در آن زمان منطقی بود و استفادهاش در طول زمان تغییر کرده، یا از ابتدا اشتباه نامگذاری شده است. این بازآرایی نسبتاً ساده برای پیادهسازی است؛ فقط نام پارامتر را در کد منبع اصلی تغییر میدهید.
۱۰.۲.۸. Replace Literal with Table Lookup — جایگزینی مقدار ثابت با جستجوی جدول
عدد ثابت سختکدشدهای دارید که در کد متد معنای خاصی دارد و نگهداری را دشوار میکند. رویکرد بهتر ذخیرهٔ مقدار در جدول و سپس بازیابی آن از جدول در صورت نیاز است. (این جدول را میتوان برای بهبود عملکرد cache کرد.) در کد زیر، حداقل موجودی ۵۰۰ دلار در نسخهٔ جدید کد از جدول تکردیفی CorporateBusinessConstants که برای ذخیرهٔ چنین مقادیری استفاده میشود بازیابی میشود. (همچنین میتوانستیم جدول چندردیفی با تعداد محدود ستون — یکی برای هر نوع — برای ذخیرهٔ داده پیادهسازی کنیم.) سپس کد با اعمال دوبارهٔ این بازآرایی برای بهدست آوردن مقدار نرخ بهره بهبود مییابد. همچنین Extract Method (صفحهٔ ۲۸۵) را برای ادغام منطق بهدست آوردن مقادیر از جدول lookup اعمال میکنیم. این بازآرایی نسخهٔ پایگاهدادهای بازآرایی Replace Magic Number With Symbolic Constant (Fowler 1999) است:
sql
-- Initial version of the code
CREATE OR REPLACE FUNCTION CalculateInterest
( inBalance IN NUMBER )
RETURN NUMBER;
AS
interest := 0;
BEGIN
IF inBalance >= 500 THEN
interest := medianBalance * 0.01;
END IF;
RETURN interest;
END;
-- Intermediate version of the code
CREATE OR REPLACE FUNCTION CalculateInterest
( inBalance IN NUMBER )
RETURN NUMBER;
AS
interest := 0;
minimumBalance NUMBER;
BEGIN
BEGIN
SELECT MinimumBalanceForInterest INTO minimumBalance
FROM CorporateBusinessConstants
WHERE RowNumber = 1;
EXCEPTION WHEN NO_DATA_FOUND THEN
minimumBalance := 0;
END;
IF inBalance >= minimumBalance THEN
interest := medianBalance * 0.01;
END IF;
RETURN interest;
END;
-- Final version of the code
CREATE OR REPLACE FUNCTION CalculateInterest
( inBalance IN NUMBER )
RETURN NUMBER;
AS
interest := 0;
minimumBalance NUMBER;
interestRate NUMBER;
BEGIN
minimumBalance := GetMinimumBalance();
interestRate := GetInterestRate();
IF inBalance >= minimumBalance THEN
interest := medianBalance * interestRate;
END IF;
RETURN interest;
END;۱۰.۲.۹. Replace Nested Conditional with Guard Clauses — جایگزینی شرط تو در تو با بندهای نگهبان
عبارات شرطی تو در تو میتوانند دشوار برای فهم باشند. در مثال زیر، عبارتهای IF تو در تو با سریای از عبارتهای IF جداگانه جایگزین شدند تا خوانایی کد بهبود یابد:
sql
-- Before code
BEGIN
IF condition1 THEN
do something 1
ELSE
IF condition2 THEN
do something 2
ELSE
IF condition3 THEN
do something 3
END IF;
END IF;
END IF;
END;
-- After code
BEGIN
IF condition1 THEN
do something 1
RETURN;
END IF;
IF condition2 THEN
do something 2
RETURN;
END IF;
IF condition3 THEN
do something 3
RETURN;
END IF;
END;۱۰.۲.۱۰. Split Temporary Variable — تقسیم متغیر موقت
متغیر موقتی دارید که برای یک یا چند هدف درون متد استفاده میشود. این احتمالاً مانع دادهای نام معنادار به آن شده، یا شاید برای یک هدف نام معنادار دارد اما برای دیگری نه. راهحل معرفی متغیر موقت برای هر هدف است، همانطور که در کد زیر میبینید که aTemporaryVariable بهعنوان مخزن برای تبدیل مقادیر امپریال به متریک استفاده میشود:
sql
-- Before code
DECLARE
aTemporaryVariable := 0;
farenheitTemperature := 0;
lengthInInches := 0;
BEGIN
retrieve farenheitTemperature
aTemporaryVariable := (farenheitTemperature32 ) * 5 / 9;
do something
retrieve lengthInInches
aTemporaryVariable := lengthInInches * 2.54;
do something
END;
-- After code
DECLARE
celciusTemperature := 0;
farenheitTemperature := 0;
lenghtInCentimeters := 0;
lengthInInches := 0;
BEGIN
retrieve farenheitTemperature
celciusTemperature := (farenheitTemperature32 ) * 5 / 9;
do something
retrieve lengthInInches
lenghtInCentimeters := lengthInInches * 2.54;
do something
END;۱۰.۲.۱۱. Substitute Algorithm — جایگزینی الگوریتم
اگر راه واضحتری برای نوشتن منطق الگوریتمی که بهعنوان متد پیادهسازی شده پیدا کردید، باید این کار را انجام دهید. اگر نیاز به تغییر الگوریتم موجود دارید، اغلب ابتدا سادهسازی و سپس بهروزرسانی آسانتر است. چون بازنویسی الگوریتم پیچیده سخت است، ابتدا باید آن را از طریق بازآراییهای دیگر — مانند Extract Method (صفحهٔ ۲۸۵) — ساده کنید و سپس Substitute Algorithm را اعمال کنید.