| وجه المقارنة المعمارية | إدارة الحالة داخل صنف المكون (Component State) | استخدام متجر بيانات محلي مصغر (Signals Store) |
|---|---|---|
| فصل المسؤوليات المعمارية | ضعيف، حيث تختلط منطق واجهة المستخدم بمعالجة البيانات وتعديلها في نفس المكان | عالي وممتاز، حيث يُعزل منطق البيانات والحالة تماماً عن صنف المكون الرسومي |
| إمكانية إعادة الاستخدام | محدودة للغاية ومحصورة داخل المكون الحالي فقط ولا يمكن مشاركتها بسهولة | عالية جداً، حيث يمكن حقن المتجر في أي مكون فرعي أو شقيق يحتاج لنفس البيانات |
| تنظيم الأكواد والصيانة | يؤدي إلى تضخم كود المكونات الكبيرة والصعوبة في تتبع مصادر تعديل البيانات | يبقي الأكواد نظيفة، مقسمة بوضوح، وسهلة جداً لاختبارات الوحدة (Unit Testing) |
عندما تتطلب معمارية التطبيق في Angular 22.1 حلاً وسطياً يجمع بين خفة الخدمات وسلطة المتاجر المركزية، يبرز نمط متجر البيانات المحلي المصغر (Signals-based Store). هذا النمط المعماري يتيح للمطور عزل بيانات مكون أو ميزة معينة بالكامل داخل كلاس خدمة مخصص يعتمد على الـ Signals, مما يوفر بيئة تفاعلية صارمة وآمنة لإدارة الحالة المحلية دون الحاجة لاستيراد مكتبات خارجية ثقيلة.
يعتمد تصميم المتجر المصغر على إخفاء الإشارات الحقيقية القابلة للتعديل (Writable Signals) داخل الخدمة، وتعريض نسخ للقراءة فقط (Readonly Signals) أو خصائص محسوبة (Computed) للخارج، مما يمنع التعديلات العشوائية من خارج المتجر.
من خلال توفير دوال وميثودات واضحة داخل المتجر (مثل addItem أو removeItem), يتم ضمان مرور أي تعديل على الحالة عبر مسار هندسي واحد ومراقب، مما يمنع الأخطاء غير المتوقعة في تزامن واجهة المستخدم.
| 1 | import { Injectable, signal, computed } from '@angular/core'; |
| 2 | |
| 3 | export interface ProductItem { |
| 4 | id: number; |
| 5 | title: string; |
| 6 | price: number; |
| 7 | inStock: boolean; |
| 8 | } |
| 9 | |
| 10 | @Injectable({ |
| 11 | providedIn: 'root' |
| 12 | }) |
| 13 | export class ProductStoreService { |
| 14 | private productsSignal = signal<ProductItem[]>([ |
| 15 | { id: 1, title: 'حاسوب هندسي متطور', price: 1450, inStock: true }, |
| 16 | { id: 2, title: 'شاشة عرض فائقة الوضوح', price: 320, inStock: false }, |
| 17 | { id: 3, title: 'لوحة مفاتيح ميكانيكية', price: 110, inStock: true } |
| 18 | ]); |
| 19 | |
| 20 | readonly products = this.productsSignal.asReadonly(); |
| 21 | |
| 22 | readonly availableCount = computed(() => |
| 23 | this.productsSignal().filter(p => p.inStock).length |
| 24 | ); |
| 25 | |
| 26 | addProduct(newProduct: ProductItem) { |
| 27 | this.productsSignal.update(current => [...current, newProduct]); |
| 28 | } |
| 29 | |
| 30 | toggleStockStatus(id: number) { |
| 31 | this.productsSignal.update(list => |
| 32 | list.map(item => item.id === id ? { ...item, inStock: !item.inStock } : item) |
| 33 | ); |
| 34 | } |
| 35 | } |
| 1 | <!-- product-store.component.html --> |
| 2 | <div class="master-container"> |
| 3 | <h3>متجر البيانات المحلي المصغر باستخدام الإشارات</h3> |
| 4 | <p>عدد المنتجات المتاحة حالياً في المخزن: <strong>{{ productStore.availableCount() }}</strong></p> |
| 5 | |
| 6 | <div class="grid-container"> |
| 7 | @for (product of productStore.products(); track product.id) { |
| 8 | <div class="card" [style.border-color]="product.inStock ? '#34d399' : '#f87171'"> |
| 9 | <h4>{{ product.title }}</h4> |
| 10 | <p>السعر: {{ product.price }} دولار</p> |
| 11 | <p>الحالة: <span>{{ product.inStock ? 'متوفر' : 'غير متوفر' }}</span></p> |
| 12 | <button (click)="productStore.toggleStockStatus(product.id)" class="toggle-btn">تبديل حالة المخزن</button> |
| 13 | </div> |
| 14 | } |
| 15 | </div> |
| 16 | |
| 17 | <button (click)="addNewItem()" class="action-btn">إضافة منتج هندسي جديد</button> |
| 18 | </div> |
| 1 | /* product-store.component.css */ |
| 2 | .master-container { |
| 3 | background: #090d16; |
| 4 | color: #fff; |
| 5 | padding: 24px; |
| 6 | border-radius: 12px; |
| 7 | } |
| 8 | |
| 9 | .grid-container { |
| 10 | display: grid; |
| 11 | grid-template-columns: repeat(3, 1fr); |
| 12 | gap: 12px; |
| 13 | margin: 16px 0; |
| 14 | } |
| 15 | |
| 16 | .card { |
| 17 | background: #1e293b; |
| 18 | padding: 16px; |
| 19 | border-radius: 8px; |
| 20 | border: 1px solid #334155; |
| 21 | } |
| 22 | |
| 23 | .card h4 { |
| 24 | margin-top: 0; |
| 25 | color: #38bdf8; |
| 26 | } |
| 27 | |
| 28 | .toggle-btn { |
| 29 | background: #334155; |
| 30 | color: #fff; |
| 31 | border: none; |
| 32 | padding: 6px 12px; |
| 33 | border-radius: 4px; |
| 34 | cursor: pointer; |
| 35 | margin-top: 8px; |
| 36 | } |
| 37 | |
| 38 | .toggle-btn:hover { |
| 39 | background: #475569; |
| 40 | } |
| 41 | |
| 42 | .action-btn { |
| 43 | background: #38bdf8; |
| 44 | color: #090d16; |
| 45 | border: none; |
| 46 | padding: 10px 20px; |
| 47 | border-radius: 6px; |
| 48 | font-weight: bold; |
| 49 | cursor: pointer; |
| 50 | margin-top: 8px; |
| 51 | } |
| 52 | |
| 53 | .action-btn:hover { |
| 54 | background: #7dd3fc; |
| 55 | } |