ممارسة: نظام إدارة الطلاب
ممارسة: نظام إدارة الطلاب
تخيل أنك معلم فصل وعندك دفتر ملاحظات: كل صفحة تُسجّل اسم الطالب ورقمه ودرجات كل مادة. عندما ينتقل طالب جديد، تقلب لصفحة فارغة لتسجيله؛ بعد الامتحانات، تجد الصفحة المقابلة لتسجيل الدرجات؛ في نهاية الفصل، تطبع كشوف الدرجات مرتبة حسب المجموع. العملية كلها هي تنفيذ عمليات "CRUD".
اليوم ننقل ذلك الدفتر إلى الكمبيوتر — نبني نظام إدارة طلاب من سطر الأوامر في Go من الصفر، نربط كل ما تعلمناه عن التراكيب والأساليب والواجهات ومعالجة الأخطاء.
متطلبات المشروع
| الميزة | الوصف |
|---|---|
| إضافة طالب | إدخال الاسم ورقم الطالب لإنشاء سجل جديد |
| إضافة درجة مادة | تسجيل درجة لمادة معينة لطالب |
| استعلام طالب | البحث برقم الطالب، عرض معلومات الطالب وجميع الدرجات |
| قائمة جميع الطلاب | عرض جميع الطلاب المسجلين بمتوسط درجاتهم |
| حذف طالب | إزالة طالب برقم الطالب |
| خروج البرنامج | حفظ البيانات والخروج |
تصميم النظام
+---------------------------------------------+
| قائمة سطر الأوامر |
+---------------------------------------------+
| StudentManager |
| (يحوي واجهة Storer، ينفذ المنطق) |
+---------------------------------------------+
| واجهة Storer |
| Save / Load -- خلفية تخزين قابلة للتبديل |
+-------------------+-------------------------+
| MemoryStore | FileStore (اختياري) |
| (خريطة ذاكرة) | (ملف JSON) |
+-------------------+-------------------------+
الأنواع الأساسية:
- Student — معلومات الطالب + درجات المواد
- Course — درجة مادة واحدة
- Storer — واجهة تجريد التخزين
- StudentManager — طبقة منطق الأعمال
الكود الكامل
أنشئ ملف main.go وانسخ الكود التالي بالكامل:
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"sort"
"strconv"
"strings"
)
// ============================================================
// تعريفات هياكل البيانات
// ============================================================
// Course يمثل درجة مادة
type Course struct {
Name string `json:"name"`
Score float64 `json:"score"`
}
// Student يمثل طالبًا
type Student struct {
ID string `json:"id"`
Name string `json:"name"`
Courses []Course `json:"courses"`
}
// Average يحسب متوسط درجات الطالب
func (s Student) Average() float64 {
if len(s.Courses) == 0 {
return 0
}
total := 0.0
for _, c := range s.Courses {
total += c.Score
}
return total / float64(len(s.Courses))
}
// AddCourse يُضيف درجة مادة للطالب
func (s *Student) AddCourse(name string, score float64) {
// إذا كانت المادة موجودة، حدّث الدرجة
for i, c := range s.Courses {
if c.Name == name {
s.Courses[i].Score = score
return
}
}
s.Courses = append(s.Courses, Course{Name: name, Score: score})
}
// String يُنفذ واجهة fmt.Stringer لسهولة الطباعة
func (s Student) String() string {
return fmt.Sprintf("[%s] %s (Average: %.1f)", s.ID, s.Name, s.Average())
}
// ============================================================
// واجهة التخزين
// ============================================================
// Storer يُعرّف واجهة التخزين التجريدية
type Storer interface {
Save(students map[string]*Student) error
Load() (map[string]*Student, error)
}
// ------------------------------------------------------------
// تنفيذ التخزين في الذاكرة
// ------------------------------------------------------------
// MemoryStore يخزن البيانات في الذاكرة (تُفقد عند خروج البرنامج)
type MemoryStore struct{}
func NewMemoryStore() *MemoryStore {
return &MemoryStore{}
}
func (m *MemoryStore) Save(students map[string]*Student) error {
// التخزين في الذاكرة لا يحتاج استمرارية، أرجع nil فقط
return nil
}
func (m *MemoryStore) Load() (map[string]*Student, error) {
return make(map[string]*Student), nil
}
// ------------------------------------------------------------
// تنفيذ التخزين في ملف
// ------------------------------------------------------------
// FileStore يحفظ البيانات في ملف بصيغة JSON
type FileStore struct {
Path string
}
func NewFileStore(path string) *FileStore {
return &FileStore{Path: path}
}
func (f *FileStore) Save(students map[string]*Student) error {
data, err := json.MarshalIndent(students, "", " ")
if err != nil {
return fmt.Errorf("serialization failed: %w", err)
}
err = os.WriteFile(f.Path, data, 0644)
if err != nil {
return fmt.Errorf("writing file failed: %w", err)
}
return nil
}
func (f *FileStore) Load() (map[string]*Student, error) {
students := make(map[string]*Student)
data, err := os.ReadFile(f.Path)
if err != nil {
if os.IsNotExist(err) {
// الملف غير موجود، أرجع خريطة فارغة
return students, nil
}
return nil, fmt.Errorf("reading file failed: %w", err)
}
err = json.Unmarshal(data, &students)
if err != nil {
return nil, fmt.Errorf("parsing data failed: %w", err)
}
return students, nil
}
// ============================================================
// طبقة منطق الأعمال
// ============================================================
// StudentManager يُدير جميع بيانات الطلاب
type StudentManager struct {
students map[string]*Student
store Storer
reader *bufio.Reader
}
// NewStudentManager يُنشئ مديرًا جديدًا
func NewStudentManager(store Storer) (*StudentManager, error) {
students, err := store.Load()
if err != nil {
return nil, fmt.Errorf("loading data failed: %w", err)
}
return &StudentManager{
students: students,
store: store,
reader: bufio.NewReader(os.Stdin),
}, nil
}
// AddStudent يُضيف طالبًا جديدًا
func (m *StudentManager) AddStudent(id, name string) error {
if id == "" || name == "" {
return fmt.Errorf("student ID and name cannot be empty")
}
if _, exists := m.students[id]; exists {
return fmt.Errorf("student ID %s already exists", id)
}
m.students[id] = &Student{ID: id, Name: name}
return m.store.Save(m.students)
}
// AddCourse يُضيف درجة مادة لطالب
func (m *StudentManager) AddCourse(id, courseName string, score float64) error {
student, err := m.findStudent(id)
if err != nil {
return err
}
if score < 0 || score > 100 {
return fmt.Errorf("grade must be between 0 and 100")
}
student.AddCourse(courseName, score)
return m.store.Save(m.students)
}
// GetStudent يُستعلم عن معلومات الطالب
func (m *StudentManager) GetStudent(id string) (*Student, error) {
return m.findStudent(id)
}
// DeleteStudent يحذف طالبًا
func (m *StudentManager) DeleteStudent(id string) error {
if _, exists := m.students[id]; !exists {
return fmt.Errorf("student ID %s does not exist", id)
}
delete(m.students, id)
return m.store.Save(m.students)
}
// ListStudents يُرجع جميع الطلاب مرتبين برقم الطالب
func (m *StudentManager) ListStudents() []*Student {
list := make([]*Student, 0, len(m.students))
for _, s := range m.students {
list = append(list, s)
}
sort.Slice(list, func(i, j int) bool {
return list[i].ID < list[j].ID
})
return list
}
// findStudent يبحث عن طالب بالمعرّف (مساعد داخلي)
func (m *StudentManager) findStudent(id string) (*Student, error) {
s, exists := m.students[id]
if !exists {
return nil, fmt.Errorf("student ID %s does not exist", id)
}
return s, nil
}
// ============================================================
// طبقة التفاعل مع سطر الأوامر
// ============================================================
// Run يبدأ حلقة التفاعل
func (m *StudentManager) Run() {
for {
m.printMenu()
choice := m.readLine("Enter option: ")
switch choice {
case "1":
m.handleAddStudent()
case "2":
m.handleAddCourse()
case "3":
m.handleGetStudent()
case "4":
m.handleListStudents()
case "5":
m.handleDeleteStudent()
case "6":
fmt.Println("\nData saved. Goodbye!")
return
default:
fmt.Println("\nInvalid option, please try again")
}
fmt.Println()
}
}
func (m *StudentManager) printMenu() {
fmt.Println("+------------------------------+")
fmt.Println("| Student Manager v1.0 |")
fmt.Println("+------------------------------+")
fmt.Println("| 1. Add Student |")
fmt.Println("| 2. Add Course Grade |")
fmt.Println("| 3. Query Student |")
fmt.Println("| 4. List All Students |")
fmt.Println("| 5. Delete Student |")
fmt.Println("| 6. Exit |")
fmt.Println("+------------------------------+")
}
func (m *StudentManager) handleAddStudent() {
id := m.readLine("Enter student ID: ")
name := m.readLine("Enter student name: ")
err := m.AddStudent(id, name)
if err != nil {
fmt.Printf("\nAdd failed: %v\n", err)
return
}
fmt.Printf("\nStudent %s added successfully!\n", name)
}
func (m *StudentManager) handleAddCourse() {
id := m.readLine("Enter student ID: ")
courseName := m.readLine("Enter course name: ")
scoreStr := m.readLine("Enter grade (0-100): ")
score, err := strconv.ParseFloat(scoreStr, 64)
if err != nil {
fmt.Printf("\nInvalid grade format: %v\n", err)
return
}
err = m.AddCourse(id, courseName, score)
if err != nil {
fmt.Printf("\nAdd failed: %v\n", err)
return
}
fmt.Printf("\nGrade added successfully!\n")
}
func (m *StudentManager) handleGetStudent() {
id := m.readLine("Enter student ID: ")
s, err := m.GetStudent(id)
if err != nil {
fmt.Printf("\nQuery failed: %v\n", err)
return
}
fmt.Println("\n--- Student Info ---")
fmt.Printf("ID: %s\n", s.ID)
fmt.Printf("Name: %s\n", s.Name)
if len(s.Courses) == 0 {
fmt.Println("No course grades yet")
} else {
fmt.Println("Course grades:")
for _, c := range s.Courses {
fmt.Printf(" %s: %.1f\n", c.Name, c.Score)
}
fmt.Printf("Average: %.1f\n", s.Average())
}
}
func (m *StudentManager) handleListStudents() {
list := m.ListStudents()
if len(list) == 0 {
fmt.Println("\nNo student records")
return
}
fmt.Println("\n--- All Students ---")
for _, s := range list {
courseCount := len(s.Courses)
fmt.Printf(" %s Courses: %d Average: %.1f\n", s, courseCount, s.Average())
}
fmt.Printf("\nTotal: %d students\n", len(list))
}
func (m *StudentManager) handleDeleteStudent() {
id := m.readLine("Enter student ID to delete: ")
err := m.DeleteStudent(id)
if err != nil {
fmt.Printf("\nDelete failed: %v\n", err)
return
}
fmt.Printf("\nStudent ID %s deleted\n", id)
}
// readLine يقرأ سطرًا من مدخل المستخدم ويُزيل المسافات
func (m *StudentManager) readLine(prompt string) string {
fmt.Print(prompt)
line, _ := m.reader.ReadString('\n')
return strings.TrimSpace(line)
}
// ============================================================
// نقطة دخول البرنامج
// ============================================================
func main() {
// استخدام تخزين الملف (البيانات تُحفظ في students.json)
store := NewFileStore("students.json")
// لاستخدام التخزين في الذاكرة (البيانات تُفقد عند الخروج)، غيّر إلى:
// store := NewMemoryStore()
mgr, err := NewStudentManager(store)
if err != nil {
fmt.Fprintf(os.Stderr, "Initialization failed: %v\n", err)
os.Exit(1)
}
fmt.Println("Welcome to the Student Management System!")
mgr.Run()
}
شرح الكود
1. تصميم هياكل البيانات
type Student struct {
ID string `json:"id"`
Name string `json:"name"`
Courses []Course `json:"courses"`
}
Studentيحوي شريحة منCourses، كل منها يحتوي اسم مادة ودرجة.- علامات
jsonتتيح سلسلة التراكيب مباشرة إلى JSON للتخزين في ملف.
2. الأساليب ومستلمات المؤشر
func (s *Student) AddCourse(name string, score float64) { ... }
func (s Student) Average() float64 { ... }
AddCourseيُعدّل الشريحة، لذا يستخدم مستلم المؤشر*Student.Averageيقرأ البيانات فقط، لذا المستلم القيميStudentكافٍ.AddCourseيُنفذ منطق "تحديث إذا كان اسم المادة موجودًا" لتجنب التكرار.
3. تجريد الواجهة — Storer
type Storer interface {
Save(students map[string]*Student) error
Load() (map[string]*Student, error)
}
StudentManagerيعتمد فقط على واجهةStorer، وليس طريقة التخزين المحددة.- يُقدّم تنفيذان:
MemoryStore(في الذاكرة) وFileStore(ملف JSON). - تريد التبديل إلى قاعدة بيانات؟ أضف نوعًا جديدًا يُنفذ
Storer— تغيير صفر في كود الأعمال.
4. استراتيجية معالجة الأخطاء
هذا المشروع يُعالج الأخطاء في كل طبقة:
| الطبقة | الاستراتيجية |
|---|---|
| التحقق من البيانات | فحص رقم الطالب/الاسم غير فارغ، نطاق الدرجة 0-100 |
| منطق الأعمال | فحص وجود رقم الطالب، خطأ عند التكرار |
| طبقة التخزين | تغليف الأخطاء الأساسية بـ fmt.Errorf("...: %w", err) |
| طبقة سطر الأوامر | الالتقاط الأخطاء وعرض رسائل ودية للمستخدم |
return fmt.Errorf("serialization failed: %w", err)
تغليف %w يُبقي معلومات الخطأ الأصلية، مما يتيح للمُستدعين استخدام errors.Is أو errors.As للتحقق من أنواع الأخطاء.
5. حلقة قائمة سطر الأوامر
for {
m.printMenu()
choice := m.readLine("Enter option: ")
switch choice { ... }
}
الحلقة الرئيسية تطبع القائمة باستمرار، تقرأ المدخلات، وتُوزعها على المعالج المقابل. هذا النمط "قراءة-توزيع" هو البنية الكلاسيكية لبرامج سطر الأوامر.
6. طبقة تخزين قابلة للتوسيع
التبديل بين طرق التخزين يتطلب تغيير سطر واحد في main():
// وضع الذاكرة
store := NewMemoryStore()
// وضع الملف
store := NewFileStore("students.json")
StudentManager لا يعلم تمامًا بالتغيير الأساسي — هذه قيمة الواجهات.
التشغيل والاختبار
# تجميع وتشغيل
go run main.go
Welcome to the Student Management System!
+------------------------------+
| Student Manager v1.0 |
+------------------------------+
| 1. Add Student |
| 2. Add Course Grade |
| 3. Query Student |
| 4. List All Students |
| 5. Delete Student |
| 6. Exit |
+------------------------------+
Enter option: 1
Enter student ID: 1001
Enter student name: Alice
Student Alice added successfully!
عند الانتهاء، يُنشئ البرنامج ملف students.json في الدليل الحالي، الذي يُحمّل تلقائيًا عند التشغيل التالي.
❓ أسئلة شائعة
س1: لماذا Average() تستخدم مستلم القيمة بينما AddCourse() تستخدم مستلم المؤشر؟
Average() تقرأ البيانات فقط لحساب المجموع، دون تعديل Student، لذا المستلم القيمة أكثر أمانًا. AddCourse() تحتاج لإضافة عناصر إلى شريحة Courses، مما يتطلب مستلم المؤشر لتعديل بيانات المُستدعي الأصلية. مبدأ بسيط: استخدم المؤشر للتعديل، استخدم القيمة للقراءة.
س2: ماذا يعني * في map[string]*Student؟
هذه خريطة بقيم مؤشرات. m.students[id] يُرجع *Student (مؤشر إلى Student)، وليس نسخة من Student. بهذه الطريقة، عندما نُعدّل بيانات الطالب عبر AddCourse، تُنعكس التغييرات مباشرة في الخريطة دون معالجة إضافية.
س3: لماذا Load() تُعالج حالة عدم وجود الملف بشكل منفصل؟
في التشغيل الأول، students.json غير موجود بعد، و os.ReadFile سُيرجع خطأ. إذا أبلغنا عن الخطأ وخرجنا، لن يتمكن المستخدم من بدء البرنامج أبدًا. لذا نتحقق بـ os.IsNotExist(err) — ملف مفقود هو أمر عادي، ونُرجع خريطة فارغة.
س4: كيف أوسّع إلى تخزين قاعدة البيانات؟
نفذ واجهة Storer فقط:
type MySQLStore struct {
db *sql.DB
}
func (m *MySQLStore) Save(students map[string]*Student) error {
// INSERT/UPDATE إلى قاعدة البيانات
}
func (m *MySQLStore) Load() (map[string]*Student, error) {
// SELECT من قاعدة البيانات
}
ثم مرر &MySQLStore{db: db} في main()، وكود الأعمال لا يحتاج تغييرات.
📖 ملخص
هذا الدرس يربط المعرفة الأساسية للمرحلة الثانية عبر مشروع كامل:
- التراكيب —
StudentوCourseيُعرّفان نموذج البيانات؛ علاماتjsonتدعم السلسلة. - الأساليب — مستلمات اليمية للعمليات للقراءة فقط؛ مستلمات المؤشر لتعديل البيانات.
- الواجهات — واجهة
Storerتفصل منطق الأعمال عن تنفيذ التخزين، مُبرمجة Go الموجهة للواجهات. - معالجة الأخطاء — التحقق وتغليف الأخطاء في كل طبقة؛ طبقة سطر الأوامر تعرض رسائل ودية للمستخدم.
- تنظيم الحزم — جميع الكود في حزمة واحدة، لكن بفصل واضح للمسؤوليات عبر الأنواع والأساليب.
هذا هو النمط الأساسي لهندسة Go: نمذجة بالتراكيب، تجسيد السلوك بالأساليب، فصل الارتباط بالواجهات، وانتشار الاستثناءات بالأخطاء.
📝 تمارين
تمرين 1: إضافة ترتيب حسب متوسط الدرجات
أضف خيار قائمة جديد "الترتيب حسب متوسط الدرجات" يُرتب جميع الطلاب حسب متوسط الدرجات من الأعلى إلى الأقل ويُخرج النتيجة. تلميح: استخدم sort.Slice.
تمرين 2: إضافة ميزات استيراد/تصدير
نفّذ أسلوبين جديدين:
ExportCSV(filename string) error— تصدير جميع الطلاب إلى ملف CSV (رقم الطالب، الاسم، المادة، الدرجة).ImportCSV(filename string) error— استيراد مجمع للطلاب والدرجات من ملف CSV.
تمرين 3: إضافة وسطية التحقق من البيانات
أنشئ تراكيب ValidatingStore يُغلف أي تنفيذ Storer، يُحقّق من جميع بيانات الطالب قبل Save (رقم الطالب غير فارغ، الاسم غير فارغ، الدرجة بين 0-100). هذا نمط المزخرف في الممارسة.



