أنماط التصميم في JavaScript
ما هي أنماط التصميم؟
أنماط التصميم هي حلول مثبتة وقابلة لإعادة الاستخدام لمشاكل شائعة تحدث في تصميم البرمجيات. ليست كودًا جاهزًا تنسخه وتلصقه في مشروعك. بل هي قوالب أو مخططات تصف كيفية حل مشكلة تصميم معينة بطريقة مرنة وقابلة للصيانة والتوسع. شُهرت أنماط التصميم من خلال كتاب "عصابة الأربعة" (GoF) المنشور في 1994، ومنذ ذلك الحين تم تكييفها لكل لغة برمجة بما في ذلك JavaScript.
في JavaScript، أنماط التصميم مهمة بشكل خاص لأن اللغة مرنة للغاية. يمكنك كتابة JavaScript بأساليب إجرائية أو كائنية التوجه أو وظيفية. هذه المرونة قوية لكنها تعني أيضًا أنه بدون أنماط يمكن أن تصبح قواعد الكود فوضوية وصعبة الصيانة بسرعة. تمنح الأنماط كودك هيكلاً وتجعل نيتك واضحة للمطورين الآخرين.
تنقسم أنماط التصميم إلى ثلاث فئات:
- الأنماط الإنشائية -- تتعامل مع آليات إنشاء الكائنات، محاولة إنشاء الكائنات بطريقة مناسبة للموقف.
- الأنماط الهيكلية -- تتعامل مع تركيب الكائنات، تعريف طرق لتجميع الكائنات والفئات في هياكل أكبر.
- الأنماط السلوكية -- تتعامل مع التواصل بين الكائنات، تعريف كيفية تفاعل الكائنات وتوزيع المسؤولية.
النمط الإنشائي: المفرد (Singleton)
يضمن نمط المفرد أن الفئة لها نسخة واحدة فقط ويوفر نقطة وصول عامة إليها. هذا مفيد للموارد المشتركة مثل كائنات التكوين ومجمعات الاتصال وخدمات التسجيل أو مخازن حالة التطبيق حيث يسبب وجود نسخ متعددة تعارضات أو إهدار موارد.
المفرد بنمط الوحدة
في JavaScript، أبسط طريقة لإنشاء مفرد هي استخدام نمط الوحدة مع تعبير دالة مستدعاة فوريًا (IIFE) أو وحدات ES. بما أن وحدات ES تُقيّم مرة واحدة فقط وتُخزّن مؤقتًا، كل ملف يستورد نفس الوحدة يحصل على نفس النسخة.
مثال: المفرد باستخدام نمط الوحدة (IIFE)
const AppConfig = (function() {
let instance = null;
function createInstance() {
// حالة خاصة
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
debug: false
};
return {
get(key) {
return config[key];
},
set(key, value) {
if (config.hasOwnProperty(key)) {
config[key] = value;
} else {
throw new Error("مفتاح تكوين غير معروف: " + key);
}
},
getAll() {
return { ...config };
}
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// الاستخدام -- كلا المتغيرين يشيران لنفس النسخة
const config1 = AppConfig.getInstance();
const config2 = AppConfig.getInstance();
config1.set("debug", true);
console.log(config2.get("debug")); // true
console.log(config1 === config2); // true
المفرد بفئة ES6
مثال: المفرد القائم على الفئة
class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance;
}
this.logs = [];
this.level = "info";
Logger.instance = this;
}
setLevel(level) {
this.level = level;
}
log(message, level = "info") {
const entry = {
message,
level,
timestamp: new Date().toISOString()
};
this.logs.push(entry);
console.log("[" + entry.level.toUpperCase() + "] "
+ entry.timestamp + ": " + entry.message);
}
error(message) { this.log(message, "error"); }
warn(message) { this.log(message, "warn"); }
getHistory() { return [...this.logs]; }
clear() { this.logs = []; }
}
const logger1 = new Logger();
const logger2 = new Logger();
logger1.log("بدأ التطبيق");
logger2.error("حدث خطأ ما");
console.log(logger1 === logger2); // true
console.log(logger1.getHistory().length); // 2
النمط الإنشائي: المصنع (Factory)
يوفر نمط المصنع واجهة لإنشاء الكائنات بدون تحديد فئتها الدقيقة. يقرر المصنع أي فئة يُنشئ نسخة منها بناءً على معاملات الإدخال. هذا مفيد عندما يكون لديك عائلة من الكائنات ذات الصلة وتريد مركزة منطق الإنشاء.
مثال: نمط المصنع
// فئات المنتجات
class TextNotification {
constructor(message) {
this.type = "text";
this.message = message;
}
send(recipient) {
console.log("إرسال نص إلى " + recipient + ": " + this.message);
}
}
class EmailNotification {
constructor(message, subject) {
this.type = "email";
this.message = message;
this.subject = subject || "إشعار";
}
send(recipient) {
console.log("إرسال بريد إلى " + recipient
+ " [" + this.subject + "]: " + this.message);
}
}
class PushNotification {
constructor(message, icon) {
this.type = "push";
this.message = message;
this.icon = icon || "default-icon.png";
}
send(recipient) {
console.log("إرسال دفع إلى " + recipient + ": " + this.message);
}
}
// المصنع
class NotificationFactory {
static create(type, options = {}) {
switch (type) {
case "text":
return new TextNotification(options.message);
case "email":
return new EmailNotification(options.message, options.subject);
case "push":
return new PushNotification(options.message, options.icon);
default:
throw new Error("نوع إشعار غير معروف: " + type);
}
}
}
// الاستخدام
const notifications = [
NotificationFactory.create("text", { message: "تم شحن الكود!" }),
NotificationFactory.create("email", {
message: "اكتمل النشر.",
subject: "حالة النشر"
}),
NotificationFactory.create("push", { message: "تعليق جديد على طلبك" })
];
notifications.forEach((n) => n.send("developer@example.com"));
النمط الإنشائي: الباني (Builder)
يفصل نمط الباني بناء كائن معقد عن تمثيله، مما يسمح لنفس عملية البناء بإنشاء تمثيلات مختلفة. وهو مثالي للكائنات ذات الخيارات الاختيارية الكثيرة حيث يكون المُنشئ ذو الوسائط العديدة مربكًا.
مثال: نمط الباني
class RequestBuilder {
constructor(url) {
this.config = {
url: url,
method: "GET",
headers: {},
body: null,
timeout: 30000,
retries: 0,
cache: true
};
}
setMethod(method) {
this.config.method = method.toUpperCase();
return this;
}
setHeader(key, value) {
this.config.headers[key] = value;
return this;
}
setBody(body) {
this.config.body = typeof body === "string"
? body : JSON.stringify(body);
return this;
}
setTimeout(ms) {
this.config.timeout = ms;
return this;
}
setRetries(count) {
this.config.retries = count;
return this;
}
noCache() {
this.config.cache = false;
return this;
}
build() {
if (!this.config.url) {
throw new Error("الرابط مطلوب");
}
if (this.config.method === "GET" && this.config.body) {
throw new Error("طلبات GET لا يمكن أن تحتوي على جسم");
}
return Object.freeze({ ...this.config });
}
}
// الاستخدام مع تسلسل الطرق
const request = new RequestBuilder("https://api.example.com/users")
.setMethod("POST")
.setHeader("Content-Type", "application/json")
.setHeader("Authorization", "Bearer token123")
.setBody({ name: "سارة", role: "مطورة" })
.setTimeout(10000)
.setRetries(3)
.build();
console.log(request);
النمط الهيكلي: الوحدة (Module)
يغلف نمط الوحدة الحالة الخاصة ويكشف واجهة برمجة عامة. قبل وجود وحدات ES كانت هذه الطريقة الأساسية لتحقيق التغليف في JavaScript. يستخدم الإغلاقات لإنشاء نطاق خاص ويعيد كائنًا بالطرق العامة.
مثال: نمط الوحدة
const ShoppingCart = (function() {
// حالة خاصة
let items = [];
let discount = 0;
// مساعد خاص
function calculateSubtotal() {
return items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0);
}
// الواجهة العامة
return {
addItem(name, price, quantity = 1) {
const existing = items.find((item) => item.name === name);
if (existing) {
existing.quantity += quantity;
} else {
items.push({ name, price, quantity });
}
return this;
},
removeItem(name) {
items = items.filter((item) => item.name !== name);
return this;
},
setDiscount(percent) {
if (percent < 0 || percent > 100) {
throw new Error("الخصم يجب أن يكون بين 0 و100");
}
discount = percent;
return this;
},
getTotal() {
const subtotal = calculateSubtotal();
return subtotal - (subtotal * discount / 100);
},
getItems() {
return items.map((item) => ({ ...item }));
},
getItemCount() {
return items.reduce((sum, item) => sum + item.quantity, 0);
},
clear() {
items = [];
discount = 0;
}
};
})();
// الاستخدام
ShoppingCart.addItem("حاسوب محمول", 999, 1)
.addItem("فأرة", 29, 2)
.setDiscount(10);
console.log(ShoppingCart.getTotal()); // 951.3
console.log(ShoppingCart.getItemCount()); // 3
النمط الهيكلي: الواجهة (Facade)
يوفر نمط الواجهة واجهة مبسطة لنظام فرعي معقد. يخفي تعقيد فئات أو واجهات برمجة متعددة متفاعلة خلف واجهة واحدة سهلة الاستخدام. هذا من أكثر الأنماط استخدامًا في JavaScript خاصة عند تغليف واجهات المتصفح أو المكتبات الخارجية.
مثال: نمط الواجهة
// أنظمة فرعية معقدة
class AudioEngine {
constructor() { this.context = null; }
initialize() { console.log("تمت تهيئة محرك الصوت"); }
setVolume(level) { console.log("مستوى الصوت:", level); }
play(buffer) { console.log("تشغيل الصوت"); }
stop() { console.log("توقف الصوت"); }
}
class VideoRenderer {
constructor(canvas) { this.canvas = canvas; }
initialize() { console.log("تمت تهيئة عارض الفيديو"); }
render(frame) { console.log("عرض الإطار"); }
setResolution(w, h) { console.log("الدقة:", w + "x" + h); }
stop() { console.log("توقف الفيديو"); }
}
class SubtitleManager {
constructor() { this.subtitles = []; }
load(url) { console.log("تحميل الترجمة من", url); }
show(time) { console.log("عرض الترجمة عند", time); }
hide() { console.log("إخفاء الترجمة"); }
setLanguage(lang) { console.log("لغة الترجمة:", lang); }
}
// الواجهة -- واجهة بسيطة للأنظمة الفرعية المعقدة
class MediaPlayer {
constructor(canvasElement) {
this.audio = new AudioEngine();
this.video = new VideoRenderer(canvasElement);
this.subtitles = new SubtitleManager();
this.isPlaying = false;
}
initialize() {
this.audio.initialize();
this.video.initialize();
console.log("مشغل الوسائط جاهز");
}
play(mediaUrl, subtitleUrl, language) {
this.audio.play(mediaUrl);
this.video.render(mediaUrl);
if (subtitleUrl) {
this.subtitles.load(subtitleUrl);
this.subtitles.setLanguage(language || "ar");
}
this.isPlaying = true;
}
pause() {
this.audio.stop();
this.video.stop();
this.isPlaying = false;
}
setVolume(level) {
this.audio.setVolume(Math.max(0, Math.min(1, level)));
}
}
const player = new MediaPlayer(document.querySelector("canvas"));
player.initialize();
player.play("/videos/intro.mp4", "/subs/intro.vtt", "ar");
player.setVolume(0.8);
النمط الهيكلي: المزخرف (Decorator)
يُرفق نمط المزخرف سلوكًا إضافيًا لكائن ديناميكيًا بدون تعديل بنيته. في JavaScript يمكن تنفيذ المزخرفات عن طريق تغليف الكائنات بوظائف جديدة. هذا مفيد لتوسيع السلوك بدون إنشاء تسلسلات وراثة معقدة.
مثال: نمط المزخرف
// المكون الأساسي
class BasicCoffee {
cost() { return 2.00; }
description() { return "قهوة أساسية"; }
}
// قاعدة المزخرف
class CoffeeDecorator {
constructor(coffee) { this.coffee = coffee; }
cost() { return this.coffee.cost(); }
description() { return this.coffee.description(); }
}
// مزخرفات محددة
class MilkDecorator extends CoffeeDecorator {
cost() { return this.coffee.cost() + 0.50; }
description() { return this.coffee.description() + "، حليب"; }
}
class SugarDecorator extends CoffeeDecorator {
cost() { return this.coffee.cost() + 0.25; }
description() { return this.coffee.description() + "، سكر"; }
}
class WhippedCreamDecorator extends CoffeeDecorator {
cost() { return this.coffee.cost() + 0.75; }
description() { return this.coffee.description() + "، كريمة مخفوقة"; }
}
// الاستخدام -- تكديس المزخرفات ديناميكيًا
let order = new BasicCoffee();
order = new MilkDecorator(order);
order = new SugarDecorator(order);
order = new WhippedCreamDecorator(order);
console.log(order.description()); // "قهوة أساسية، حليب، سكر، كريمة مخفوقة"
console.log(order.cost()); // 3.50
النمط الهيكلي: الوكيل (Proxy)
يوفر نمط الوكيل بديلاً أو عنصرًا نائبًا لكائن آخر للتحكم في الوصول إليه. يمتلك JavaScript كائن Proxy مدمجًا (تم تقديمه في ES6) يجعل تنفيذ هذا النمط أصليًا وقويًا. يمكن للوكلاء اعتراض وإعادة تعريف العمليات الأساسية مثل الوصول للخصائص والتعيين واستدعاء الدوال والمزيد.
مثال: نمط الوكيل مع ES6 Proxy
// وكيل التحقق -- يفرض فحص الأنواع على كائن
function createValidatedObject(schema) {
const data = {};
return new Proxy(data, {
set(target, property, value) {
const validator = schema[property];
if (!validator) {
throw new Error("خاصية غير معروفة: " + property);
}
if (typeof value !== validator.type) {
throw new TypeError(
property + " يجب أن يكون " + validator.type
+ "، حصلنا على " + typeof value
);
}
if (validator.min !== undefined && value < validator.min) {
throw new RangeError(
property + " يجب أن يكون على الأقل " + validator.min
);
}
if (validator.max !== undefined && value > validator.max) {
throw new RangeError(
property + " يجب أن يكون على الأكثر " + validator.max
);
}
target[property] = value;
return true;
},
get(target, property) {
if (property in target) {
return target[property];
}
const validator = schema[property];
return validator ? validator.default : undefined;
}
});
}
const userSchema = {
name: { type: "string", default: "" },
age: { type: "number", min: 0, max: 150, default: 0 },
email: { type: "string", default: "" }
};
const user = createValidatedObject(userSchema);
user.name = "سارة"; // يعمل
user.age = 30; // يعمل
console.log(user.name); // "سارة"
// user.age = -5; // RangeError
// user.age = "ثلاثون"; // TypeError
// user.phone = "123"; // Error: خاصية غير معروفة
مثال: وكيل التخزين المؤقت
function createCachingProxy(targetFunction, ttl = 60000) {
const cache = new Map();
return new Proxy(targetFunction, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
console.log("إصابة الذاكرة المؤقتة:", key);
return cached.value;
}
console.log("فقدان الذاكرة المؤقتة:", key);
const result = target.apply(thisArg, args);
cache.set(key, { value: result, timestamp: Date.now() });
return result;
}
});
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const cachedFib = createCachingProxy(fibonacci, 30000);
console.log(cachedFib(35)); // استدعاء بطيء أول مرة
console.log(cachedFib(35)); // فوري من الذاكرة المؤقتة
النمط السلوكي: المراقب / ناشر-مشترك (Observer / PubSub)
يعرّف نمط المراقب تبعية واحد-إلى-كثير بين الكائنات. عندما يتغير حالة كائن واحد (الموضوع)، يتم إخطار جميع تابعيه (المراقبين) وتحديثهم تلقائيًا. يضيف نمط ناشر-مشترك وسيطًا بين الناشرين والمشتركين مما يفصلهم أكثر.
مثال: نمط المراقب
class EventEmitter {
constructor() {
this.events = new Map();
}
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(callback);
return this;
}
off(event, callback) {
if (!this.events.has(event)) return this;
const listeners = this.events.get(event);
this.events.set(event,
listeners.filter((cb) => cb !== callback)
);
return this;
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
emit(event, ...args) {
if (!this.events.has(event)) return false;
this.events.get(event).forEach((cb) => {
try { cb(...args); }
catch (error) { console.error("خطأ في المستمع لـ " + event, error); }
});
return true;
}
removeAllListeners(event) {
if (event) { this.events.delete(event); }
else { this.events.clear(); }
return this;
}
}
// الاستخدام -- مخزن يُخطر عند تغير الحالة
class Store extends EventEmitter {
constructor(initialState = {}) {
super();
this.state = initialState;
}
setState(updates) {
const previous = { ...this.state };
this.state = { ...this.state, ...updates };
this.emit("change", this.state, previous);
for (const key of Object.keys(updates)) {
if (previous[key] !== updates[key]) {
this.emit("change:" + key, updates[key], previous[key]);
}
}
}
getState() { return { ...this.state }; }
}
const store = new Store({ count: 0, user: null });
store.on("change:count", (newVal, oldVal) => {
console.log("تغير العدد من", oldVal, "إلى", newVal);
});
store.setState({ count: 1 }); // "تغير العدد من 0 إلى 1"
store.setState({ user: "سارة" });
النمط السلوكي: الوسيط (Mediator)
يعرّف نمط الوسيط كائنًا يغلف كيفية تفاعل مجموعة من الكائنات. بدلاً من تواصل الكائنات مباشرة مع بعضها (مما يخلق ارتباطًا محكمًا)، تتواصل عبر الوسيط. هذا يقلل التبعيات بين الكائنات ويجعل النظام أسهل في التعديل.
مثال: نمط الوسيط (غرفة دردشة)
class ChatRoom {
constructor(name) {
this.name = name;
this.users = new Map();
this.messageHistory = [];
}
join(user) {
this.users.set(user.name, user);
user.room = this;
this.broadcast("النظام", user.name + " انضم للغرفة.");
}
leave(user) {
this.users.delete(user.name);
user.room = null;
this.broadcast("النظام", user.name + " غادر الغرفة.");
}
send(message, fromUser, toUserName) {
const entry = {
from: fromUser.name,
to: toUserName || "الكل",
message: message,
timestamp: new Date().toISOString()
};
this.messageHistory.push(entry);
if (toUserName) {
const recipient = this.users.get(toUserName);
if (recipient) {
recipient.receive(message, fromUser.name, true);
}
} else {
this.broadcast(fromUser.name, message);
}
}
broadcast(senderName, message) {
this.users.forEach((user) => {
if (user.name !== senderName) {
user.receive(message, senderName, false);
}
});
}
}
class ChatUser {
constructor(name) {
this.name = name;
this.room = null;
this.inbox = [];
}
send(message, toUserName) {
if (!this.room) {
console.log(this.name + ": لست في غرفة!");
return;
}
this.room.send(message, this, toUserName);
}
receive(message, fromName, isPrivate) {
const prefix = isPrivate ? "[خاص]" : "[الغرفة]";
const formatted = prefix + " " + fromName + ": " + message;
this.inbox.push(formatted);
console.log(this.name + " استلم -- " + formatted);
}
}
const room = new ChatRoom("المطورون");
const alice = new ChatUser("سارة");
const bob = new ChatUser("أحمد");
const charlie = new ChatUser("خالد");
room.join(alice);
room.join(bob);
room.join(charlie);
alice.send("مرحبًا بالجميع!");
bob.send("أهلاً سارة!", "سارة");
charlie.send("أعمل على الواجهة الجديدة.");
النمط السلوكي: الاستراتيجية (Strategy)
يعرّف نمط الاستراتيجية عائلة من الخوارزميات ويغلف كل واحدة ويجعلها قابلة للتبديل. يتيح للخوارزمية أن تتغير بشكل مستقل عن العملاء الذين يستخدمونها. هذا النمط مثالي للمواقف التي لديك فيها مناهج متعددة لنفس المهمة وتريد اختيار واحد أثناء التشغيل.
مثال: نمط الاستراتيجية
const sortStrategies = {
bubble(arr) {
const result = [...arr];
for (let i = 0; i < result.length; i++) {
for (let j = 0; j < result.length - i - 1; j++) {
if (result[j] > result[j + 1]) {
[result[j], result[j + 1]] = [result[j + 1], result[j]];
}
}
}
return result;
},
quick(arr) {
if (arr.length <= 1) return [...arr];
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter((x) => x < pivot);
const middle = arr.filter((x) => x === pivot);
const right = arr.filter((x) => x > pivot);
return [...sortStrategies.quick(left), ...middle,
...sortStrategies.quick(right)];
},
merge(arr) {
if (arr.length <= 1) return [...arr];
const mid = Math.floor(arr.length / 2);
const left = sortStrategies.merge(arr.slice(0, mid));
const right = sortStrategies.merge(arr.slice(mid));
return mergeSorted(left, right);
}
};
function mergeSorted(left, right) {
const result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) result.push(left[i++]);
else result.push(right[j++]);
}
return result.concat(left.slice(i)).concat(right.slice(j));
}
class Sorter {
constructor(strategy = "quick") {
this.setStrategy(strategy);
}
setStrategy(name) {
if (!sortStrategies[name]) {
throw new Error("استراتيجية غير معروفة: " + name);
}
this.strategyName = name;
this.strategy = sortStrategies[name];
}
sort(data) {
console.log("الفرز باستراتيجية", this.strategyName);
const start = performance.now();
const result = this.strategy(data);
const duration = performance.now() - start;
console.log("تم الفرز في", duration.toFixed(2), "مللي ثانية");
return result;
}
}
const sorter = new Sorter("bubble");
const data = [64, 34, 25, 12, 22, 11, 90];
console.log(sorter.sort(data));
sorter.setStrategy("quick");
console.log(sorter.sort(data));
sorter.setStrategy("merge");
console.log(sorter.sort(data));
النمط السلوكي: الأمر (Command)
يغلف نمط الأمر طلبًا ككائن مما يسمح لك بتحديد معاملات العمليات ووضعها في قائمة انتظار وتسجيلها ودعم وظيفة التراجع/الإعادة. يحتوي كل كائن أمر على جميع المعلومات المطلوبة لتنفيذ أو عكس إجراء.
مثال: نمط الأمر مع التراجع/الإعادة
class Command {
execute() { throw new Error("يجب تنفيذ execute()"); }
undo() { throw new Error("يجب تنفيذ undo()"); }
describe() { return "أمر غير معروف"; }
}
class AddTextCommand extends Command {
constructor(editor, text, position) {
super();
this.editor = editor;
this.text = text;
this.position = position;
}
execute() { this.editor.insertAt(this.position, this.text); }
undo() { this.editor.deleteRange(this.position, this.position + this.text.length); }
describe() { return "إضافة نص: \"" + this.text + "\" عند " + this.position; }
}
class DeleteTextCommand extends Command {
constructor(editor, start, end) {
super();
this.editor = editor;
this.start = start;
this.end = end;
this.deletedText = "";
}
execute() {
this.deletedText = this.editor.getText(this.start, this.end);
this.editor.deleteRange(this.start, this.end);
}
undo() { this.editor.insertAt(this.start, this.deletedText); }
describe() { return "حذف نص من " + this.start + " إلى " + this.end; }
}
class TextEditor {
constructor() { this.content = ""; }
insertAt(position, text) {
this.content = this.content.slice(0, position) + text + this.content.slice(position);
}
deleteRange(start, end) {
this.content = this.content.slice(0, start) + this.content.slice(end);
}
getText(start, end) { return this.content.slice(start, end); }
toString() { return this.content; }
}
class CommandManager {
constructor() { this.history = []; this.undone = []; }
execute(command) {
command.execute();
this.history.push(command);
this.undone = [];
}
undo() {
const command = this.history.pop();
if (command) { command.undo(); this.undone.push(command); }
}
redo() {
const command = this.undone.pop();
if (command) { command.execute(); this.history.push(command); }
}
}
const editor = new TextEditor();
const manager = new CommandManager();
manager.execute(new AddTextCommand(editor, "مرحبًا ", 0));
manager.execute(new AddTextCommand(editor, "بالعالم!", 7));
console.log(editor.toString()); // "مرحبًا بالعالم!"
manager.undo();
console.log(editor.toString()); // "مرحبًا "
manager.redo();
console.log(editor.toString()); // "مرحبًا بالعالم!"
النمط السلوكي: المكرر (Iterator)
يوفر نمط المكرر طريقة للوصول إلى عناصر مجموعة بشكل تسلسلي بدون كشف تمثيلها الداخلي. يمتلك JavaScript دعمًا مدمجًا للمكررات من خلال بروتوكول Symbol.iterator ودوال المولد.
مثال: مكرر مخصص مع Symbol.iterator
class NumberRange {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
const step = this.step;
return {
next() {
if (current <= end) {
const value = current;
current += step;
return { value, done: false };
}
return { done: true };
}
};
}
}
for (const num of new NumberRange(1, 10, 2)) {
console.log(num); // 1, 3, 5, 7, 9
}
console.log([...new NumberRange(0, 5)]); // [0, 1, 2, 3, 4, 5]
// مكرر قائم على المولد لبنية شجرة
class TreeNode {
constructor(value, children = []) {
this.value = value;
this.children = children;
}
*[Symbol.iterator]() {
yield this.value;
for (const child of this.children) {
yield* child;
}
}
}
const tree = new TreeNode("الجذر", [
new TreeNode("أ", [
new TreeNode("أ1"),
new TreeNode("أ2")
]),
new TreeNode("ب", [
new TreeNode("ب1")
])
]);
console.log([...tree]); // ["الجذر", "أ", "أ1", "أ2", "ب", "ب1"]
مفاهيم MVC و MVVM
MVC (نموذج-عرض-متحكم) و MVVM (نموذج-عرض-نموذج العرض) هي أنماط معمارية تنظم تطبيقات كاملة. تفصل البيانات والعرض والمنطق إلى طبقات متميزة. بينما تنفذ الأطر الحديثة هذه الأنماط داخليًا، فهمها يساعدك في كتابة كود أفضل تنظيمًا.
مثال: تنفيذ MVC بسيط
// النموذج -- يدير البيانات ومنطق الأعمال
class TodoModel {
constructor() { this.todos = []; this.listeners = []; }
subscribe(listener) { this.listeners.push(listener); }
notify() { this.listeners.forEach((fn) => fn(this.todos)); }
addTodo(text) {
this.todos.push({ id: Date.now(), text, done: false });
this.notify();
}
toggleTodo(id) {
const todo = this.todos.find((t) => t.id === id);
if (todo) { todo.done = !todo.done; this.notify(); }
}
removeTodo(id) {
this.todos = this.todos.filter((t) => t.id !== id);
this.notify();
}
getAll() { return [...this.todos]; }
}
// العرض -- يتعامل مع العرض وإدخال المستخدم
class TodoView {
constructor(container) {
this.container = container;
this.onAdd = null;
this.onToggle = null;
this.onRemove = null;
}
render(todos) {
this.container.innerHTML = "";
const form = document.createElement("form");
const input = document.createElement("input");
input.placeholder = "أضف مهمة...";
const button = document.createElement("button");
button.textContent = "إضافة";
form.appendChild(input);
form.appendChild(button);
form.addEventListener("submit", (e) => {
e.preventDefault();
if (input.value.trim() && this.onAdd) {
this.onAdd(input.value.trim());
input.value = "";
}
});
this.container.appendChild(form);
const list = document.createElement("ul");
todos.forEach((todo) => {
const li = document.createElement("li");
li.textContent = todo.text;
li.style.textDecoration = todo.done ? "line-through" : "none";
li.addEventListener("click", () => {
if (this.onToggle) this.onToggle(todo.id);
});
const removeBtn = document.createElement("button");
removeBtn.textContent = "X";
removeBtn.addEventListener("click", (e) => {
e.stopPropagation();
if (this.onRemove) this.onRemove(todo.id);
});
li.appendChild(removeBtn);
list.appendChild(li);
});
this.container.appendChild(list);
}
}
// المتحكم -- يربط النموذج والعرض
class TodoController {
constructor(model, view) {
this.model = model;
this.view = view;
this.view.onAdd = (text) => this.model.addTodo(text);
this.view.onToggle = (id) => this.model.toggleTodo(id);
this.view.onRemove = (id) => this.model.removeTodo(id);
this.model.subscribe((todos) => this.view.render(todos));
this.view.render(this.model.getAll());
}
}
متى تستخدم الأنماط
أنماط التصميم هي أدوات وليست قواعد. تطبيق نمط حيث لا يكون مطلوبًا يضيف تعقيدًا غير ضروري. إليك دليلاً عمليًا لمتى يكون كل نمط مفيدًا أكثر:
- المفرد -- استخدمه للموارد المشتركة حقًا: المسجلات، التكوين، اتصالات قاعدة البيانات، حالة التطبيق الشاملة. تجنبه عندما تحتاج نسخ قابلة للاختبار ومعزولة.
- المصنع -- استخدمه عندما يكون منطق الإنشاء معقدًا أو عندما يعتمد النوع الدقيق على ظروف التشغيل أو تريد فصل المستهلكين عن الفئات المحددة.
- الباني -- استخدمه عندما تتطلب الكائنات خيارات تكوين كثيرة. إذا كان المُنشئ يحتوي أكثر من ثلاثة أو أربعة معاملات، فكر في الباني.
- الوحدة -- استخدمه لتغليف الحالة الخاصة وكشف واجهة عامة نظيفة. في JavaScript الحديث، وحدات ES تخدم هذا الغرض أصلاً.
- الواجهة -- استخدمها لتبسيط الأنظمة الفرعية المعقدة أو توفير واجهة موحدة عبر مكتبات متعددة.
- المزخرف -- استخدمه لإضافة سلوك للكائنات بدون وراثة. مثالي للاهتمامات المتقاطعة مثل التسجيل والتخزين المؤقت والتحقق.
- الوكيل -- استخدمه للتحميل الكسول والتحكم في الوصول والتسجيل والتحقق أو التخزين المؤقت.
- المراقب -- استخدمه عندما يجب أن تؤدي التغييرات في كائن واحد إلى تحديثات في كثير من الكائنات الأخرى.
- الوسيط -- استخدمه عندما تحتاج كائنات كثيرة للتواصل والإشارات المباشرة تخلق شبكة متشابكة من التبعيات.
- الاستراتيجية -- استخدمه عندما لديك خوارزميات متعددة لنفس المهمة وتريد اختيار واحدة أثناء التشغيل.
- الأمر -- استخدمه عندما تحتاج تراجع/إعادة أو قائمة انتظار أوامر أو تسجيل العمليات.
- المكرر -- استخدمه عندما تحتاج تجاوزًا مخصصًا على المجموعات. استفد من بروتوكول المكرر المدمج في JavaScript مع Symbol.iterator.
أنماط مضادة يجب تجنبها
كما توجد أنماط جيدة، توجد أنماط مضادة -- مناهج شائعة تبدو مفيدة لكنها تؤدي إلى مشاكل. التعرف عليها لا يقل أهمية عن معرفة الأنماط الصحيحة.
- الكائن الإلهي -- كائن أو فئة واحدة تفعل كل شيء. قسّمها إلى وحدات أصغر ومركزة باستخدام مبدأ المسؤولية الواحدة.
- التحسين المبكر -- إضافة تخزين مؤقت أو وكلاء أو أنماط معقدة قبل قياس مشاكل الأداء الفعلية. قم بالتنميط أولاً ثم حسّن.
- جحيم الاستدعاءات -- استدعاءات متداخلة بعمق صعبة القراءة والصيانة. أعد البناء باستخدام الوعود أو async/await أو نمط المراقب.
- كود السباغيتي -- كود بدون هيكل واضح حيث كل شيء يعتمد على كل شيء آخر. طبّق أنماط الوحدة والوسيط لتنظيم المسؤوليات.
- المطرقة الذهبية -- استخدام نفس النمط لكل مشكلة. لكل نمط حالات استخدام محددة؛ طابق النمط مع المشكلة.
- برمجة النسخ واللصق -- تكرار الكود بدلاً من تجريده. استخدم نمط الاستراتيجية أو طريقة القالب لاستخراج المنطق المشترك.
- فئات الأشباح -- فئات موجودة فقط لتمرير البيانات بين فئات أخرى بدون إضافة قيمة. احذفها واترك الكائنات تتواصل مباشرة أو عبر وسيط.
إعادة البناء نحو الأنماط
نادرًا ما تبدأ مشروعًا باختيار الأنماط مسبقًا. بدلاً من ذلك تظهر الأنماط كلما نما كودك وحددت مشاكل متكررة. إليك كيفية التعرف على متى يحتاج كودك لنمط وكيفية إعادة البناء نحوه.
مثال: إعادة البناء من الشروط إلى الاستراتيجية
// قبل: عبارة switch متنامية (رائحة كود)
function calculateShipping(method, weight, distance) {
switch (method) {
case "standard":
return weight * 0.5 + distance * 0.1;
case "express":
return weight * 1.0 + distance * 0.3 + 5.00;
case "overnight":
return weight * 2.0 + distance * 0.5 + 15.00;
// كل طريقة جديدة تتطلب تعديل هذه الدالة...
default:
throw new Error("طريقة شحن غير معروفة");
}
}
// بعد: إعادة بناء لنمط الاستراتيجية
const shippingStrategies = {
standard: (weight, distance) =>
weight * 0.5 + distance * 0.1,
express: (weight, distance) =>
weight * 1.0 + distance * 0.3 + 5.00,
overnight: (weight, distance) =>
weight * 2.0 + distance * 0.5 + 15.00
};
// إضافة استراتيجية جديدة لا تتطلب تغيير الكود الموجود
shippingStrategies.bicycle = (weight, distance) =>
weight * 0.3 + distance * 0.05;
function calculateShippingRefactored(method, weight, distance) {
const strategy = shippingStrategies[method];
if (!strategy) {
throw new Error("طريقة شحن غير معروفة: " + method);
}
return strategy(weight, distance);
}
console.log(calculateShippingRefactored("express", 5, 100));
console.log(calculateShippingRefactored("bicycle", 2, 10));
تمرين عملي
ابنِ تطبيقًا مصغرًا يوضح أنماط تصميم متعددة تعمل معًا. أنشئ نظام إدارة مهام بالمتطلبات التالية: (1) استخدم نمط المفرد لفئة AppState تحمل حالة التطبيق العامة (قائمة المهام، المستخدم الحالي، الإعدادات). (2) استخدم نمط المصنع لإنشاء أنواع مهام مختلفة: BasicTask وBugReport وFeatureRequest، كل منها بخصائص وقواعد تحقق مختلفة. (3) استخدم نمط المراقب (EventEmitter) حتى يُخطر AppState واجهة المستخدم عند إضافة أو إزالة أو تحديث المهام. (4) استخدم نمط الأمر لتنفيذ التراجع والإعادة لعمليات المهام (إضافة، إزالة، تبديل الإكمال). أنشئ فئات AddTaskCommand وRemoveTaskCommand وToggleTaskCommand. (5) استخدم نمط الاستراتيجية لفرز المهام: حسب الأولوية أو تاريخ الاستحقاق أو تاريخ الإنشاء أو أبجديًا، مما يسمح للمستخدم بتبديل الاستراتيجيات أثناء التشغيل. (6) استخدم نمط الوكيل لإضافة التحقق للمهام -- تأكد من أن العناوين ليست فارغة وتواريخ الاستحقاق في المستقبل والأولوية بين 1 و5. (7) استخدم نمط المكرر مع دالة مولد تُنتج المهام المفلترة حسب الحالة (الكل، مكتملة، قيد الانتظار). اربط جميع الأنماط معًا بحيث: إنشاء مهمة يستخدم المصنع، وإضافتها تمر عبر وكيل محقق، وعملية الإضافة مغلفة في أمر لدعم التراجع، وتغير الحالة يُخطر المراقبين، والعرض يستخدم استراتيجية الفرز الحالية. اختبر كل نمط بشكل فردي ثم اختبر النظام المتكامل.