الكود المختصر ليس دائمًا كودًا نظيفًا: لماذا يجب أن نعيد التفكير في مفهوم النظافة البرمجية؟
Clean Code)، قد نقع في فخ الاعتقاد بأن تقليل عدد الأسطر يعني بالضرورة كودًا أفضل. بينما يكون هذا صحيحًا في كثير من الحالات، إلا أنه ليس قاعدة مطلقة دائمًا.
إذا تمكنت من إنجاز مهمة بطريقة تمكن المطورين الآخرين من متابعة عملي وفهمه فورًا (أو بسهولة على الأقل)، فهذا هو النهج الذي سأتبعه. فجودة الكود لا تقاس فقط بمدى اختصاره، بل بقدرته على التواصل مع من سيقرأه أو يعدل عليه.
ما هو الكود النظيف حقًا؟
في عصرنا الحالي، أصبحت قدرتنا نحن البشر على قراءة الكود وفهمه بكفاءة أكثر أهمية بكثير من قدرة الجهاز على ذلك (في معظم الحالات). عندما أكتب الكود، يكون هدفي الأول دائمًا هو إنجاز المهمة المطلوبة. بعد ذلك، أركز على كتابة الكود ليكون سهل القراءة من قبل البشر، ثم على تعقيد وقت التشغيل (runtime complexity)، ثم على الإيجاز. أخيرًا، إذا أمكن، أجعل الكود سهل إعادة الاستخدام.
إذا اضطررت لكتابة كود بطريقة تجعله غير سهل القراءة من قبل البشر لتلبية متطلبات التعقيد الزمني أو قابلية إعادة الاستخدام، فكن على يقين بأنه سيكون موثقًا بشكل جيد للغاية. التوثيق الجيد هنا يصبح ضرورة لا غنى عنها لتعويض أي نقص في وضوح الكود.
مثال على ما أعتبره كودًا نظيفًا
تلقيت مؤخرًا تحديًا لإيجاد الرقم الفردي الوحيد في مصفوفة من الأرقام الزوجية، أو العكس. تم إعطائي مصفوفة من الأعداد الصحيحة (Integers) كمدخل، ولم أكن أعرف ما إذا كانت تحتوي على أرقام فردية أو زوجية. كان هناك ضمان بوجود 3 قيم على الأقل في المصفوفة، وواحدة فقط منها ستكون فردية/زوجية (أي الشاذة). هذا كان حلي:
func findOutlier ( _ array: [Int]) -> Int {
//since we're guaranteed to have 3 values, grab the first 3
let parityArr = [ array[ 0 ], array[ 1 ], array[ 2 ] ]
//track any odd or even numbers found in parityArr || O(1) - (technically O(n) but we know the input won't grow)
var odd = 0
var even = 0
for num in parityArr {
//number is even if
if num % 2 == 0 {
even += 1
//number is odd
} else {
odd += 1
}
}
//track and test whether there were more odd or even numbers in the array
var isEven = false
if even > odd {
isEven = true
}
//return the first match that's an outlier based on the array containing more even or more odd numbers || O(n) - we don't know the input size
if isEven {
return array.first( where : ({ $ 0 % 2 != 0 }))!
} else {
return array.first( where : ({ $ 0 % 2 == 0 }))!
}
}
إذا لاحظت، لقد تركت ملاحظات تتضمن تعقيد وقت التشغيل (runtime complexity)، أو مدى كفاءة الخوارزمية في التوسع، حتى لو كان ذلك واضحًا جدًا لمن يهتم بهذا النوع من التفاصيل. كما تركت ملاحظات حول حجم المدخلات لعملية معينة (على الرغم من أن ذلك واضح أيضًا).
متى يكون الإيجاز ضروريًا ومتى يجب التفصيل؟
هناك حالات يمكن أن يقلل فيها الكود الموجز من وقت التجميع (compilation time) أو وقت التنفيذ (runtime execution)، أو غير ذلك من الأمور. ومع ذلك، يبقى اهتمامي الرئيسي هو ما إذا كان المطور التالي يمكنه قراءته، ومتابعته بسهولة، والعمل معه. هذا هو المعيار الذهبي الذي أتبعه.
كيف يمكن أن يؤثر الإيجاز المفرط على قابلية القراءة؟
في جزء الإرجاع على سبيل المثال، كان بإمكاني تحويل هذا السطر return array.first(where: ({ $0 % 2 != 0 }))! إلى حلقة تكرارية (for loop) حيث أقوم بإرجاع أول تطابق. لكنه كان سيؤدي نفس الغرض تمامًا، وبسبب طريقة تسمية الدالة (first(where:))، أعتقد أنه بنفس القدر من سهولة القراءة. ولكن ربما لا تفهم أنت، أو زميلك في العمل، صيغة الـ closure. لا بأس في ذلك – في هذه الحالة، قم بتفصيل الكود. لقد اخترت عدم التفصيل لأن هذا يبدو لي بنفس القدر من القراءة بينما هو أكثر إيجازًا.
السطر return array.first(where: { ... }) مفصلًا يبدو كالتالي:
for num in array {
if num % 2 != 0 {
return num
}
}
هناك بعض الفرص لجعل الكود في هذا المثال أكثر إيجازًا، ومع ذلك يظل قابلاً للقراءة لغالبية المطورين. على هذا النحو، كان بإمكاني أيضًا جعل هذه الكتلة البرمجية:
var isEven = false
if even > odd {
isEven = true
}
تبدو هكذا:
var isEven = even > odd
يمكن تحويل كتلة الإرجاع المذكورة أعلاه إلى فحص من سطر واحد باستخدام المعامل الثلاثي (ternary operator)، ولكن يبدو أن هناك عددًا متزايدًا من المطورين غير الملمين بالمعامل الثلاثي. أعتقد أن كتلة if/else أكثر قابلية للقراءة في معظم الحالات أيضًا:
if isEven {
return array.first( where : ({ $ 0 % 2 != 0 }))!
} else {
return array.first( where : ({ $ 0 % 2 == 0 }))!
}
مقارنة بهذا السطر الواحد:
return isEven ? array.first(where: {$0 %2 != 0}) : array.first(where: {$0 %2 == 0})
شخصيًا، أجد أن كلا العبارتين المختصرتين في سطر واحد أقل سهولة في القراءة، خاصة عندما يتعلق الأمر بالمعامل الثلاثي. على أي حال، كنت راضيًا جدًا عن حلي – فقد اجتاز جميع اختبارات الوحدة (unit tests)، وكان فعالًا جدًا، وكان قابلاً للقراءة من قبل البشر. ولكن عندما رأيت حلول الآخرين، شعرت في البداية بالخجل قليلاً من حلي البدائي…
الكثير منهم كانوا يستخدمون دالة filter، والكثير منهم كانوا يستخدمون المعامل الثلاثي. معظم حلولهم كانت أكثر إيجازًا بكثير.
مثال على الإيجاز المفرط
كانت الإجابة الأعلى تقييمًا عبارة عن سطرين من الكود واجهت صعوبة في قراءتهما في البداية – لكن هذا الكود سينجز المهمة بالتأكيد. قد يكون أكثر كفاءة من حلي في بعض الحالات، وهو بالتأكيد موجز جدًا:
func findOutlier ( _ array: [Int]) -> Int {
let odd = array. filter {$ 0 % 2 != 0 }
return odd. count > 1 ? array. filter {$ 0 % 2 == 0 }[ 0 ] : odd[ 0 ]
}
كلا المثالين يلبيان المعيار الأول لكتابة كود نظيف (أو أي كود في الواقع) – إنهما يعملان. كلاهما أيضًا موجز، على الرغم من أن حلي يمكن أن يكون أكثر إيجازًا. للوهلة الأولى، اعتقدت أن الحل الأكثر إيجازًا كان رائعًا. إنه أنيق، وفعال، وموجز.
ثم بدأت في تفكيك جملة الإرجاع (return) إلى كتلة if/else وأدركت أن حلي ربما يكون أكثر كفاءة في معظم الأوقات. أنا أقوم بالتكرار على المصفوفة بأكملها مرة واحدة فقط، وفقط إذا كان الرقم الشاذ (outlier) هو الرقم الأخير في المصفوفة. إنه لا يزال حلاً جيدًا، لكنني لن أقول إنه رائع (أو كما لاحظ الكثيرون على الموقع – أفضل ممارسة).
في حالة مصفوفة غالبها أرقام زوجية في الحل الموجز، سيتم تصفيتها (filtered) مرتين. مرة لإنشاء المصفوفة المسماة odd (والتي يمكن تسميتها بشكل أفضل أيضًا) – وهذا يعني التكرار على المصفوفة بأكملها. ثم مرة أخرى إذا تبين أنها ليست مصفوفة غالبها أرقام فردية. هذا ليس مشكلة كبيرة إذا كان هناك 3 أرقام فقط. ولكن بالنظر إلى مصفوفة تحتوي على 10,000 رقم، فإنك تنظر إلى فترة زمنية كبيرة حيث ينتظر المستخدم شيئًا ليتم حسابه لا يحتاج إلى حسابه.
شيء آخر يجب ملاحظته حول حلي مقابل الحل الموجز هو أن إجابتي يتم إرجاعها بمجرد العثور عليها في المصفوفة. لنفترض أن مصفوفة الإدخال كانت فردية، وكان الرقم الزوجي هو الرقم الأول في المصفوفة. في حلي، سيتم حسابه وإرجاعه على الفور تقريبًا، بينما في الحل الموجز، سننتظر تصفية المصفوفة بأكملها قبل إرجاع الإجابة.
ملاحظة حول قابلية إعادة الاستخدام
لقد تطرقت إلى قابلية إعادة الاستخدام (re-usability) في وقت مبكر، لكننا لم نتحدث عنها كثيرًا. الكود القابل لإعادة الاستخدام يعني أنه يمكنك استخدامه في أكثر من موقف واحد. هذا أحد الاهتمامات الرئيسية لكتابة الكود النظيف، ولكن فقط عندما يكون ذلك منطبقًا. يمكننا تحقيق ذلك باستخدام معاملات (parameters) في الدوال تكون مرنة لحالات استخدام مختلفة، وأشياء أخرى تتماشى مع القدرة على استخدام الكود الخاص بنا في مكان آخر دون أي تعديل أو بتعديل بسيط.
ولكن كيف يمكن أن يؤثر كتابة كود قابل لإعادة الاستخدام على قابلية القراءة؟ كان بإمكاني جعل هذه الدالة بأكملها عامة (generic). كانت ستظل تلبي المعايير، وستجعلها أكثر قابلية لإعادة الاستخدام. كنا سنتمكن من فحص أي نوع رقمي على سبيل المثال، لكن ذلك لم يكن ضمن نطاق هذا المشروع، والقيام بذلك كان سيجعلها أقل قابلية للقراءة إذا لم تكن ملمًا بصيغة الـ generic.
الحفاظ على نظافة الكود يجنب المخاطر
أحد مخاطر كتابة كود موجز جدًا هو أنه يصعب التعامل مع الحالات الهامشية (edge cases). هذا لأنه يجعل من الصعب رؤية “الأجزاء المتحركة” بلمحة واحدة. أنا بالتأكيد لا أقول إن حلي مثالي. يمكنني بالفعل رؤية طريقة واحدة يمكنني من خلالها جعله أكثر كفاءة (يمكننا تخطي عملية الـ O(n) النهائية في بعض الحالات) ومع ذلك يظل قابلاً للقراءة.
لكن النقطة المهمة هي أنه يمكنني العودة إلى هذا الكود في أي وقت في المستقبل القريب أو البعيد، ورؤية كيف يعمل بسهولة، وكيف يمكنني تحسينه. تذكر دائمًا أن هناك الكثير مما يدخل في كتابة الكود النظيف. النظافة لا تعني فقط الإيجاز! اكتب الكود الخاص بك بحيث يمكن للمطورين الآخرين العمل معه – كل إنسان يعمل عليه سيشكرك على ذلك.
الخلاصة التقنية
يُعد هذا المقال تذكيرًا هامًا بأن الإيجاز في الكود ليس دائمًا مرادفًا للنظافة أو الكفاءة المثلى. بينما نسعى كمطورين لكتابة أكواد موجزة، يجب أن نضع في اعتبارنا أن الهدف الأسمى هو قابلية القراءة والصيانة على المدى الطويل. الأمثلة المقدمة توضح كيف أن الحل الأكثر تفصيلاً قد يكون في الواقع أكثر كفاءة في سيناريوهات معينة، وأسهل في الفهم والتعديل من قبل فريق العمل. التركيز على الوضوح، والتوثيق الجيد، والتفكير في التعقيد الزمني الحقيقي للعمليات، يمثل ركائز أساسية للكود النظيف الذي يخدم المشروع والمطورين على حد سواء. يجب أن يكون الكود النظيف بمثابة قصة واضحة يمكن لأي مطور قراءتها وفهمها، وليس لغزًا يتطلب فك شفرته.