مشروع: نمذجة حساب مصرفي
تجمع هذه الدرس الأخير كل ما تعلّمته في هذه الوحدة: الكلاسات والكائنات، والحقول والحالة، والبنّاؤون، والتغليف، ومُعدِّلات الوصول، والـ getters والـ setters، والتحقّق من الإدخال. بدلًا من أمثلة منفصلة، ستبني كلاس BankAccount واحدًا وواقعيًا من الصفر — خطوة بخطوة — وستفهم تمامًا لماذا تُتّخذ كل قرار تصميمي.
لماذا الحساب المصرفي؟
الحساب المصرفي حالة دراسية كلاسيكية في البرمجة الكائنية لأنه يتضمّن قواعد طبيعية تعكس قيودًا واقعية:
- يجب أن يكون الرصيد محميًا تمامًا — البنك وحده يقرّر متى وكيف يتغيّر.
- يجب أن تكون الإيداعات موجبة.
- يجب ألّا تتجاوز السحوبات الرصيد المتاح.
- يحتاج كل حساب إلى صاحب وحالة ابتدائية تُضبط عند الإنشاء.
تنعكس هذه القواعد بشكل مثالي على التغليف والتحقّق من الإدخال — المهارتان اللتان طوّرتهما في الدرسين الرابع والخامس.
الخطوة 1 — تعريف الكلاس وحقوله الخاصة
ابدأ بالبيانات. يحتاج الحساب المصرفي إلى رقم حساب واسم صاحب ورصيد. الثلاثة جميعًا private لأنّه لا ينبغي لأي كود خارجي أن يلمسها مباشرة.
public class BankAccount {
private final String accountNumber; // يُضبط مرة واحدة ولا يتغيّر
private final String ownerName;
private double balance;
}
لماذا final لرقم الحساب والاسم؟ بعد فتح الحساب، لا يتغيّر رقم الحساب ولا اسم صاحبه. وضع final يجعل هذا العقد صريحًا ويجعل المُصرِّف يفرضه. أمّا balance فليس final لأنّه يُفترض أن يتغيّر مع كل عملية.
الخطوة 2 — كتابة البنّاء
البنّاء هو المكان الوحيد الذي يضبط الحالة الابتدائية. يتحقّق من المدخلات قبل قبولها، لذا يُولَد كائن BankAccount دائمًا في حالة صحيحة.
public BankAccount(String accountNumber, String ownerName, double initialBalance) {
if (accountNumber == null || accountNumber.isBlank()) {
throw new IllegalArgumentException("Account number cannot be blank.");
}
if (ownerName == null || ownerName.isBlank()) {
throw new IllegalArgumentException("Owner name cannot be blank.");
}
if (initialBalance < 0) {
throw new IllegalArgumentException("Initial balance cannot be negative.");
}
this.accountNumber = accountNumber;
this.ownerName = ownerName;
this.balance = initialBalance;
}
لاحظ البادئة this. — إنّها تُميّز الحقل عن المعامل حين يشتركان في الاسم ذاته، تمامًا كما تعلّمت في الدرس الثالث.
الخطوة 3 — إضافة الإيداع مع التحقّق
الإيداع يزيد الرصيد، لكن فقط إذا كان المبلغ موجبًا بالفعل.
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive.");
}
balance += amount;
System.out.println("Deposited " + amount + ". New balance: " + balance);
}
تحقّق أولًا، ثم تصرّف. تحقّق دائمًا من أن الإدخال منطقي قبل تغيير أي حالة. إن حدّثت balance ثم رمَيت استثناءً، سيبقى الكائن في حالة فاسدة. بالتحقّق في أعلى الدالة، إمّا أن ينجح كل شيء أو لا يتغيّر شيء.
الخطوة 4 — إضافة السحب مع التحقّق
السحب يُنقص الرصيد، لكن تسري قاعدتان: يجب أن يكون المبلغ موجبًا، ويجب أن تكون هناك أموال كافية.
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive.");
}
if (amount > balance) {
throw new IllegalStateException(
"Insufficient funds. Balance: " + balance + ", requested: " + amount
);
}
balance -= amount;
System.out.println("Withdrew " + amount + ". New balance: " + balance);
}
استخدم نوع الاستثناء المناسب. IllegalArgumentException يشير إلى أن المُستدعي أرسل بيانات خاطئة (مبلغ سالب). IllegalStateException يشير إلى أن الكائن في حالة خاطئة للعملية (رصيد غير كافٍ). اختيار النوع الصحيح يجعل رسائل الخطأ أسهل في الفهم والتنقيح.
الخطوة 5 — إضافة الـ getters للقراءة فقط
يحتاج الكود الخارجي إلى قراءة الرصيد والمعلومات، لكن ليس لكتابتها مباشرة. وفّر getters بدون setters مقابلة — وذلك مقصود.
public String getAccountNumber() { return accountNumber; }
public String getOwnerName() { return ownerName; }
public double getBalance() { return balance; }
لا توجد setBalance(). الطريقة الوحيدة لتغيير الرصيد هي عبر deposit() أو withdraw()، وكلتاهما تطبّق قواعد العمل. هذا هو التغليف يؤدّي وظيفته تمامًا.
الخطوة 6 — تجاوز toString
دالة toString() المفيدة تتيح طباعة الحساب بشكل واضح أثناء التنقيح أو التسجيل.
@Override
public String toString() {
return "BankAccount{accountNumber='" + accountNumber
+ "', owner='" + ownerName
+ "', balance=" + balance + "}";
}
جمع كل شيء معًا
إليك الكلاس الكامل يليه برنامج صغير يختبر كل دالة:
public class BankAccount {
private final String accountNumber;
private final String ownerName;
private double balance;
public BankAccount(String accountNumber, String ownerName, double initialBalance) {
if (accountNumber == null || accountNumber.isBlank())
throw new IllegalArgumentException("Account number cannot be blank.");
if (ownerName == null || ownerName.isBlank())
throw new IllegalArgumentException("Owner name cannot be blank.");
if (initialBalance < 0)
throw new IllegalArgumentException("Initial balance cannot be negative.");
this.accountNumber = accountNumber;
this.ownerName = ownerName;
this.balance = initialBalance;
}
public void deposit(double amount) {
if (amount <= 0)
throw new IllegalArgumentException("Deposit amount must be positive.");
balance += amount;
System.out.println("Deposited " + amount + ". New balance: " + balance);
}
public void withdraw(double amount) {
if (amount <= 0)
throw new IllegalArgumentException("Withdrawal amount must be positive.");
if (amount > balance)
throw new IllegalStateException("Insufficient funds.");
balance -= amount;
System.out.println("Withdrew " + amount + ". New balance: " + balance);
}
public String getAccountNumber() { return accountNumber; }
public String getOwnerName() { return ownerName; }
public double getBalance() { return balance; }
@Override
public String toString() {
return "BankAccount{accountNumber='" + accountNumber
+ "', owner='" + ownerName
+ "', balance=" + balance + "}";
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount("ACC-001", "Sara Ali", 500.0);
System.out.println(account);
account.deposit(200.0); // رصيد: 700.0
account.withdraw(150.0); // رصيد: 550.0
System.out.println("Final balance: " + account.getBalance());
// هذا سيُطلق IllegalStateException
try {
account.withdraw(10000.0);
} catch (IllegalStateException e) {
System.out.println("Caught: " + e.getMessage());
}
}
}
ما الذي بنيته ولماذا يهمّ
يُظهر كلاس BankAccount كل مفهوم من هذه الوحدة يعمل معًا:
- الكلاس والكائن —
BankAccount هو المخطّط؛ account هو النسخة (الكائن).
- الحقول والحالة —
balance يمثّل الحالة المتطوّرة للحساب.
- البنّاء — يضمن حالة بداية صحيحة مع تعيينات
this.
- التغليف — حقول
private محمية خلف دوال عامة.
- التحقّق — كل دالة مُعدِّلة تتحقّق من مدخلاتها قبل التصرّف.
- الوصول للقراءة فقط — getters بدون setters تفرض قاعدة أن العمليات وحدها تُغيّر الرصيد.
- toString — إخراج واضح للتنقيح والتسجيل.
هذا النمط — حالة خاصة، وبنّاء يتحقّق، ودوال عامة مبنية على السلوك، وgetters للقراءة فقط — هو أساس كود Java الاحترافي. تهانيّ على إتمام وحدة أساسيات البرمجة الكائنية!