كيف تستخدم متغيرات البيئة بفعالية: دليل المطورين لتحسين الأداء والأمان

دقائق القراءة: 6

متغيرات البيئة (Environment variables) هي أحد المفاهيم الأساسية التي يعتمد عليها مطورو التطبيقات بشكل يومي، وقد حجزت لنفسها مكانًا بارزًا في منهجية تطبيقات العوامل الاثني عشر (twelve-factor app) المعيارية. توفر هذه المتغيرات قائمة طويلة من المزايا تشمل قابلية تهيئة التطبيقات وتعزيز أمانها، وهي جوانب يتم تناولها في العديد من المصادر التقنية.

لا شك أن متغيرات البيئة أداة قوية وفعالة، وأنا أؤيد تمامًا فكرة استخدامها. ومع ذلك، لكل أداة ثمن، ومتغيرات البيئة، إذا لم تُستخدم بحذر، يمكن أن تؤدي إلى آثار ضارة على قواعد الكود والتطبيقات الخاصة بنا.

لعنة متغيرات البيئة: متى تتحول الميزة إلى عيب؟

كيف يمكن لمتغيرات البيئة أن تكون سلبية وهي تساعدنا في كتابة كود أكثر أمانًا وتسهل تهيئة تطبيقاتنا لبيئات مختلفة؟ الغريب أن أوجه القصور في متغيرات البيئة تنبع في الواقع من طبيعتها الأساسية التي تجعلها رائعة: إنها عالمية (global) وخارجية (external). من خلالها، يتمكن مطورو التطبيقات من حقن التهيئة وإدارة الأسرار في مكان يصعب اختراقه.

كمطورين، نعلم جميعًا مدى سوء الحالات العالمية (global states) لتطبيقاتنا. هذه المشكلات تم مناقشتها باستفاضة في العديد من المراجع التقنية. في هذه المقالة، سأركز على عيبين رئيسيين أواجههما غالبًا عند التعامل مع متغيرات البيئة:

  • عدم المرونة / ضعف قابلية الاختبار (Inflexibility / Poor testability): تجعل من الصعب اختبار المكونات بشكل مستقل.
  • فهم الكود / قابلية القراءة (Code comprehension / readability): يصعب تتبع مصدر التهيئة وفهم متطلبات الوحدة البرمجية.

الاستخدام الأمثل لمتغيرات البيئة: حقن التبعية كحل

بشكل مشابه للطريقة التي أتعامل بها مع المتغيرات العالمية أو الأنماط العالمية (مثل singleton) التي تُطبق في أماكن غير صحيحة، فإن سلاحي المفضل هو حقن التبعية (dependency injection). لن يكون الأمر مماثلًا تمامًا لما نفعله مع تبعيات الكود، لكن المبادئ هي نفسها. بدلًا من استخدام متغيرات البيئة (التبعيات) مباشرة، نقوم بحقنها في مواقع الاستدعاء (callsites)، أي المكان الذي تُستخدم فيه فعليًا. هذا يعكس العلاقة من “مواقع الاستدعاء تعتمد” إلى “مواقع الاستدعاء تتطلب”.

يحل حقن التبعية هذه المشكلات من خلال:

  • السماح للمطورين بحقن التهيئة بسهولة أكبر وقت الاختبار.
  • تقليل النطاق الذهني لقارئي الكود ليقتصر على الحزمة (package) فقط، مما يلغي جميع العوامل الخارجية.

فكيف نطبق هذه المبادئ عمليًا؟ سأستخدم مثالًا بلغة Node.js لتوضيح كيفية إعادة هيكلة قاعدة الكود والتخلص من متغيرات البيئة المتناثرة.

سيناريو افتراضي: تطبيق بسيط لإدارة المهام

لنفترض أن لدينا تطبيقًا بسيطًا بنقطة نهاية واحدة (endpoint) سيستعلم عن جميع المهام (TODOs) في قاعدة بيانات PostgreSQL. إليك وحدة قاعدة البيانات الخاصة بنا مع متغيرات البيئة المتناثرة فيها:


const { Client } = require("pg");

function Postgres() {
  const c = new Client({
    connectionString: process.env.POSTGRES_CONNECTION_URL,
  });
  this.client = c;
  return this;
}

Postgres.prototype.init = async function() {
  await this.client.connect();
  return this;
};

Postgres.prototype.getTodos = async function() {
  return this.client.query("SELECT * FROM todos");
};

module.exports = Postgres;

وستُحقن هذه الوحدة في متحكم HTTP الخاص بنا عبر نقطة دخول التطبيق (application's entrypoint):


const express = require("express");
const TodoController = require("./controller/todo");
const Postgres = require("./pg");

const app = express();

(async function() {
  const db = new Postgres();
  await db.init();
  const controller = new TodoController(db);
  controller.install(app);

  app.listen(process.env.PORT, (err) => {
    if (err) console.error(err);
    console.log(`UP AND RUNNING @ ${process.env.PORT}`);
  });
})();

بالنظر إلى ملف نقطة الدخول أعلاه، ليس لدينا أي طريقة لمعرفة متطلبات التطبيق لمتغيرات البيئة (أو تهيئة البيئة بشكل عام) (وهذه نقطة سلبية لسهولة قراءة الكود 👎).

إعادة هيكلة الكود: نحو تهيئة واضحة ومنظمة

الخطوة الأولى لتحسين الكود المعروض سابقًا هي تحديد جميع المواقع التي تُستخدم فيها متغيرات البيئة بشكل مباشر. في حالتنا المحددة أعلاه، الأمر بسيط للغاية نظرًا لصغر حجم قاعدة الكود. ولكن بالنسبة لقواعد الكود الأكبر، يمكنك استخدام أدوات التدقيق اللغوي (linting tools) مثل ESLint للبحث عن جميع المواقع التي تستخدم متغيرات البيئة مباشرة. ما عليك سوى إعداد قاعدة، على سبيل المثال، تمنع استخدام متغيرات البيئة (مثل قاعدة node/no-process-env من إضافة eslint-plugin-node).

الآن حان الوقت لإزالة الاستخدامات المباشرة لمتغيرات البيئة من وحدات تطبيقنا وتضمين هذه التهيئة كجزء من متطلبات الوحدة:


const { Client } = require("pg");

function Postgres(opts) {
  const { connectionString } = opts;
  const c = new Client({
    connectionString,
  });
  this.client = c;
  return this;
}

Postgres.prototype.init = async function() {
  await this.client.connect();
  return this;
};

Postgres.prototype.getTodos = async function() {
  return this.client.query("SELECT * FROM todos");
};

module.exports = Postgres;

بعد هذا التعديل، سيتم توفير هذه التهيئة فقط من نقطة دخول تطبيقنا:


const express = require("express");
const TodoController = require("./controller/todo");
const Postgres = require("./pg");

const app = express();

(async function() {
  const db = new Postgres({
    connectionString: process.env.POSTGRES_CONNECTION_URL,
  });
  await db.init();
  const controller = new TodoController(db);
  controller.install(app);

  app.listen(process.env.PORT, (err) => {
    if (err) console.error(err);
    console.log(`UP AND RUNNING @ ${process.env.PORT}`);
  });
})();

أصبح من الواضح الآن بشكل أكبر ما هي متطلبات البيئة لتطبيقنا عند النظر إلى نقطة الدخول. هذا يمنع المشاكل المحتملة المتعلقة بمتغيرات البيئة التي قد تُنسى إضافتها.

أسئلة متكررة ومناقشات إضافية

هذه بعض الأسئلة التي أعتقد أنها قد تُطرح من قبل قراء هذا المنشور. ربما ليست أسئلة متكررة فعلية، ولكن ما الضرر في معالجة الآراء البديلة المحتملة؟

لماذا لا نستخدم ملف/وحدة تهيئة مركزية؟

لقد رأيت العديد من المحاولات لحل هذه المشكلة باستخدام موقع مركزي لاستخلاص هذه القيم (مثل ملف أو وحدة config.js لمشاريع Node.js). لكن هذا النهج ليس أفضل من استخدام متغيرات البيئة التي يوفرها وقت تشغيل التطبيق (مثل process.env)؛ لأن كل شيء لا يزال موحدًا في حالة عالمية إلى حد ما (كائن تهيئة واحد يُستخدم في جميع أنحاء التطبيق). في الواقع، قد يكون الأمر أسوأ، حيث أننا الآن نقدم موقعًا آخر لتدهور الكود (code rot).

ماذا لو أردت إعدادًا بدون تهيئة (zero-config) لوحدتي؟

نعم، من منا لا يحب الوحدات الجاهزة للعمل بدون تهيئة. مرة أخرى، أود التأكيد على أن بناء البرمجيات يدور حول إجراء مقايضات (trade-offs)، وهذا يأتي على حساب قابلية القراءة كما ناقشنا في هذا المنشور بأكمله. إذا كنت لا تزال ترغب في إعداد محتمل بدون تهيئة، أقترح أن يكون لديك كائنات تهيئة (أي وسيطة البناء opts في مثال الكود السابق) واستخدام مباشر لمتغيرات البيئة كخيار احتياطي فقط، شيء من هذا القبيل:


function Postgres(opts) {
  const connectionString = opts.connectionString || process.env.POSTGRES_CONNECTION_URL;
  const c = new Client({
    connectionString,
  });
  this.client = c;
  return this;
}

بهذه الطريقة، سيظل قراء الكود الخاص بنا قادرين على التعرف على متطلبات الوحدة (وإن كان ذلك بوضوح أقل، حيث تم التضحية به من أجل قابلية التهيئة الصفرية).

الخلاصة التقنية

تُعد متغيرات البيئة أداة لا غنى عنها في تطوير التطبيقات الحديثة، لكن استخدامها العشوائي يمكن أن يؤدي إلى تعقيدات كبيرة في قابلية الاختبار وقابلية قراءة الكود. من خلال تبني مبادئ حقن التبعية، يمكننا تحويل متغيرات البيئة من مصادر محتملة للمشاكل إلى مكونات تهيئة منظمة وواضحة. هذا النهج لا يعزز فقط مرونة التطبيق وأمانه، بل يساهم أيضًا في بناء قواعد كود أكثر استدامة وأسهل في الصيانة والتطوير. تجنب الحالات العالمية المباشرة، واعتمد على تمرير التهيئة كتبعيات، لضمان بيئة تطوير أكثر كفاءة واحترافية.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *