مقدمة في إعادة هيكلة الكود القديم (Legacy Code) بلغة Go
العمل مع الكود القديم (legacy code) هو جزء لا مفر منه في حياة أي مطور Go. بمرور الوقت، تتراكم الديون التقنية في المشاريع: تظهر دوال ضخمة، تكرار في الكود، غياب الاختبارات ومعالجة الأخطاء. إعادة هيكلة الكود القديم بلغة Go ليست مجرد إعادة كتابة، بل هي عملية استراتيجية لتحسين البنية دون تغيير السلوك الخارجي للنظام.
في هذه المقالة، سنستعرض الأساليب الرئيسية لإعادة هيكلة مشاريع Go، وسنقدم أمثلة كود محددة، ونوضح كيفية جعل قاعدة الكود القديمة أكثر نظافة وأمانًا وكفاءة.
لماذا يتطلب الكود القديم بلغة Go نهجًا خاصًا؟
تشتهر Go ببساطتها ودقتها، ولكن حتى فيها تتراكم المشاكل:
- غياب الـ Interfaces — ارتباط صارم بين المكونات؛
- تجاهل الأخطاء — return بدون التحقق من err؛
- المتغيرات العامة (Global Variables) — تبعيات ضمنية؛
- تكرار المنطق — نسخ ولصق بدلاً من التركيب (composition)؛
- الدوال الطويلة — أكثر من 50 سطرًا مع مسؤوليات متعددة.
إعادة الهيكلة بدون اختبارات هي مثل عملية جراحية بدون تخدير. لذلك، الخطوة الأولى هي ضمان تغطية اختبارية للأجزاء الحرجة.
استراتيجية إعادة الهيكلة: من الكتلة الواحدة (Monolith) إلى الوحدات (Modules)
1. استخراج الـ Interfaces وعكس التبعيات (Dependency Inversion)
لنبدأ بكود قديم نموذجي، حيث يكون منطق الأعمال مرتبطًا بشكل صارم بتنفيذ التخزين:
// قبل إعادة الهيكلةtype UserService struct { db *sql.DB}
func (s *UserService) GetUser(id int) (*User, error) { row := s.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id) var u User err := row.Scan(&u.ID, &u.Name) return &u, err}المشكلة: لا يمكن اختبار هذا الكود بدون قاعدة بيانات حقيقية. الحل — إدخال Interface:
// بعد إعادة الهيكلةtype Storage interface { GetUser(id int) (*User, error)}
type UserService struct { storage Storage}
func NewUserService(storage Storage) *UserService { return &UserService{storage: storage}}
func (s *UserService) GetUser(id int) (*User, error) { return s.storage.GetUser(id)}الآن يمكننا بسهولة استبدال التنفيذ الفعلي بـ mock في الاختبارات.
2. تقسيم الدوال الكبيرة (Single Responsibility)
الكود القديم النموذجي — دالة تفعل كل شيء مرة واحدة:
// قبل إعادة الهيكلةfunc processOrder(order Order) error { // التحقق (Validation) if order.Amount <= 0 { return errors.New("invalid amount") } // حساب الخصم discount := 0.0 if order.CustomerType == "vip" { discount = 0.1 } // الحفظ في قاعدة البيانات _, err := db.Exec("INSERT INTO orders ...") if err != nil { return err } // إرسال البريد الإلكتروني return sendEmail(order.Email, "Order confirmed")}بعد إعادة الهيكلة، نقوم بتقسيمها إلى دوال صغيرة قابلة للاختبار:
// بعد إعادة الهيكلةtype OrderProcessor struct { db Database notifier Notifier}
func (p *OrderProcessor) Process(order Order) error { if err := validateOrder(order); err != nil { return err } order = applyDiscount(order) if err := p.db.SaveOrder(order); err != nil { return fmt.Errorf("save order: %w", err) } return p.notifier.SendConfirmation(order.Email)}
func validateOrder(order Order) error { if order.Amount <= 0 { return errors.New("amount must be positive") } return nil}
func applyDiscount(order Order) Order { if order.CustomerType == "vip" { order.Amount *= 0.9 } return order}3. استبدال المتغيرات العامة بـ DI (Dependency Injection)
المتغيرات العامة — العدو الأول للاختبار