الفئات المجردة والواجهات
ما هي الفئات المجردة؟
الفئة المجردة هي فئة لا يمكن إنشاء نسخة منها مباشرة -- لا يمكنك إنشاء كائن منها باستخدام new. بدلاً من ذلك تعمل كمخطط يجب على الفئات الأخرى أن ترثه وتكمله. الفئات المجردة يمكن أن تحتوي على طرق مجردة (بدون تنفيذ فقط التوقيع) وطرق ملموسة (بتنفيذ كامل). استخدم الكلمة المفتاحية abstract قبل class.
فكر في الفئة المجردة مثل وصفة غير مكتملة: تخبرك بالخطوات (توقيعات الطرق) لكن بعض الخطوات تقول “أنت تقرر كيف تفعل هذا الجزء” (الطرق المجردة). كل فئة فرعية يجب أن تملأ تلك الفراغات.
أول فئة مجردة لك
// لا يمكن إنشاء: Shape() -- إنها مجردة
abstract class Shape {
String color;
Shape(this.color);
// طريقة مجردة -- بدون جسم فقط التوقيع
// الفئات الفرعية يجب أن تنفذها
double get area;
// طريقة مجردة
double get perimeter;
// طريقة ملموسة -- لها جسم ترثها الفئات الفرعية
void describe() {
print('شكل $color: المساحة=${area.toStringAsFixed(2)}، المحيط=${perimeter.toStringAsFixed(2)}');
}
}
class Circle extends Shape {
double radius;
Circle(String color, this.radius) : super(color);
@override
double get area => 3.14159 * radius * radius;
@override
double get perimeter => 2 * 3.14159 * radius;
}
class Rectangle extends Shape {
double width;
double height;
Rectangle(String color, this.width, this.height) : super(color);
@override
double get area => width * height;
@override
double get perimeter => 2 * (width + height);
}
void main() {
// Shape s = Shape('أحمر'); // خطأ! لا يمكن إنشاء فئة مجردة
Circle c = Circle('أزرق', 5);
Rectangle r = Rectangle('أخضر', 10, 4);
c.describe(); // شكل أزرق: المساحة=78.54، المحيط=31.42
r.describe(); // شكل أخضر: المساحة=40.00، المحيط=28.00
// يمكن استخدام النوع المجرد كنوع متغير
List<Shape> shapes = [c, r];
double totalArea = shapes.fold(0, (sum, s) => sum + s.area);
print('المساحة الإجمالية: ${totalArea.toStringAsFixed(2)}'); // 118.54
}
abstract. المترجم سيعطيك خطأ إذا نسيت تنفيذ طريقة مجردة.الطرق المجردة مقابل الطرق الملموسة
الفئة المجردة يمكنها مزج النوعين بحرية:
مزج المجرد والملموس
abstract class Animal {
String name;
Animal(this.name);
// مجردة -- كل حيوان يصدر صوتاً مختلفاً
void makeSound();
// مجردة -- كل حيوان يتحرك بشكل مختلف
void move();
// ملموسة -- كل الحيوانات تأكل بنفس الطريقة
void eat(String food) {
print('$name يأكل $food.');
}
// ملموسة تعتمد على مجردة
void introduce() {
print('أنا $name.');
makeSound(); // تستدعي تنفيذ الفئة الفرعية!
move();
}
}
class Dog extends Animal {
Dog(String name) : super(name);
@override
void makeSound() => print('$name: هاو! هاو!');
@override
void move() => print('$name يركض على أربع أرجل.');
}
class Bird extends Animal {
Bird(String name) : super(name);
@override
void makeSound() => print('$name: تغريد! تغريد!');
@override
void move() => print('$name يطير في الهواء.');
}
void main() {
Dog dog = Dog('ريكس');
Bird bird = Bird('تويتي');
dog.introduce();
// أنا ريكس.
// ريكس: هاو! هاو!
// ريكس يركض على أربع أرجل.
bird.introduce();
// أنا تويتي.
// تويتي: تغريد! تغريد!
// تويتي يطير في الهواء.
// الطريقة الملموسة موروثة
dog.eat('عظام'); // ريكس يأكل عظام.
}
introduce() طريقة ملموسة تستدعي الطرق المجردة makeSound() و move(). الأب يحدد تسلسل العمليات بينما الفئات الفرعية تحدد السلوك المحدد. هذا يسمى نمط طريقة القالب -- أحد أكثر أنماط التصميم فائدة في OOP.الواجهات في Dart
على عكس Java أو C# لا تملك Dart كلمة مفتاحية interface منفصلة. بدلاً من ذلك كل فئة هي ضمنياً واجهة. أي فئة يمكن استخدامها كواجهة مع الكلمة المفتاحية implements. عندما تنفذ فئة يجب أن تتجاوز جميع أعضائها -- الخصائص والطرق.
استخدام implements
// هذه الفئة تعمل تلقائياً كواجهة
class Printable {
void printFormatted() {
print('مخرجات الطباعة الافتراضية');
}
}
class Savable {
void save() {
print('حفظ افتراضي');
}
void delete() {
print('حذف افتراضي');
}
}
// ينفذ كلاهما -- يجب تجاوز جميع الأعضاء من كليهما
class Document implements Printable, Savable {
String title;
String content;
Document(this.title, this.content);
@override
void printFormatted() {
print('=== $title ===');
print(content);
print('===============');
}
@override
void save() {
print('حفظ المستند "$title" على القرص...');
}
@override
void delete() {
print('حذف المستند "$title"...');
}
}
void main() {
Document doc = Document('تقريري', 'هذا هو المحتوى.');
doc.printFormatted();
doc.save();
// يمكن استخدام أنواع الواجهات
Printable p = doc;
p.printFormatted(); // يعمل! Doc ينفذ Printable
}
•
extends -- وراثة التنفيذ. تحصل على كل كود الأب مجاناً وتتجاوز فقط ما تريد تغييره. يمكن وراثة فئة واحدة فقط.•
implements -- الوعد بتوفير التنفيذ. لا تحصل على شيء مجاناً ويجب تجاوز كل عضو. يمكن تنفيذ عدة فئات.استخدم
extends عندما تريد إعادة استخدام الكود. استخدم implements عندما تريد ضمان أن فئة تتبع عقداً.الفئات المجردة كواجهات
أكثر نمط شيوعاً هو تعريف فئات مجردة خصيصاً لاستخدامها كواجهات. هذا يعطيك طرقاً مجردة (العقد) بدون أي تنفيذ يحتاج للتجاوز.
نمط الواجهة المجردة
// تعريف العقود بفئات مجردة
abstract class AuthService {
Future<bool> login(String email, String password);
Future<void> logout();
bool get isLoggedIn;
String? get currentUserEmail;
}
// تنفيذ Firebase
class FirebaseAuth implements AuthService {
bool _loggedIn = false;
String? _email;
@override
Future<bool> login(String email, String password) async {
print('Firebase: تسجيل دخول $email...');
_loggedIn = true;
_email = email;
return true;
}
@override
Future<void> logout() async {
_loggedIn = false;
_email = null;
}
@override
bool get isLoggedIn => _loggedIn;
@override
String? get currentUserEmail => _email;
}
// تنفيذ وهمي للاختبار
class MockAuth implements AuthService {
bool _loggedIn = false;
@override
Future<bool> login(String email, String password) async {
_loggedIn = email == 'test@test.com' && password == 'password';
return _loggedIn;
}
@override
Future<void> logout() async => _loggedIn = false;
@override
bool get isLoggedIn => _loggedIn;
@override
String? get currentUserEmail => _loggedIn ? 'test@test.com' : null;
}
// الكود يعتمد على الواجهة وليس التنفيذ
class LoginScreen {
final AuthService auth; // يعمل مع أي AuthService
LoginScreen(this.auth);
Future<void> handleLogin(String email, String password) async {
bool success = await auth.login(email, password);
if (success) {
print('مرحباً ${auth.currentUserEmail}!');
} else {
print('فشل تسجيل الدخول.');
}
}
}
void main() async {
// الإنتاج: استخدم Firebase
var screen = LoginScreen(FirebaseAuth());
await screen.handleLogin('ahmed@test.com', 'secret123');
// الاختبار: استخدم Mock
var testScreen = LoginScreen(MockAuth());
await testScreen.handleLogin('test@test.com', 'password');
}
get_it و provider تجعل هذا أقوى مع حقن التبعيات.دمج extends و implements
يمكن للفئة أن ترث من أب وتنفذ واجهات في نفس الوقت:
extends + implements معاً
abstract class Loggable {
void log(String message);
}
abstract class Serializable {
Map<String, dynamic> toJson();
}
class BaseModel {
final String id;
final DateTime createdAt;
BaseModel({required this.id}) : createdAt = DateTime.now();
}
// يرث من BaseModel وينفذ واجهتين
class User extends BaseModel implements Loggable, Serializable {
String name;
String email;
User({required super.id, required this.name, required this.email});
@override
void log(String message) {
print('[مستخدم:$id] $message');
}
@override
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
'created_at': createdAt.toIso8601String(),
};
@override
String toString() => 'مستخدم($name, $email)';
}
void main() {
var user = User(id: 'u001', name: 'أحمد', email: 'ahmed@test.com');
print(user.id); // u001 (من BaseModel)
user.log('تم تحديث الملف'); // [مستخدم:u001] تم تحديث الملف
print(user.toJson());
// يمكن استخدامه كأي من أنواعه
Loggable loggable = user;
Serializable serializable = user;
BaseModel base = user;
}
الكلمة المفتاحية interface (Dart 3+)
قدم Dart 3 مُعدّلات الفئات لمزيد من التحكم:
مُعدّلات فئات Dart 3
// sealed class -- يمكن الوراثة فقط في نفس الملف (رائعة للتبديل الشامل)
sealed class Result {}
class Success extends Result {
final String data;
Success(this.data);
}
class Failure extends Result {
final String error;
Failure(this.error);
}
class Loading extends Result {}
String handleResult(Result result) {
// Dart تعرف جميع الأنواع الفرعية الممكنة -- شامل!
return switch (result) {
Success(data: var d) => 'حصلنا على: $d',
Failure(error: var e) => 'خطأ: $e',
Loading() => 'جاري التحميل...',
};
}
void main() {
print(handleResult(Success('مرحبا'))); // حصلنا على: مرحبا
print(handleResult(Failure('404'))); // خطأ: 404
print(handleResult(Loading())); // جاري التحميل...
}
•
abstract -- لا يمكن إنشاء نسخة مباشرة.•
interface -- يمكن التنفيذ لكن لا يمكن الوراثة (خارج مكتبتها).•
base -- يمكن الوراثة لكن لا يمكن التنفيذ (خارج مكتبتها).•
final -- لا يمكن الوراثة أو التنفيذ (خارج مكتبتها).•
sealed -- لا يمكن الوراثة خارج نفس الملف. تُمكّن مطابقة الأنماط الشاملة في switch.•
mixin -- يمكن استخدامها كمزيج (تُغطى في الدرس التالي).مثال عملي: بنية الإضافات
بناء نظام إضافات الدفع
// تعريف عقد الواجهة
abstract class PaymentGateway {
String get name;
bool get isAvailable;
Future<PaymentResult> processPayment({
required double amount,
required String currency,
});
}
class PaymentResult {
final bool success;
final String transactionId;
final String? errorMessage;
PaymentResult.success(this.transactionId)
: success = true, errorMessage = null;
PaymentResult.failure(this.errorMessage)
: success = false, transactionId = '';
@override
String toString() => success
? 'الدفع تم (معاملة: $transactionId)'
: 'فشل الدفع: $errorMessage';
}
class StripeGateway implements PaymentGateway {
@override
String get name => 'Stripe';
@override
bool get isAvailable => true;
@override
Future<PaymentResult> processPayment({
required double amount,
required String currency,
}) async {
print('Stripe: معالجة \$$amount $currency...');
return PaymentResult.success('stripe_${DateTime.now().millisecondsSinceEpoch}');
}
}
// خدمة الدفع تعتمد على الواجهة
class CheckoutService {
final PaymentGateway gateway;
CheckoutService(this.gateway);
Future<void> checkout(double amount) async {
if (!gateway.isAvailable) {
print('${gateway.name} غير متاح.');
return;
}
var result = await gateway.processPayment(amount: amount, currency: 'USD');
print(result);
}
}
void main() async {
var checkout = CheckoutService(StripeGateway());
await checkout.checkout(99.99);
}
تمرين عملي
افتح DartPad وابنِ: (1) أنشئ فئة مجردة DataRepository بطرق مجردة: Future<List<Map>> getAll() و Future<Map?> getById(String id) و Future<void> create(Map data) و Future<void> update(String id, Map data) و Future<void> delete(String id) وطريقة ملموسة exists(String id) تستدعي getById. (2) أنشئ InMemoryRepository implements DataRepository تخزن البيانات في List<Map>. (3) أنشئ LoggingRepository extends InMemoryRepository تتجاوز كل طريقة لطباعة سجل قبل استدعاء super. (4) استخدم متغير DataRepository للعمل مع كلا التنفيذين. (5) أنشئ فئة sealed ApiResponse بأنواع فرعية Success و Error و Loading وتعامل معها بتعبير switch شامل.