Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

نظام بنكي

جزء من قسم البرمجة كائنية التوجه في رحلة GO على Coddy. الدرس 106 من 107.

challenge icon

التحدي

سهل

لنَبْنِ نظامًا مصرفيًا يدير الحسابات والمعاملات والعمليات المالية! ستنشئ نظامًا متينًا تكون فيه سلامة البيانات أمرًا بالغ الأهمية: يجب ألا تتغير أرصدة الحسابات إلا من خلال أساليب تم التحقق منها، كما يجب التعامل بسلاسة مع الأخطاء مثل السحب على المكشوف أو المبالغ غير الصالحة.

ستنظم شيفرتك عبر خمسة ملفات:

  • account.go: عرّف واجهة Transactable مع الأساليب Deposit(amount float64) error وWithdraw(amount float64) error. ثم أنشئ نوعين من الحسابات يطبّقان هذه الواجهة:

    حساب CheckingAccount مع الحقول غير المصدَّرة id وholder وbalance. تسمح حسابات CheckingAccount بعمليات سحب تجعل الرصيد يساوي الصفر تمامًا، ولكن ليس أقل من الصفر.

    حساب SavingsAccount مع الحقول غير المصدَّرة نفسها، بالإضافة إلى الحقل minBalance. يجب أن تحافظ حسابات SavingsAccount على رصيد أدنى قدره 100.0؛ ويجب أن تفشل عمليات السحب التي من شأنها خفض الرصيد إلى أقل من هذا الحد.

    يحتاج كلا نوعي الحسابات إلى بُناة (NewCheckingAccount وNewSavingsAccount) وإلى أسلوب getter هو Balance() float64. بالنسبة إلى عمليات الإيداع، ارفض المبالغ السالبة أو الصفرية. وبالنسبة إلى عمليات السحب، ارفض المبالغ السالبة أو الصفرية والمبالغ التي تتجاوز الأموال المتاحة.

  • transaction.go: أنشئ بنية Transaction لتسجيل نشاط account. تحتوي كل معاملة على Type (string: "deposit" أو "withdrawal")، وAmount (float64)، وAccountID (string). أضف بانيًا هو NewTransaction وأسلوبًا هو String() string يعيد التنسيق: [Type] $[Amount] on [AccountID]، مع تنسيق المبلغ إلى منزلتين عشريتين.
  • bank.go: أنشئ بنية Bank تدير عدة حسابات وتسجل المعاملات. خزّن الحسابات في map حسب المعرّف واحتفظ بـ slice من المعاملات. نفّذ ما يلي:
    • NewBank() *Bank: يهيّئ مجموعات فارغة
    • AddAccount(id string, account Transactable): يسجّل حسابًا
    • Deposit(accountID string, amount float64) error: يجري إيداعًا في account ويسجّل المعاملة عند النجاح
    • Withdraw(accountID string, amount float64) error: يجري سحبًا من account ويسجّل المعاملة عند النجاح
    • Transfer(fromID, toID string, amount float64) error: ينقل الأموال بين الحسابات atomically (إذا نجح السحب وفشل الإيداع، فاعكس السحب)
    • GetBalance(accountID string) (float64, error): يعيد رصيد account

    أعد الأخطاء المناسبة للحسابات غير الموجودة.

  • errors.go: عرّف أخطاء sentinel لعملياتك المصرفية:
    • ErrAccountNotFound: عندما لا يكون معرّف account موجودًا
    • ErrInsufficientFunds: عندما يتجاوز السحب الرصيد المتاح
    • ErrInvalidAmount: عندما يكون مبلغ الإيداع/السحب صفريًا أو سالبًا
    • ErrMinBalanceRequired: عندما ينتهك سحب SavingsAccount الرصيد الأدنى
  • main.go: أنشئ Bank وعالج العمليات استنادًا إلى الإدخال.

    اقرأ عدد الحسابات المطلوب إنشاؤها. ولكل حساب، اقرأ نوع account (checking أو savings)، وID، واسم holder، وinitial balance. أنشئ نوع الحساب المناسب وأضِفه إلى Bank.

    ثم اقرأ عدد العمليات. تكون كل عملية واحدة مما يلي:

    • deposit [accountID] [amount]
    • withdraw [accountID] [amount]
    • transfer [fromID] [toID] [amount]
    • balance [accountID]

    لكل عملية، اطبع النتيجة: OK لعمليات الإيداع/السحب/التحويل الناجحة، أو الرصيد منسقًا إلى منزلتين عشريتين لاستعلامات الرصيد، أو Error: [message] عند الفشل باستخدام رسائل أخطاء sentinel الخاصة بك.

سيتم توفير المدخلات التالية:

  • عدد الحسابات، ثم نوع كل account ومعرّفه وholder وinitial balance الخاص به (4 أسطر لكل account)
  • عدد العمليات، ثم كل عملية (سطر واحد لكل عملية)

على سبيل المثال، عند إعطاء:

2
checking
ACC001
Alice
500.00
savings
ACC002
Bob
1000.00
6
deposit ACC001 200
balance ACC001
withdraw ACC002 950
withdraw ACC001 800
transfer ACC001 ACC002 100
balance ACC002

يجب أن يكون إخراجك:

OK
700.00
Error: minimum balance required
Error: insufficient funds
OK
1000.00

وعند إعطاء:

1
checking
C100
Charlie
100.00
4
withdraw C100 100
balance C100
deposit C100 -50
withdraw C999 50

يجب أن يكون إخراجك:

OK
0.00
Error: invalid amount
Error: account not found

لاحظ كيف تسمح واجهة Transactable لـ Bank بالعمل مع نوعي الحسابات بطريقة موحّدة، بينما يفرض كل نوع من الحسابات قواعده الخاصة. يوضّح أسلوب Transfer العمليات atomic؛ فإذا حدث خطأ في منتصف العملية، يظل النظام متسقًا!

جرّب بنفسك

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)

	// اقرأ عدد الحسابات
	scanner.Scan()
	numAccounts, _ := strconv.Atoi(scanner.Text())

	// أنشئ بنكاً جديداً
	bank := NewBank()

	// اقرأ وأنشئ الحسابات
	for i := 0; i < numAccounts; i++ {
		scanner.Scan()
		accountType := scanner.Text()
		scanner.Scan()
		id := scanner.Text()
		scanner.Scan()
		holder := scanner.Text()
		scanner.Scan()
		balance, _ := strconv.ParseFloat(scanner.Text(), 64)

		// TODO: أنشئ نوع الحساب المناسب بناءً على accountType
		// وأضفه إلى البنك
		_ = accountType
		_ = id
		_ = holder
		_ = balance
	}

	// اقرأ عدد العمليات
	scanner.Scan()
	numOps, _ := strconv.Atoi(scanner.Text())

	// عالج كل عملية
	for i := 0; i < numOps; i++ {
		scanner.Scan()
		parts := strings.Fields(scanner.Text())
		operation := parts[0]

		// TODO: تعامل مع كل نوع عملية:
		// - "deposit": إيداع في الحساب، اطبع "OK" أو خطأ
		// - "withdraw": سحب من الحساب، اطبع "OK" أو خطأ
		// - "transfer": تحويل بين الحسابات، اطبع "OK" أو خطأ
		// - "balance": احصل على الرصيد واطبعه منسقاً إلى منزلتين عشريتين
		_ = operation
	}
}

جميع دروس البرمجة كائنية التوجه

16التحديات النهائية

منصة تعليم إلكترونينظام بنكينظام شخصيات الألعاب

تدرّب بنفسك: مترجم Go عبر الإنترنت