خطوة إعادة الهيكلة، قائمة تحقق البداية
إعادة الهيكلة (refactoring) مهارة تصبح، بفعل التدريب الكافي، طبيعة ثانية سهلة في معظم الحالات.
وكثيرًا ما يُخلط بين هذا النشاط وتغييرات تصميمية أكبر، لكنهما منفصلان. والتمييز بين إعادة الهيكلة وغيرها من أنشطة البرمجة مفيد لأنه يتيح لي العمل بوضوح وانضباط.
إعادة الهيكلة مقابل أنشطة أخرى
إعادة الهيكلة ليست سوى تحسين للكود الموجود دون تغيير السلوك؛ ولذلك لا ينبغي أن تضطر الاختبارات إلى التغيير.
ولهذا فهي الخطوة الثالثة في دورة التطوير الموجه بالاختبار (TDD). فبعد أن تضيف سلوكًا واختبارًا يسنده، ينبغي أن تكون إعادة الهيكلة نشاطًا لا يتطلب أي تغيير في كود اختبارك. أنت تفعل شيئًا آخر إذا كنت “تعيد هيكلة” كود ما وتضطر إلى تغيير الاختبارات في الوقت نفسه.
كثير من عمليات إعادة الهيكلة المفيدة جدًا سهلة التعلم والتنفيذ (والـ IDE لديك يؤتمت كثيرًا منها بالكامل تقريبًا)، لكنها تصبح مع الوقت بالغة الأثر في جودة نظامنا.
أنشطة أخرى، مثل التصميم “الكبير”
إذن فأنا لا أغيّر السلوك “الحقيقي”، لكن يجب أن أغيّر اختباراتي؟ ما هذا؟
لنفترض أنك تعمل على نوع (type) وتريد تحسين جودة كوده. إعادة الهيكلة لا ينبغي أن تتطلب منك تغيير الاختبارات، لذا لا يمكنك:
- تغيير السلوك
- تغيير توقيعات الـ methods
…لأن اختباراتك مرتبطة بهذين الأمرين، لكن يمكنك:
- إدخال methods وحقول خاصة، وحتى أنواع وواجهات (interfaces) جديدة
- تغيير الباطن الداخلي للـ methods العامة
وماذا لو أردت تغيير توقيع method؟
func (b BirthdayGreeter) WishHappyBirthday(age int, firstname, lastname string, email Email) {
// some fascinating emailing code
}قد تشعر أن قائمة وسائطه طويلة جدًا، وتريد إضفاء مزيد من التماسك والمعنى على الكود.
func (b BirthdayGreeter) WishHappyBirthday(person Person)حسنًا، أنت تصمّم الآن، ويجب أن تحرص على أن تتقدم بحذر. فإذا لم تفعل ذلك بانضباط، فقد تجعل كودك، والاختبار الذي يسنده، و على الأرجح الأشياء التي تعتمد عليه، في فوضى — وتذكّر أن اختباراتك ليست وحدها من تستخدم WishHappyBirthday، ونأمل أن الكود “الحقيقي” يستخدمه أيضًا!
ينبغي أن تظل قادرًا على قيادة هذا التغيير باختبار أولًا. ويمكنك أن تتشبث بالتفاصيل الدقيقة حول ما إذا كان هذا تغييرًا في “السلوك”، لكنك تريد لـ method أن يتصرف بشكل مختلف.
ولأن هذا تغيير في السلوك، فطبّق عملية الـ TDD هنا أيضًا. ومن فوائد الـ TDD أنها تمنحك طريقة بسيطة وآمنة وقابلة للتكرار لقيادة تغيير السلوك في نظامك؛ فلماذا تتخلى عنها في هذه المواقف لمجرد أنها تبدو مختلفة؟
وفي هذه الحالة، ستغيّر اختباراتك الموجودة لتستخدم النوع الجديد. والخطوات الصغيرة المتكررة التي اعتدت إجرائها مع الـ TDD لتقليل المخاطر وإضفاء الانضباط والوضوح ستساعدك في هذه المواقف أيضًا.
والأرجح أن يكون لديك عدة اختبارات تستدعي WishHappyBirthday؛ وفي هذه السيناريوهات أقترح أن تعلّق كل الاختبارات إلا واحدًا، وتقود التغيير، ثم تتعامل مع بقية الاختبارات كما تراه مناسبًا.
التصميم الكبير
قد يتطلب التصميم تغييرات أكبر ومحادثات أوسع، وفيه عادةً قدر من الذاتية. وغالبًا ما يكون تغيير تصميم أجزاء من نظامك عملية أطول من إعادة الهيكلة؛ ومع ذلك، ينبغي أن تسعى إلى تقليل المخاطر بالتفكير في كيفية إجرائه بخطوات صغيرة.
رؤية الخشب من بين الأشجار
يصبح الحديث عن مسائل التصميم “الكبير” أيسر عندما يكون الكود الأساسي جيد الصياغة. فإذا كان عليك أنت وزملائك أن تقضوا وقتًا كبيرًا في تحليل كود فوضوي ذهنيًا في كل مرة تفتحون فيها ملفًا، فما فرصتك في التفكير في تصميم الكود؟
ولهذا فإن إعادة الهيكلة المستمرة بالغة الأهمية في عملية الـ TDD. فإذا فشلنا في معالجة مشكلات التصميم الصغيرة، صعب علينا هندسة التصميم الكلي لنظامنا الأوسع.
وللأسف، يزداد الكود سيئ الصياغة سوءًا بشكل أسي كلما راكم المهندسون تعقيدًا فوق أساسات مهتزة.
قائمة تحقق ذهنية للبدء
اعتد أن تمرّ على قائمة تحقق ذهنية في كل دورة من دورات الـ TDD. وكلما أرغمت نفسك على التمرّن، صار الأمر أسهل. فهي مهارة تحتاج إلى تمرين. وتذكّر أن كل تغيير من هذه التغييرات ينبغي ألا يتطلب أي تغيير في اختباراتك.
وقد أدرجت اختصارات لـ IntelliJ/GoLand يستخدمها زملائي وأنا. وكلما درّبت مهندسًا جديدًا، أشجعه على محاولة اكتساب الذاكرة العضلية واعتياد استخدام هذه الأدوات لإعادة الهيكلة بسرعة وأمان.
تضمين المتغيرات (Inline variables)
إذا أنشأت متغيرًا لمجرد تمريره إلى method/دالة أخرى:
url := baseURL + "/user/" + id
res, err := client.Get(url)فكّر في تضمينه في مكانه (command+option+n) إلا إذا كان اسم المتغير يضيف معنى مهمًا.
res, err := client.Get(baseURL + "/user/" + id)لا تكن مفرطًا في البراعة مع التضمين؛ فالهدف ليس الوصول إلى صفر من المتغيرات والاكتفاء بأسطر واحدة سخيفة لا يستطيع أحد قراءتها. وإذا كان بإمكانك إعطاء القيمة اسمًا ذا معنى، فربما كان الأفضل تركها كما هي.
تطبيق DRY على القيم باستخراج المتغيرات
“لا تكرّر نفسك” (Don’t repeat yourself - DRY). هل تستخدم القيمة نفسها عدة مرات في دالة؟ فكّر في استخراجها وحفظها في متغير باسم ذي معنى (command+option+v).
يساعد ذلك في القراءة، ويجعل تغيير القيمة أسهل مستقبلًا، إذ لن تضطر إلى تذكّر تحديث مواضعها المتعددة.
تطبيق DRY عمومًا
DRY تحظى بسمعة سيئة هذه الأيام، ولها بعض الحق في ذلك. فـ DRY من المفاهيم التي يسهل أكثر من اللازم فهمها على المستوى السطحي، ثم يُساء تطبيقها.
فقد يذهب مهندس بالـ DRY بعيدًا أكثر من اللازم، فينشئ تجريدات محيّرة ومتشابكة لتوفير بضعة أسطر من الكود، بدلًا من الفكرة الحقيقية للـ DRY، وهي التقاط فكرة في مكان واحد. وتقليل عدد أسطر الكود غالبًا أثر جانبي للـ DRY، لكنه ليس الهدف الفعلي.
فنعم، يمكن إساءة تطبيق DRY، لكن الطرف النقيض المتطرف، أي رفض تطبيق DRY على أي شيء، شر أيضًا. فالكود المكرر يضيف ضجيجًا ويزيد تكاليف الصيانة. ورفض جمع مفاهيم أو قيم مترابطة في شيء واحد خوفًا من إساءة استخدام DRY يسبّب مشكلات مختلفة.
لذا بدلًا من التطرف في أي من الجانبين، “يجب تطبيق DRY على كل شيء” أو “DRY سيئة”، شغّل عقلك وفكّر في الكود الذي أمامك. ما المكرر؟ وهل يحتاج إلى التكرار؟ وهل تبدو قائمة الوسائط معقولة إذا غلّفت بعض الكود المكرر في method؟ وهل يبدو الكود موثّقًا لنفسه ويغلّف “الفكرة” بوضوح؟
وفي تسع حالات من كل عشر، يمكنك النظر إلى قائمة وسائط دالة، فإن بدت فوضوية ومحيّرة، فالمرجّح أنها تطبيق رديء للـ DRY.
وإذا شعرت أن جعل كود ما متوافقًا مع DRY أمر صعب، فأنت على الأرجح تزيد الأمور تعقيدًا؛ فكّر في التوقف.
طبّق DRY بعناية، لكن التمرّن عليه بكثرة سيحسّن حكمك. وأشجع زملائي على “تجربته فحسب” واستخدام التحكم في الإصدارات للعودة إلى بر الأمان إن كان خطأ.
تجربة هذه الأمور ستعلّمك أكثر من الحديث عنها، كما أن التحكم في الإصدارات مقترنًا باختبارات آلية جيدة يمنحك الإعداد الأمثل للتجريب والتعلّم.
استخراج القيم “السحرية”.
قيم فريدة ذات معنى غير مُفسَّر أو متعددة الظهور، ويمكن (ويُفضَّل) استبدالها بثوابت مُسمّاة
استخدم استخراج المتغير (command+option+v) أو الثابت (command+option+c) لإعطاء معنى للقيم السحرية. ويمكن النظر إلى هذا كعكس إعادة هيكلة التضمين. وكثيرًا ما أجد نفسي “أبدّل” الكود بين التضمين والاستخراج لأحكم على ما أعتقد أنه أسهل قراءة.
وتذكّر أن استخراج القيم المكررة يضيف أيضًا مستوى من الارتباط (coupling). فكل ما يستخدم تلك القيمة يصبح مرتبطًا بها. تأمل الكود التالي:
func main() {
api1Client := http.Client{
Timeout: 1 * time.Second,
}
api2Client := http.Client{
Timeout: 1 * time.Second,
}
api3Client := http.Client{
Timeout: 1 * time.Second,
}
//etc
}نحن نجهّز بعض عملاء HTTP لتطبيقنا. وهناك بعض القيم السحرية هنا، ويمكننا تطبيق DRY على Timeout باستخراج متغير وإعطائه اسمًا ذا معنى.

الآن يبدو الكود هكذا
func main() {
timeout := 1 * time.Second
api1Client := http.Client{
Timeout: timeout,
}
api2Client := http.Client{
Timeout: timeout,
}
api3Client := http.Client{
Timeout: timeout,
}
// etc..
}لم تعد لدينا قيمة سحرية؛ فقد أعطيناها اسمًا ذا معنى، لكننا جعلنا أيضًا العملاء الثلاثة يتشاركون المهلة نفسها. وقد يكون ذلك ما تريده؛ فعمليات إعادة الهيكلة مرتبطة بالسياق إلى حد بعيد، لكنه أمر يستحق الحذر.
وإذا كنت تجيد استخدام الـ IDE، فيمكنك تنفيذ إعادة هيكلة التضمين لتستعيد العملاء قيم Timeout منفصلة مرة أخرى.
اجعل الـ methods والدوال العامة سهلة التصفح
هل في كودك methods أو دوال عامة طويلة بشكل مفرط؟
غلّف الخطوات في methods/دوال خاصة باستخدام إعادة هيكلة استخراج الـ method (command+option+m).
في الكود أدناه طقوس مملة ومشتِّتة للانتباه حول إنشاء نص JSON وتحويله إلى io.Reader حتى نتمكن من إرساله بـ POST في طلب HTTP.
func (ws *WidgetService) CreateWidget(name string) error {
url := ws.baseURL + "/widgets"
payload := []byte(`{"name": "` + name + `"}`)
req, err := http.NewRequest(
http.MethodPost,
url,
bytes.NewBuffer(payload),
)
//todo: handle codes, err etc
}أولًا، استخدم إعادة هيكلة تضمين المتغير (command+option+n) لوضع payload داخل إنشاء الـ buffer.
func (ws *WidgetService) CreateWidget(name string) error {
url := ws.baseURL + "/widgets"
req, err := http.NewRequest(
http.MethodPost,
url,
bytes.NewBuffer([]byte(`{"name": "`+name+`"}`)),
)
// etc
}الآن يمكننا استخراج إنشاء حمولة JSON إلى دالة باستخدام إعادة هيكلة استخراج الـ method (command+option+m) لإزالة الضجيج من الـ method.
func (ws *WidgetService) CreateWidget(name string) error {
url := ws.baseURL + "/widgets"
req, err := http.NewRequest(
http.MethodPost,
url,
createWidgetPayload(name),
)
// etc
}ينبغي أن تصف الـ methods والدوال العامة ماذا تفعل، لا كيف تفعله.
كلما اضطررت إلى التفكير لأفهم ما يفعله الكود، أسأل نفسي إن كان بإمكاني إعادة هيكلته ليكون هذا الفهم أوضح على الفور
– Martin Fowler
يساعدك ذلك على فهم التصميم الكلي على نحو أفضل، ثم يتيح لك طرح أسئلة عن المسؤوليات:
لماذا يفعل هذا الـ method الأمر X؟ ألا ينبغي أن يكون ذلك في Y؟
لماذا يؤدّي هذا الـ method مهام كثيرة؟ هل يمكننا توحيد ذلك في مكان آخر؟
الدوال والـ methods الخاصة رائعة؛ فهي تتيح لك تغليف أسئلة “الكيف” غير المهمة داخل “الماذا”.
لكنني الآن لا أعرف كيف يعمل!
من الاعتراضات الشائعة على إعادة الهيكلة هذه، التي تفضّل دوالًا وmethods أصغر مركّبة من غيرها، أنها قد تصعّب فهم كيفية عمل الكود. وردّي الصريح على ذلك هو
هل تعلّمت كيف تتنقّل في قواعد الكود باستخدام أدواتك بفعالية؟
وبشكل متعمد تمامًا، بصفتي كاتب CreateWidget، لا أريد أن يكون إنشاء نص بعينه شخصية أساسية في سرد الـ method. فهو ضجيج مشتِّت وغير ذي صلة بالنسبة للقارئ في 99% من الوقت.
لكن إن كان أحدهم يهتم فعلًا، فاضغط command+b (أو أي مفتاح يقوم عندك بـ"الانتقال إلى الرمز") على createWidgetPayload … واقرأه. ثم اضغط command+left-arrow للرجوع مرة أخرى.
انقل إنشاء القيم إلى زمن البناء.
كثيرًا ما تضطر الـ methods إلى إنشاء قيم واستخدامها، مثل url في method الـ CreateWidget لدينا سابقًا.
type WidgetService struct {
baseURL string
client *http.Client
}
func NewWidgetService(baseURL string) *WidgetService {
client := http.Client{
Timeout: 10 * time.Second,
}
return &WidgetService{baseURL: baseURL, client: &client}
}
func (ws *WidgetService) CreateWidget(name string) error {
url := ws.baseURL + "/widgets"
req, err := http.NewRequest(
http.MethodPost,
url,
createWidgetPayload(name),
)
// etc
}ومن تقنيات إعادة الهيكلة التي يمكنك تطبيقها هنا أنه إذا كانت هناك قيمة تُنشأ ولا تعتمد على وسائط الـ method، فيمكنك بدلًا من ذلك إنشاء حقل في نوعك وحسابه في دالة البناء (constructor) لديك.
type WidgetService struct {
client *http.Client
createWidgetURL string
}
func NewWidgetService(baseURL string) *WidgetService {
client := http.Client{
Timeout: 10 * time.Second,
}
return &WidgetService{
createWidgetURL: baseURL + "/widgets",
client: &client,
}
}
func (ws *WidgetService) CreateWidget(name string) error {
req, err := http.NewRequest(
http.MethodPost,
ws.createWidgetURL,
createWidgetPayload(name),
)
// etc
}وبنقلها إلى زمن البناء، يمكنك تبسيط الـ methods لديك.
المقارنة والتباين في CreateWidget
نبدأ بهذا
func (ws *WidgetService) CreateWidget(name string) error {
url := ws.baseURL + "/widgets"
payload := []byte(`{"name": "` + name + `"}`)
req, err := http.NewRequest(
http.MethodPost,
url,
bytes.NewBuffer(payload),
)
// etc
}وبضع عمليات إعادة هيكلة أساسية، قادتها الأدوات الآلية بالكامل تقريبًا، انتهينا إلى
func (ws *WidgetService) CreateWidget(name string) error {
req, err := http.NewRequest(
http.MethodPost,
ws.createWidgetURL,
createWidgetPayload(name),
)
// etc
}هذا تحسين صغير، لكنه بلا شك أسهل قراءة. وإن كنت متمرّنًا جيدًا، فلن يستغرق هذا النوع من التحسين منك دقيقة واحدة تقريبًا، وما دمتَ قد طبّقت الـ TDD جيدًا، فستكون لديك شبكة أمان من الاختبارات تضمن أنك لا تكسر شيئًا. وهذه التحسينات الصغيرة المستمرة ضرورية لصحة أي قاعدة كود على المدى الطويل.
حاول إزالة التعليقات.
القاعدة الإرشادية التي نتبعها هي أننا متى شعرنا بالحاجة إلى التعليق على شيء، كتبنا method بدلًا من ذلك.
– Martin Fowler
ومرة أخرى، يمكن أن تكون إعادة هيكلة استخراج الـ method صديقتك هنا.
استثناءات القاعدة
هناك تحسينات يمكنك إجراؤها على كودك وتتطلب تغييرًا في اختباراتك، ومع ذلك يسعدني أن أضعها في سلّة “إعادة الهيكلة”، حتى وإن خالفت القاعدة.
ومن الأمثلة البسيطة إعادة تسمية رمز عام (مثل method أو نوع أو دالة) بـ shift+F6. وهذا سيغيّر بالطبع كود الإنتاج والاختبار.
لكن لأنه تغيير آلي وآمن، فخطر الدخول في دوامة كسر الاختبارات وكود الإنتاج، التي يقع فيها كثيرون مع أنواع أخرى من تغييرات التصميم، ضئيل.
ولهذا السبب، فأي تغييرات يمكنك تنفيذها بأمان عبر الـ IDE/المحرر، يسعدني أن أسميها إعادة هيكلة.
استعن بأدواتك لتتمرّن على إعادة الهيكلة.
- ينبغي أن تشغّل اختبارات الوحدة لديك في كل مرة تجري فيها تغييرًا صغيرًا من هذه التغييرات. فنحن نستثمر وقتًا في جعل كودنا قابلًا لاختبار الوحدة، وحلقة التغذية الراجعة التي لا تستغرق سوى بضعة أجزاء من الثانية من أهم فوائد ذلك؛ فاستفد منها!
- اعتمد على التحكم في الإصدارات. ولا تخجل من تجربة الأفكار. فإن كنت راضيًا، فسجّلها في commit؛ وإن لم تكن، فارجع عنها. وينبغي أن يكون ذلك مريحًا وسهلًا وغير ذي شأن كبير.
- وكلما أحسنت الاستفادة من اختبارات الوحدة والتحكم في الإصدارات، صار التمرّن على إعادة الهيكلة أسهل. ومتى أتقنت هذا الانضباط، ازدادت مهاراتك في التصميم بسرعة لأن لديك حلقة تغذية راجعة وشبكة أمان موثوقتين وفعّالتين.
- وكثيرًا جدًا ما سمعت في مسيرتي المهنية مطورين يشكون من عدم امتلاك وقت لإعادة الهيكلة؛ وللأسف، من الواضح أن الأمر يستغرق منهم وقتًا طويلًا لأنهم لا يفعلونه بانضباط — ولم يتمرّنوا عليه بما يكفي.
- ومع أن الكتابة ليست عنق الزجاجة أبدًا، ينبغي أن تكون قادرًا على استخدام أي محرر/IDE تستخدمه لإعادة الهيكلة بأمان وسرعة. فمثلًا، إذا كانت أداتك لا تتيح لك استخراج المتغيرات بضغطة مفتاح، فستفعل ذلك أقل لأنه أكثر جهدًا وخطرًا.
لا تطلب إذنًا لإعادة الهيكلة
ينبغي أن تكون إعادة الهيكلة حدثًا متكررًا في عملك، شيئًا تفعله طوال الوقت. وينبغي ألا تكون أيضًا مستنزِفة للوقت، خصوصًا إذا نُفّذت على دفعات صغيرة ومتكررة.
وإذا لم تُعِد الهيكلة، فستتضرر جودتك الداخلية، وستنخفض قدرة فريقك، ويزداد الضغط.
ولدى Martin Fowler اقتباس رائع آخر لنا.
عدا أوقات قربك الشديد من موعد نهائي، لا ينبغي أن تؤجل إعادة الهيكلة لأنك لا تملك وقتًا. وقد أظهرت التجربة في عدة مشاريع أن جولة من إعادة الهيكلة تؤدي إلى زيادة الإنتاجية. وعدم امتلاك وقت كافٍ غالبًا علامة على أنك بحاجة إلى بعض إعادة الهيكلة.
الخلاصة
ليست هذه قائمة شاملة، بل مجرد بداية. اقرأ كتاب Refactoring لـ Martin Fowler (الطبعة الثانية) لتصبح محترفًا.
ينبغي أن تكون إعادة الهيكلة سريعة وآمنة للغاية عندما تكون متمرّنًا جيدًا، فلا عذر يُذكر لعدم القيام بها. وكثيرون يرون إعادة الهيكلة قرارًا يتخذه الآخرون، لا مهارة تُتعلَّم حتى تصبح جزءًا منتظمًا من عملك.
ينبغي أن نسعى دائمًا إلى ترك الكود في حالة مثالية.
إعادة الهيكلة الجيدة تؤدي إلى كود أسهل فهمًا. وفهم الكود يعني أن اكتشاف التصاميم الأفضل يصبح أسهل. ومن الصعب جدًا العثور على تصاميم في أنظمة ذات دوال ضخمة، وكود مكرر بلا داعٍ، وتداخل عميق، وما إلى ذلك. وإعادة الهيكلة الصغيرة المتكررة ضرورية لتصميم أفضل.