كيفية كتابة كود سهل القراءة. هل سمعت من قبل أن المبرمجين يقضون وقتًا أطول في قراءة الكود بدلاً من كتابته؟ حسنًا، لقد وجدت أن هذا صحيح في كثير من الأحيان: كمطور، ستقضي غالبًا وقتًا أطول في قراءة الكود والتفكير فيه بدلاً من كتابته فعليًا.
وهذا يعني أنه بغض النظر عن مدى حرصك على جعل الكود يعمل بأفضل طريقة ممكنة، من المهم أيضًا أن يكون ممتعًا وسهل القراءة.
في هذه المقالة، سنلقي نظرة على مثال لوظيفة: createOrUpdateUserOnLogin
. هذه الوظيفة موجودة في قاعدة كود JavaScript بعيدة، وهي تتوسل أن تصبح أكثر سهولة ومتعة للقراءة. سنستعرض createOrUpdateUserOnLogin
، ونوضح ما يجعلها صعبة القراءة ولماذا، وفي النهاية سنعيد هيكلتها لجعلها أسهل في القراءة والفهم.
الدالة مكتوبة بلغة JavaScript وتستخدم JSDoc لتوثيق المعاملات الخاصة بها. لا يُشترط بالضرورة معرفة JavaScript لأن منطق الدالة سيتم شرحه بالتفصيل. يتم استخدام JSDoc فقط لتوثيق ما تمثله معاملات الدالة.
الدالة الإشكالية
هذه الدالة ليست من نسج الخيال. إنها دالة حقيقية في قاعدة كود لتطبيق يستخدمه أكثر من ألف مستخدم. ها هي:
/**
* @param {Object} dto
* @param {string} dto.email
* @param {string} dto.firstName
* @param {string} dto.lastName
* @param {string} [dto.photoUrl]
* @param {'apple' | 'google'} [dto.loginProvider]
* @param {string} [dto.appleId]
* @returns {string} token - access token
*/
async function createOrUpdateUserOnLogin(dto) {
let user;
if (dto.loginProvider == "apple") {
user = await findOneByAppleId(dto.appleId);
if (user?.isDisabled) {
throw new Error("Unable to login");
}
if (user && !user.verified) {
user = await setUserAsVerified(user.email);
}
if (!user) {
user = await findOneByEmail(dto.email);
if (user && dto.appleId) {
user = await updateUserAppleId(user, dto.appleId);
}
if (user && !user.verified) {
user = await setUserAsVerified(user.email);
}
}
} else {
user = await findOneByEmail(dto.email);
if (user?.isDisabled) {
throw new Error("غير قادر ");
}
if (user && !user.photoUrl && dto.photoUrl) {
user.photoUrl = dto.photoUrl;
user = await updateUserDetails(user._id, user);
}
if (user && !user.verified) {
user = await setUserAsVerified(user.email);
}
}
if (!user) {
user = await this.usersService.create(loginProviderDto);
}
return await this.createToken(user);
}
ربما يمكنك أن تلاحظ من خلال قراءة الكود ودراسته أنه من الصعب متابعته وفهمه بسهولة. إذا تركت جهاز الكمبيوتر لأخذ استراحة بعد قراءة هذه الدالة مباشرة، فمن المحتمل أنك لن تتذكر بالضبط ما تفعله الدالة عندما تعود.
ولكن هذا ليس الحال، ولا ينبغي أن يكون، عند قراءة قصة جيدة، بغض النظر عن طولها. يمكنك متابعتها بسهولة وتذكر التفاصيل الأساسية بعد سماعها.
تُنفَّذ هذه الدالة عندما يحاول المستخدم تسجيل الدخول أو إنشاء حساب جديد. يمكن للمستخدمين المصادقة باستخدام حساباتهم في Google أو Apple، ويجب أن تُرجع الدالة رمز وصول (access token) عند نجاح العملية.
بعض المستخدمين قاموا بتعطيل حساباتهم. هؤلاء المستخدمون غير مسموح لهم بالمصادقة بنجاح. كما أن منطق الدالة يتضمن عمليات لتحديث بيانات المستخدمين المسجلين مسبقًا بناءً على بعض الشروط.
تقوم الدالة بأحد أمرين:
- إنشاء رمز مصادقة (authentication token) لحساب موجود وإرجاعه بعد تحديث بيانات الحساب.
- إنشاء حساب جديد إذا لم يكن موجودًا، ثم إرجاع رمز المصادقة.
هذا ينتهك مبدأ المسؤولية الواحدة (Single Responsibility Principle)، ولكن إصلاح هذا الانتهاك يعد تحديًا لمقال آخر.
الهدف هنا هو إعادة هيكلة هذه الدالة (Refactor) بحيث تكون قابلة للقراءة بشكل جيد لدرجة أن حتى الأشخاص غير المبرمجين يمكنهم قراءتها وفهم ما تقوم به. والأفضل من ذلك، نريد أن يتمكنوا من تذكر وظيفتها بعد فترة من الابتعاد عنها.
الدالة تخضع لاختبارات جيدة، لذا لا داعي للقلق بشأن كسر أي وظيفة أثناء إعادة الهيكلة. ستقوم الاختبارات بالإبلاغ عن أي تغييرات تؤدي إلى أخطاء.
ما الذي يجعل هذا الكود صعب القراءة؟
هناك عدة عوامل تجعل قراءة هذا الكود صعبة. التداخل العميق (Deep Nesting) هو أحد هذه العوامل، حيث أن وجود عبارات if
داخل عبارات if
أخرى يجعل من الصعب تتبع التغييرات التي تحدث أثناء تنفيذ الكود. في حالة الدالة createOrUpdateUserOnLogin
، التداخل يأتي من الشروط المتعددة. في حالات أخرى، يمكن أن يشمل التداخل وجود عبارة if
داخل حلقة while
المتداخلة في عبارة if
أخرى. التداخل العميق يزيد من تعقيد قراءة وفهم الكود، كما أن تدفق الكود يصبح غير مريح بصريًا، مما يجعل كتابة الاختبارات أكثر تعقيدًا لأنك بحاجة إلى التعامل مع العمليات داخل الكتل المتداخلة.
عامل آخر هو الشروط المعقدة (Complex Conditionals)، حيث تحتوي عبارات مثل user && !user.photoUrl && dto.photoUrl
على الكثير من المنطق الذي يجب أن تحفظه في ذاكرتك قصيرة المدى أثناء متابعة قراءة الكود.
هناك أيضًا التدفق غير المنظم (Haphazard Flow) الذي يجعل من الصعب معرفة ما الذي تفعله الدالة بمجرد نظرة سريعة. تبدو وكأنها تقوم بالكثير، لكنها في الواقع تقوم بأشياء محددة تتكرر: منع المستخدمين المعطلين من تسجيل الدخول مرتين، وتحديث حالة التحقق للمستخدمين ثلاث مرات، والبحث عن المستخدمين بواسطة البريد الإلكتروني مرتين.
كيفية إعادة هيكلة الكود لجعله أسهل وأكثر متعة في القراءة
بعد فحص الدالة لتحديد المشكلات التي تجعلها صعبة القراءة، يمكن تطبيق مجموعة من التغييرات لجعل الكود أكثر وضوحًا ومتعة. أولاً، من المهم التعامل مع حالات الفشل في بداية الدالة. هذا يعني أن تبدأ بمعالجة السيناريوهات التي قد تفشل فيها الدالة وإزالتها من الطريق مبكرًا، بحيث يمكن للدالة التركيز بشكل كامل على حالات النجاح. لتحقيق ذلك، يمكن استخدام عبارات return
أو إلقاء الأخطاء (throwing errors) في المراحل الأولى من تنفيذ الدالة، ما يؤدي إلى تقديم سرد منطقي ومباشر لتدفق الكود.
إعادة ترتيب تدفق الكود هي خطوة أخرى يجب مراعاتها. إذا كان بالإمكان تنفيذ بعض العمليات قبل غيرها، وكان ذلك يسهم في جعل تدفق الكود أكثر وضوحًا وأسهل للقراءة، فإنه يجب إعادة ترتيب هذه العمليات مع الحفاظ على هدف الدالة. التدفق المنظم يجعل الكود أكثر سهولة للفهم ويترك انطباعًا ممتعًا لدى القارئ.
من المفيد أيضًا استخدام لغة مألوفة وبسيطة عند تسمية المعرفات، مما يساعد على جعل الكود أكثر قابلية للقراءة. يمكن تحقيق ذلك من خلال تحديث أسماء المعرفات وضغط الشروط المعقدة إلى أسماء معرفات سهلة التذكر، مما يجعل الكود مألوفًا ومفهومًا على الفور.
وأخيرًا، تجنب الكتل المتداخلة من الكود يعد أمرًا ضروريًا. عند محاولة فهم الكود أو تصحيحه، يصبح من الصعب تتبع التغيرات في قيم المعرفات داخل الكتل المتداخلة. مع كل شرط متداخل، تتضاعف احتمالات مسارات التنفيذ التي قد تغير قيمة معرّف واحد، مما يضيف عبئًا عقليًا كبيرًا ويزيد من احتمالية الأخطاء عند تحديث الكود. إضافة إلى ذلك، التأثير البصري للكتل المتداخلة ليس مريحًا، ويجعل كتابة الاختبارات أكثر تعقيدًا مما ينبغي.
بعد إعادة هيكلة الكود باستخدام هذه الإرشادات، يمكن تحقيق تصميم أكثر وضوحًا ومنطقية يسهل على أي شخص قراءته وفهمه.
async function updateUserOnLogin(dto) {
let user = await findUserByEmail(dto.email); // 1
if (!user) {
user = await createUser(dto);
}
if (user.isDisabled) { // 2a
throw new Error("غير قادر على الدخول"); // 2b
}
const userIsNotVerified = Boolean(user.isVerified) == false // 3a
if (userIsNotVerified) { // 3b
await setUserAsVerified(user.email);
}
const shouldUpdateAppleId = dto.loginProvider == "apple" && dto.appleId // 4a
if (shouldUpdateAppleId) { // 4b
await setUserAppleId(user.email, dto.appleId);
}
const shouldUpdatePhotoUrl = !user.photoUrl && dto.photoUrl // 5a
if (shouldUpdatePhotoUrl) { // 5b
await updateUserDetails(user._id, { photoUrl: dto.photoUrl });
}
return await this.createToken(user);
}
حسنًا، دعونا الآن نرى بالضبط ما الذي قمنا به لجعل الكود أكثر متعة في القراءة.
1. إعادة ترتيب التدفق
من خلال النظر إلى تعليق JSDoc الموجود أعلى الدالة، يمكننا ملاحظة أن البريد الإلكتروني
هو حقل مطلوب. الحسابات الموجودة بالفعل لديها عنوان email
بغض النظر عن مزود تسجيل الدخول. يمكننا جلب الحساب بواسطة البريد الإلكتروني أولاً واتخاذ قرار بإنشاء حساب جديد إذا لم يكن موجودًا (انظر القسم 1 من الكود). بهذه الطريقة، يتم التعامل مع حالات الفشل مبكرًا.
اختيار إلقاء خطأ إذا كان الحساب معطلاً في البداية (القسم 2b) هو أيضًا محاولة للتعامل مع حالات الفشل مبكرًا. هذا لا يؤثر على الحسابات الجديدة لأن الحسابات الجديدة لا تكون معطلة افتراضيًا.
التعامل مع حالات الفشل مبكرًا يساعدنا في فهم الكود بسهولة أكبر، لأنه يتيح لنا التركيز فقط على ما سيحدث دون الحاجة إلى تتبع حالات الخطأ السابقة (مثل تذكر ما إذا كان كائن المستخدم
يحتوي على قيمة أم لا في القسم 5) أثناء المتابعة.
الكود الذي أُعيدت هيكلته قد أزال أيضًا الشروط المتداخلة بالكامل وما زال يعمل كما هو متوقع.
2. استخدام لغة يومية مألوفة
في محاولة لجعل الكود يبدو وكأنه لغة يومية مألوفة، استخدمنا أسماء متغيرات واضحة وقابلة للفهم (انظر الأقسام 2a، 3، 4، 5). عندما يكون الكود مكتوبًا بهذه الطريقة، حتى غير المبرمجين مثل مديري المنتجات يمكنهم قراءته وفهم ما يحدث.
لغة يومية مألوفة تقرأ مثل pseudocode: “إذا لم يتم التحقق من المستخدم، قم بتعيين حالة التحقق” و”إذا كان يجب تحديث apple id، قم بتحديث appleId”.
استخدام لغة يومية مألوفة هو المفتاح لجعل الكود يبدو وكأنه قصة.
خاتمة
الكود الذي يكون ممتعًا للقراءة يعزز قابلية الصيانة وبالتالي يطيل عمر البرمجيات. يمكن للمساهمين قراءته وفهمه وتحديثه بسهولة. مثل قراءة قصة مكتوبة جيدًا، يمكن أن تكون قراءة الكود نشاطًا ممتعًا.