الأنماط المتقدمة للبرمجة الكائنية في TypeScript
بالإضافة إلى ميزات الأصناف الأساسية، يقدم TypeScript أنماطاً متقدمة تمكّن من تصميمات كائنية متطورة. هذا الدرس يستكشف الخلطات، والمزخرفات، ونوع this، وتقنيات أصناف متقدمة أخرى.
نمط الخلطات (Mixins Pattern)
الخلطات تسمح لك بتركيب السلوك من مصادر متعددة في صنف واحد. TypeScript يدعم الخلطات من خلال مزيج من تعبيرات الأصناف والواجهات:
// نوع لدوال المُنشئ
type Constructor<T = {}> = new (...args: any[]) => T;
// دالة خلطة: تضيف وظيفة الطابع الزمني
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = new Date();
getTimestamp(): Date {
return this.timestamp;
}
};
}
// دالة خلطة: تضيف وظيفة التفعيل
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isActive = false;
activate(): void {
this.isActive = true;
console.log("Activated!");
}
deactivate(): void {
this.isActive = false;
console.log("Deactivated!");
}
};
}
// الصنف الأساسي
class User {
constructor(public name: string) {}
}
// تطبيق الخلطات
const TimestampedUser = Timestamped(User);
const ActiveUser = Activatable(TimestampedUser);
const user = new ActiveUser("Alice");
console.log(user.name); // "Alice"
console.log(user.getTimestamp()); // كائن Date
user.activate(); // "Activated!"
console.log(user.isActive); // true
// دمج خلطات متعددة
function applyMixins<T extends Constructor, M extends Constructor[]>(
Base: T,
...mixins: M
): T {
return mixins.reduce((combined, mixin) => mixin(combined), Base) as T;
}
class Product {
constructor(public name: string, public price: number) {}
}
const EnhancedProduct = applyMixins(Product, Timestamped, Activatable);
const product = new EnhancedProduct("Laptop", 999);
console.log(product.name); // "Laptop"
console.log(product.price); // 999
console.log(product.getTimestamp()); // كائن Date
product.activate(); // "Activated!"
ملاحظة: الخلطات هي بديل قوي للوراثة المتعددة. تسمح لك بتركيب الوظائف من مصادر متعددة دون تعقيد التسلسلات الهرمية للوراثة التقليدية.
نوع this
نوع this الخاص في TypeScript يشير إلى نوع الصنف المحتوي، مما يمكّن من الواجهات السلسة وربط الطرق:
// واجهة سلسة باستخدام نوع this
class Calculator {
private value: number = 0;
add(n: number): this {
this.value += n;
return this;
}
subtract(n: number): this {
this.value -= n;
return this;
}
multiply(n: number): this {
this.value *= n;
return this;
}
divide(n: number): this {
if (n !== 0) {
this.value /= n;
}
return this;
}
getResult(): number {
return this.value;
}
}
// ربط الطرق
const result = new Calculator()
.add(10)
.multiply(2)
.subtract(5)
.getResult();
console.log(result); // 15
// نوع This يعمل مع الوراثة
class ScientificCalculator extends Calculator {
power(n: number): this {
this.add(Math.pow(this.getResult(), n) - this.getResult());
return this;
}
sqrt(): this {
this.add(Math.sqrt(this.getResult()) - this.getResult());
return this;
}
}
const sciResult = new ScientificCalculator()
.add(16)
.sqrt() // يعيد ScientificCalculator، وليس Calculator
.power(2) // لا يزال قابلاً للربط
.getResult();
console.log(sciResult); // 16
// نمط Builder مع نوع this
class QueryBuilder<T> {
private filters: string[] = [];
private sortField?: keyof T;
private limit?: number;
where(field: keyof T, value: any): this {
this.filters.push(`${String(field)} = ${value}`);
return this;
}
orderBy(field: keyof T): this {
this.sortField = field;
return this;
}
take(count: number): this {
this.limit = count;
return this;
}
build(): string {
let query = "SELECT * FROM table";
if (this.filters.length > 0) {
query += ` WHERE ${this.filters.join(" AND ")}`;
}
if (this.sortField) {
query += ` ORDER BY ${String(this.sortField)}`;
}
if (this.limit) {
query += ` LIMIT ${this.limit}`;
}
return query;
}
}
interface User {
id: number;
name: string;
age: number;
}
const query = new QueryBuilder<User>()
.where("age", 25)
.where("name", "Alice")
.orderBy("id")
.take(10)
.build();
console.log(query);
// SELECT * FROM table WHERE age = 25 AND name = Alice ORDER BY id LIMIT 10
الأصناف العامة مع القيود
ادمج الأنواع العامة مع قيود الأصناف للحصول على تجريدات قوية:
// نمط المستودع العام
interface Entity {
id: number;
}
class Repository<T extends Entity> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
findById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
findAll(): T[] {
return [...this.items];
}
update(id: number, updates: Partial<T>): boolean {
const item = this.findById(id);
if (item) {
Object.assign(item, updates);
return true;
}
return false;
}
delete(id: number): boolean {
const index = this.items.findIndex(item => item.id === id);
if (index !== -1) {
this.items.splice(index, 1);
return true;
}
return false;
}
count(): number {
return this.items.length;
}
}
// الاستخدام مع أنواع كيانات محددة
interface User extends Entity {
name: string;
email: string;
}
interface Product extends Entity {
name: string;
price: number;
}
const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: "Alice", email: "alice@example.com" });
userRepo.add({ id: 2, name: "Bob", email: "bob@example.com" });
const user = userRepo.findById(1);
console.log(user); // { id: 1, name: "Alice", email: "alice@example.com" }
userRepo.update(1, { email: "newalice@example.com" });
console.log(userRepo.findById(1)?.email); // "newalice@example.com"
const productRepo = new Repository<Product>();
productRepo.add({ id: 1, name: "Laptop", price: 999 });
console.log(productRepo.count()); // 1
نصيحة: نمط المستودع مع الأنواع العامة ممتاز لإنشاء طبقات وصول بيانات قابلة لإعادة الاستخدام. يوفر سلامة الأنواع مع تقليل تكرار الكود عبر أنواع الكيانات المختلفة.
الطرق الثابتة المجردة (TypeScript 4.2+)
TypeScript 4.2 قدم القدرة على التصريح عن طرق ثابتة مجردة في الأصناف المجردة:
// صنف مجرد مع طريقة ثابتة مجردة
abstract class DatabaseModel {
abstract id: number;
// طريقة مثيل مجردة
abstract save(): Promise<void>;
// طريقة ثابتة مجردة
static abstract tableName: string;
static abstract findById(id: number): Promise<any>;
// طريقة ثابتة ملموسة
static async findAll<T extends DatabaseModel>(
this: typeof DatabaseModel & { tableName: string; new(): T }
): Promise<T[]> {
console.log(`Finding all from ${this.tableName}`);
// منطق استعلام قاعدة البيانات هنا
return [];
}
}
class User extends DatabaseModel {
static tableName = "users";
constructor(
public id: number,
public name: string,
public email: string
) {
super();
}
async save(): Promise<void> {
console.log(`Saving user ${this.name} to ${User.tableName}`);
}
static async findById(id: number): Promise<User | null> {
console.log(`Finding user by id ${id} from ${this.tableName}`);
// منطق استعلام قاعدة البيانات
return null;
}
}
class Product extends DatabaseModel {
static tableName = "products";
constructor(
public id: number,
public name: string,
public price: number
) {
super();
}
async save(): Promise<void> {
console.log(`Saving product ${this.name} to ${Product.tableName}`);
}
static async findById(id: number): Promise<Product | null> {
console.log(`Finding product by id ${id} from ${this.tableName}`);
return null;
}
}
// الاستخدام
const user = new User(1, "Alice", "alice@example.com");
await user.save(); // "Saving user Alice to users"
const foundUser = await User.findById(1); // "Finding user by id 1 from users"
const allUsers = await User.findAll(); // "Finding all from users"
المُنشئات المحمية (Protected Constructors)
المُنشئات المحمية تمنع الإنشاء المباشر مع السماح بإنشاء مثيلات من الأصناف الفرعية:
// نمط Singleton مع مُنشئ محمي
class Logger {
private static instance: Logger;
protected constructor() {
// المُنشئ المحمي يمنع: new Logger()
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
// لا يمكن الإنشاء مباشرة
// const logger = new Logger(); // خطأ: المُنشئ محمي
// يجب استخدام الطريقة الثابتة
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true (نفس المثيل)
// لكن يمكن التوسيع مع مُنشئ محمي
class ConsoleLogger extends Logger {
constructor() {
super(); // يمكن استدعاء المُنشئ المحمي
}
log(message: string): void {
console.log(`[CONSOLE] ${message}`);
}
}
const consoleLogger = new ConsoleLogger(); // موافق في الصنف الفرعي
consoleLogger.log("Hello"); // "[CONSOLE] Hello"
تحذير: يجب استخدام أنماط Singleton باعتدال. يمكن أن تجعل الاختبار صعباً وتنشئ تبعيات مخفية. فكر في حقن التبعيات أو أنماط أخرى عندما يكون ذلك ممكناً.
التحميل الزائد للطرق (Method Overloading)
TypeScript يدعم التحميل الزائد للطرق من خلال توقيعات الدوال:
class DataParser {
// توقيعات التحميل الزائد
parse(data: string): object;
parse(data: string[]): object[];
parse(data: object): string;
// توقيع التنفيذ (يجب أن يكون متوافقاً مع جميع التحميلات الزائدة)
parse(data: string | string[] | object): object | object[] | string {
if (typeof data === "string") {
return JSON.parse(data);
} else if (Array.isArray(data)) {
return data.map(item => JSON.parse(item));
} else {
return JSON.stringify(data);
}
}
}
const parser = new DataParser();
const obj = parser.parse('{"name":"Alice"}'); // النوع: object
console.log(obj); // { name: "Alice" }
const arr = parser.parse(['{"id":1}', '{"id":2}']); // النوع: object[]
console.log(arr); // [{ id: 1 }, { id: 2 }]
const str = parser.parse({ name: "Bob" }); // النوع: string
console.log(str); // '{"name":"Bob"}'
// تحميل زائد للمُنشئ
class Point {
constructor(x: number, y: number);
constructor(coords: { x: number; y: number });
constructor(xOrCoords: number | { x: number; y: number }, y?: number) {
if (typeof xOrCoords === "number") {
this.x = xOrCoords;
this.y = y!;
} else {
this.x = xOrCoords.x;
this.y = xOrCoords.y;
}
}
constructor(public x: number, public y: number) {}
}
const point1 = new Point(10, 20);
const point2 = new Point({ x: 10, y: 20 });
أسماء الحقول الخاصة (ES2022)
TypeScript يدعم حقول ECMAScript الخاصة باستخدام البادئة #، والتي توفر خصوصية وقت التشغيل (على عكس الكلمة المفتاحية private في TypeScript والتي هي وقت الترجمة فقط):
class BankAccount {
// حقل ECMAScript خاص (خاص وقت التشغيل)
#balance: number;
// حقل TypeScript خاص (وقت الترجمة فقط)
private accountNumber: string;
constructor(initialBalance: number) {
this.#balance = initialBalance;
this.accountNumber = this.generateAccountNumber();
}
deposit(amount: number): void {
this.#balance += amount;
}
getBalance(): number {
return this.#balance;
}
// طريقة خاصة مع بناء جملة #
#validateAmount(amount: number): boolean {
return amount > 0 && amount <= this.#balance;
}
withdraw(amount: number): boolean {
if (this.#validateAmount(amount)) {
this.#balance -= amount;
return true;
}
return false;
}
private generateAccountNumber(): string {
return Math.random().toString(36).substr(2, 9);
}
}
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// account.#balance; // خطأ بناء جملة - حقل خاص
// account.accountNumber; // خطأ TypeScript - خاصية خاصة
// الفرق:
// - private في TypeScript: يُحذف في وقت التشغيل، يُفرض فقط في وقت الترجمة
// - #private في ECMAScript: خاص حقاً في وقت التشغيل، لا يمكن الوصول إليه
الكتل الثابتة (TypeScript 4.4+)
الكتل الثابتة تسمح بمنطق تهيئة ثابت معقد:
class Configuration {
static apiKey: string;
static endpoint: string;
static maxRetries: number;
// كتلة ثابتة للتهيئة
static {
// منطق تهيئة معقد
const env = process.env.NODE_ENV || "development";
if (env === "production") {
this.apiKey = process.env.PROD_API_KEY || "";
this.endpoint = "https://api.example.com";
this.maxRetries = 5;
} else {
this.apiKey = "dev-key-12345";
this.endpoint = "http://localhost:3000";
this.maxRetries = 3;
}
console.log(`Configuration loaded for ${env}`);
}
// كتل ثابتة متعددة يتم تنفيذها بالترتيب
static {
console.log("Additional initialization");
}
static getConfig() {
return {
apiKey: this.apiKey,
endpoint: this.endpoint,
maxRetries: this.maxRetries
};
}
}
// الكتل الثابتة تعمل عند تقييم الصنف
console.log(Configuration.getConfig());
تمرين: أنشئ صنف عام Cache<K, V> مع نوع إرجاع this لربط الطرق. نفذ طرق set(key: K, value: V), get(key: K), has(key: K), delete(key: K), و clear(). أضف خلطة Timestamped تتتبع متى تمت إضافة كل إدخال. قم بتوسيع الذاكرة المؤقتة بصنف فرعي TTLCache يزيل الإدخالات المنتهية الصلاحية تلقائياً. استخدم التحميل الزائد للطرق لطريقة set لقبول معامل TTL اختياري.
أنماط المزخرفات (Decorator Patterns - تجريبي)
على الرغم من أن المزخرفات لا تزال تجريبية في TypeScript، إلا أنها توفر طريقة قوية لإضافة بيانات وصفية وتعديل سلوك الصنف:
// ملاحظة: فعّل "experimentalDecorators" في tsconfig.json
// مزخرف صنف
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
// مزخرف طريقة
function log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
// مزخرف خاصية
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false
});
}
@sealed
class Calculator {
@readonly
version = "1.0";
@log
add(a: number, b: number): number {
return a + b;
}
@log
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// Calling add with args: [2, 3]
// Result: 5
calc.multiply(4, 5);
// Calling multiply with args: [4, 5]
// Result: 20
ملاحظة: المزخرفات حالياً مقترح TC39 في المرحلة 3. دعم المزخرفات التجريبي في TypeScript يختلف قليلاً عن المقترح. تحقق دائماً من وثائق TypeScript الأحدث لاستخدام المزخرفات.
الخلاصة
الأنماط المتقدمة للأصناف في TypeScript تمكّن من تصميمات كائنية متطورة. الخلطات توفر التركيب، ونوع this يمكّن من الواجهات السلسة، والقيود العامة تنشئ تجريدات قابلة لإعادة الاستخدام، وميزات مثل المُنشئات المحمية، والتحميل الزائد للطرق، والحقول الخاصة توفر تحكماً دقيقاً. هذه الأنماط ضرورية لبناء تطبيقات مؤسسية قابلة للتوسع والصيانة.