Go Programming

كسر حاجز أمان النوع: تنفيذ نمط المستودع العام في Go لتجريد قاعدة البيانات

لطالما فخر مطورو Go بالبساطة والشفافية في الكود. ومع ذلك، مع تعقيد التطبيقات، يؤدي الكود المكرر للعمليات على قاعدة البيانات إلى ازدحام هياكل الشفرة البرمجية. قبل الإصدار 1.18 من Go، كان تنفيذ طبقة مستودع عام يتطلب إما حيل واجهات معقدة أو نسخ ولصق المنطق عبر كيانات مختلفة. مع إدخال الأنواع العامة (Generics)، تطورت Go لتصبح لغة قادرة على دعم أنماط التصميم القوية دون التضحية بفلسفتها الأساسية المتمثلة في البساطة.

في هذا المنشور، سنستكشف كيفية تنفيذ نمط المستودع العام في Go الحديثة. سنقوم بإنشاء تجريد قابل لإعادة الاستخدام يسمح لك بإجراء عمليات CRUD الشائعة (إنشاء، قراءة، تحديث، حذف) على أي كيان، مما يقلل بشكل كبير من الكود المكرر مع الحفاظ على أمان النوع.

لماذا استخدام مستودع عام؟

يعمل نمط المستودع (Repository) كوسيط بين طبقة النطاق (Domain) وطبقة تعيين البيانات. هدفه الأساسي هو فصل منطق الأعمال عن منطق الوصول إلى البيانات. في تطبيق Go النموذجي، قد تجد نفسك تكتب هياكل استعلام متطابقة تقريباً لنماذج User و Post و Comment.

بدون الأنواع العامة، غالباً ما يتضمن تحقيق هذا القابلية لإعادة الاستخدام تمرير واجهات أو استخدام الانعكاس (Reflection)، وكلاهما قد يؤدي إلى أخطاء وقت التشغيل أو عبء أداء. تسمح لنا الأنواع العامة بتعريف واجهة أو هيكل واحد يعمل مع أي نوع T، مما يضمن أن طبقة قاعدة البيانات تعرف بالضبط الحقول الموجودة في وقت الترجمة (Compile time).

تعريف الواجهة العامة

لنبدأ بتعريف واجهة عامة تحدد العمليات الأساسية. ستكون هذه الواجهة معلمة بنوع T. لهذا المثال، سنفترض أننا نستخدم ORM افتراضي أو محرك SQL، لكن النمط يبقى متسقاً بغض النظر عن مكتبة البيانات الأساسية.

package repository

import (
	"context"
	"errors"
)

// Entity represents a generic database entity with an ID field.
// In a real application, you might use a more complex interface or require
// specific tags for ORM mapping.
type Entity interface {
	GetID() any
}

// ErrNotFound is returned when an entity is not found.
var ErrNotFound = errors.New("entity not found")

// GenericRepository defines standard CRUD operations for any entity type T.
type GenericRepository[T Entity] interface {
	Create(ctx context.Context, entity T) error
	GetByID(ctx context.Context, id any) (T, error)
	Update(ctx context.Context, entity T) error
	Delete(ctx context.Context, id any) error
	List(ctx context.Context) ([]T, error)
}

لاحظ أن T يجب أن يفي بالقيود المفروضة على Entity. يضمن هذا أن أي نوع يتم تمريره إلى مستودعنا يحتوي على طريقة GetID()، وهو أمر حاسم لعمليات التحديث والحذف.

تنفيذ المستودع الملموس

الآن، دعنا ننفذ نوعاً ملموساً يفي بهذه الواجهة. بينما لن نكتب ORM كامل هنا، يمكننا عرض هيكل التنفيذ. سنستخدم خريطة (map) كمخزن بيانات وهمي لإبقاء المثال مركزاً على النمط بدلاً من اتصال قاعدة البيانات.

package repository

import (
	"context"
	"fmt"
	"sync"
)

// MockRepository is a generic implementation for demonstration purposes.
type MockRepository[T Entity] struct {
	mu    sync.Mutex
	Store map[any]T
}

// NewMockRepository creates a new instance of the mock repository.
func NewMockRepository[T Entity]() *MockRepository[T] {
	return &MockRepository[T]{
		Store: make(map[any]T),
	}
}

// Create adds a new entity to the store.
func (r *MockRepository[T]) Create(ctx context.Context, entity T) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	
	id := entity.GetID()
	if _, exists := r.Store[id]; exists {
		return fmt.Errorf("entity with ID %v already exists", id)
	}
	r.Store[id] = entity
	return nil
}

// GetByID retrieves an entity by its ID.
func (r *MockRepository[T]) GetByID(ctx context.Context, id any) (T, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	
	entity, exists := r.Store[id]
	if !exists {
		var zero T
		return zero, ErrNotFound
	}
	return entity, nil
}

// List returns all entities in the store.
func (r *MockRepository[T]) List(ctx context.Context) ([]T, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	
	entities := make([]T, 0, len(r.Store))
	for _, v := range r.Store {
		entities = append(entities, v)
	}
	return entities, nil
}

من خلال استخدام MockRepository[T Entity]، نضمن أن المترجم يفرض أن الأنواع التي تطبق Entity فقط يمكن استخدامها مع هذا المستودع. إذا حاولت إنشاء مستودع لهيكل يفتقر إلى طريقة GetID()، فإن الكود سيفشل في الترجمة.

مثال على الاستخدام العملي

لنرَ كيفية استخدام هذا النمط في طبقة الخدمة. عرف هيكل مستخدم بسيط:

type User struct {
	ID   int64
	Name string
}

func (u User) GetID() any {
	return u.ID
}

يمكنك الآن إنشاء مستودع مخصص للمستخدمين:

userRepo := repository.NewMockRepository[User]()
err := userRepo.Create(context.Background(), User{ID: 1, Name: "Alice"})
if err != nil {
    log.Fatal(err)
}

user, err := userRepo.GetByID(context.Background(), 1)
if err == repository.ErrNotFound {
    log.Println("User not found")
} else if err != nil {
    log.Fatal(err)
}
fmt.Printf("Retrieved user: %+v\n", user)

الخاتمة

يوفر تنفيذ أنماط المستودع العام في Go طريقة قوية لتجريد منطق قاعدة البيانات مع الاستفادة من أمان النوع الذي يحبه مطورو Go. إنه يلغي الكود المكرر، ويجعل الاختبار أسهل من خلال السماح بتنفيذات وهمية بسيطة، ويوفر عقداً واضحاً للوصول إلى البيانات.

بينما يستخدم هذا المثال مخزناً وهمياً، تنطبق المبادئ نفسها عند التكامل مع مكتبات مثل GORM أو sqlx أو ent. من خلال اعتماد الأنواع العامة، يمكنك كتابة تطبيقات Go أكثر تعبيراً وقابلية للصيانة وقابلة للتوسع.

Share: