Skip to main content

Command Palette

Search for a command to run...

Go-তে Interface কীভাবে Code Decouple করে?

Updated
20 min read
Go-তে Interface কীভাবে Code Decouple করে?
I

A highly motivated and experienced full-stack developer with a proven track record of developing and deploying web applications. Skilled in a range of programming languages and frameworks, as well as database technologies. Comfortable working in a fast-paced environment and able to adapt to new technologies quickly. A team player who is also able to work independently when required.

একটা HTTP Server দিয়ে পুরো ব্যাপারটা বুঝে নেওয়া যাক


আমরা সবাই জানি Go একটা সিম্পল ল্যাঙ্গুয়েজ, কিন্তু interface নিয়ে অনেকেরই confusion থাকে। আজকে আমরা দেখব কীভাবে interface আসলে তোমার code-কে flexible এবং maintainable বানায়। একটা real-world HTTP server বানাতে বানাতে step by step বুঝব কেন interface ছাড়া code জট পাকিয়ে যায় আর interface দিয়ে কীভাবে সব ঝামেলা সলভ হয়ে যায়।

সমস্যা #১: Database-এর সাথে Tight Coupling

ধরো তুমি একটা user management API বানাচ্ছ। শুরুতে তুমি ভাবলে MongoDB ইউজ করবে। তাই সব জায়গায় MongoDB-এর code লিখে ফেললে।

প্রথম Version - সব জায়গায় MongoDB

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

// MongoDB struct - ধরো এটা আসল MongoDB connection
type MongoDB struct {
    ConnectionString string
}

// MongoDB-তে user save করার method
func (db *MongoDB) SaveUser(username, email string) error {
    fmt.Printf("MongoDB-তে save হচ্ছে: %s (%s)\n", username, email)
    // এখানে actual MongoDB save logic থাকবে
    return nil
}

// MongoDB থেকে user fetch করার method
func (db *MongoDB) GetUser(username string) (string, error) {
    fmt.Printf("MongoDB থেকে fetch হচ্ছে: %s\n", username)
    // এখানে actual MongoDB query logic থাকবে
    return fmt.Sprintf("%s@example.com", username), nil
}

// HTTP handler যেটা user create করে
func CreateUserHandler(db *MongoDB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        username := r.URL.Query().Get("username")
        email := r.URL.Query().Get("email")

        // সরাসরি MongoDB-এর method call করছি
        err := db.SaveUser(username, email)
        if err != nil {
            http.Error(w, "Error saving user", http.StatusInternalServerError)
            return
        }

        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(map[string]string{
            "message": "User created successfully",
        })
    }
}

// HTTP handler যেটা user fetch করে
func GetUserHandler(db *MongoDB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        username := r.URL.Query().Get("username")

        // আবারও সরাসরি MongoDB-এর method call
        email, err := db.GetUser(username)
        if err != nil {
            http.Error(w, "Error fetching user", http.StatusInternalServerError)
            return
        }

        json.NewEncoder(w).Encode(map[string]string{
            "username": username,
            "email":    email,
        })
    }
}

func main() {
    // MongoDB connection setup
    db := &MongoDB{
        ConnectionString: "mongodb://localhost:27017",
    }

    // Routes setup
    http.HandleFunc("/user/create", CreateUserHandler(db))
    http.HandleFunc("/user/get", GetUserHandler(db))

    fmt.Println("Server চালু হয়েছে port 8080-তে")
    http.ListenAndServe(":8080", nil)
}

এই Code-এর সমস্যাগুলো কী?

এবার চিন্তা করো তোমার boss বললো, "আমরা PostgreSQL-এ switch করব কারণ MongoDB expensive হয়ে যাচ্ছে।" তখন তোমাকে কী করতে হবে?

প্রথমত, তোমাকে পুরো codebase-এ গিয়ে প্রতিটা জায়গায় *MongoDB খুঁজে বের করে সেটা *PostgreSQL বানাতে হবে। দেখো কত জায়গায় problem হবে:

সমস্যা ১.১: Handler Functions পুরোপুরি MongoDB-এর উপর নির্ভরশীল

CreateUserHandler আর GetUserHandler দুটোই *MongoDB parameter হিসেবে নিচ্ছে। এর মানে হলো এই handlers শুধুমাত্র MongoDB-এর সাথেই কাজ করতে পারে। তুমি যদি PostgreSQL বা কোনো in-memory database ইউজ করতে চাও, তাহলে এই পুরো handler functions আবার নতুন করে লিখতে হবে।

সমস্যা ১.২: Testing করা প্রায় impossible

ধরো তুমি এই handlers-এর unit test লিখতে চাচ্ছ। কিন্তু test লেখার জন্য তোমাকে একটা real MongoDB instance run করতে হবে। এটা অনেক slow এবং test setup খুবই complicated হয়ে যায়। তুমি চাইলে fake বা mock database দিয়ে test করতে পারবে না, কারণ handler শুধুমাত্র *MongoDB type-ই accept করে।

সমস্যা ১.৩: Code reuse করা যায় না

মনে করো তোমার আরেকটা microservice আছে যেটা MySQL ইউজ করে। তুমি চাইলেও এই handlers সেখানে ইউজ করতে পারবে না। কারণ? এগুলো hardcoded MongoDB-এর জন্য বানানো।

সমস্যা ১.৪: পুরো codebase change করতে হয়

Database switch করতে গেলে শুধু database layer-এই change করলেই হয় না। তোমাকে সব handler, সব function যেখানে *MongoDB আছে সব জায়গায় গিয়ে change করতে হবে। এটা error-prone এবং সময়সাপেক্ষ।

এই সমস্যাটাকে বলা হয় tight coupling। মানে তোমার business logic (HTTP handlers) আর তোমার infrastructure (database) একসাথে জড়িয়ে আছে। এদের আলাদা করা যাচ্ছে না।


সমাধান #১: Interface দিয়ে Abstraction তৈরি করা

এবার দেখো কীভাবে interface দিয়ে এই problem solve করা যায়। মূল idea হলো, "আমার handler-এর জানা দরকার না database কোনটা। শুধু জানা দরকার সে কী কী operation করতে পারে।"

Interface দিয়ে Decoupled Version

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

// ১. প্রথমে একটা interface define করি যেটা বলে দেয় 
//    আমাদের কী কী capability দরকার
type UserRepository interface {
    SaveUser(username, email string) error
    GetUser(username string) (string, error)
}

// ২. MongoDB implementation - interface satisfy করছে
type MongoDB struct {
    ConnectionString string
}

func (db *MongoDB) SaveUser(username, email string) error {
    fmt.Printf("MongoDB-তে save হচ্ছে: %s (%s)\n", username, email)
    return nil
}

func (db *MongoDB) GetUser(username string) (string, error) {
    fmt.Printf("MongoDB থেকে fetch হচ্ছে: %s\n", username)
    return fmt.Sprintf("%s@example.com", username), nil
}

// ৩. PostgreSQL implementation - একই interface satisfy করছে
type PostgreSQL struct {
    ConnectionString string
}

func (db *PostgreSQL) SaveUser(username, email string) error {
    fmt.Printf("PostgreSQL-এ save হচ্ছে: %s (%s)\n", username, email)
    return nil
}

func (db *PostgreSQL) GetUser(username string) (string, error) {
    fmt.Printf("PostgreSQL থেকে fetch হচ্ছে: %s\n", username)
    return fmt.Sprintf("%s@example.com", username), nil
}

// ৪. এবার handler concrete type-এর বদলে interface নিচ্ছে
func CreateUserHandler(repo UserRepository) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        username := r.URL.Query().Get("username")
        email := r.URL.Query().Get("email")

        // এখন repo MongoDB-ও হতে পারে, PostgreSQL-ও হতে পারে
        // handler-এর কিছু যায় আসে না
        err := repo.SaveUser(username, email)
        if err != nil {
            http.Error(w, "Error saving user", http.StatusInternalServerError)
            return
        }

        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(map[string]string{
            "message": "User created successfully",
        })
    }
}

func GetUserHandler(repo UserRepository) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        username := r.URL.Query().Get("username")

        email, err := repo.GetUser(username)
        if err != nil {
            http.Error(w, "Error fetching user", http.StatusInternalServerError)
            return
        }

        json.NewEncoder(w).Encode(map[string]string{
            "username": username,
            "email":    email,
        })
    }
}

func main() {
    // এখন তুমি যেকোনো implementation ইউজ করতে পারো

    // MongoDB ইউজ করতে চাইলে:
    // var repo UserRepository = &MongoDB{ConnectionString: "mongodb://localhost:27017"}

    // PostgreSQL ইউজ করতে চাইলে:
    var repo UserRepository = &PostgreSQL{ConnectionString: "postgres://localhost:5432"}

    // Handler setup - একই handlers কাজ করবে যেকোনো database-এর সাথে
    http.HandleFunc("/user/create", CreateUserHandler(repo))
    http.HandleFunc("/user/get", GetUserHandler(repo))

    fmt.Println("Server চালু হয়েছে port 8080-তে")
    http.ListenAndServe(":8080", nil)
}

কী পরিবর্তন হলো এবং কেন এটা Better?

চলো step by step দেখি কী কী advantage পেলাম:

সুবিধা ১.১: Handlers এখন Database-Independent

লক্ষ করো, CreateUserHandler এবং GetUserHandler এখন UserRepository interface নিচ্ছে, কোনো concrete type নয়। এর মানে হলো এই handlers যেকোনো database-এর সাথে কাজ করতে পারবে যেটা UserRepository interface implement করে। তোমাকে শুধু SaveUser আর GetUser methods implement করতে হবে, সব কাজ হয়ে যাবে।

সুবিধা ১.২: Database Switch করা এখন এক লাইনের কাজ

main() function-এ দেখো, MongoDB থেকে PostgreSQL-এ switch করতে শুধু একটা লাইন comment/uncomment করলেই হচ্ছে। পুরো application-এর আর কোনো code touch করতে হচ্ছে না। এটাই হলো decoupling-এর power।

সুবিধা ১.৩: Testing এখন অনেক সহজ

এখন তুমি সহজেই mock repository বানিয়ে test করতে পারবে, যেটা পরে আমরা দেখব। Real database ছাড়াই handlers test করা যাবে।

সুবিধা ১.৪: Code Reusability বেড়ে গেছে

এই handlers এখন যেকোনো project-এ ইউজ করা যাবে। শুধু সেই project-এর database-এর জন্য UserRepository interface implement করলেই হবে।


সমস্যা #২: Testing করা যাচ্ছে না

এবার আরেকটা বড় সমস্যা নিয়ে কথা বলি। তুমি যখন প্রথম coupled version-এ unit test লিখতে যাবে, তখন দেখবে অনেক ঝামেলা। কারণ test চালাতে হলে তোমাকে একটা real MongoDB instance run করতে হবে।

Testing-এর সমস্যাগুলো Coupled Code-এ

চলো দেখি coupled code-এ test লিখতে গেলে কী কী problem হয়:

সমস্যা ২.১: Test Setup অনেক জটিল

প্রতিবার test run করার আগে তোমাকে MongoDB start করতে হবে, database create করতে হবে, connection setup করতে হবে। এটা শুধু সময়সাপেক্ষই না, test environment setup করাও complicated।

সমস্যা ২.২: Tests অনেক Slow

Real database-এর সাথে interaction করতে হচ্ছে, তাই প্রতিটা test অনেক সময় নিচ্ছে। একটা simple handler test করতে হয়তো ১-২ সেকেন্ড লাগছে। যখন তোমার ১০০টা test হবে, তখন পুরো test suite চালাতে মিনিটের পর মিনিট লাগবে।

সমস্যা ২.৩: Test Isolation নেই

একটা test যদি database-এ data লিখে রাখে, সেটা অন্য test-কে affect করতে পারে। তোমাকে প্রতিটা test-এর আগে database clean করতে হবে, যা আরও complexity add করে।

সমস্যা ২.৪: CI/CD Pipeline-এ Problem

GitHub Actions বা GitLab CI-তে test run করতে গেলে সেখানেও MongoDB setup করতে হবে। এটা pipeline-কে slow এবং unreliable বানায়।

Coupled Code-এ Test লেখার চেষ্টা

চলো দেখি প্রথম version-এ test লিখতে গেলে কেমন দেখাতো:

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestCreateUserHandler_Coupled(t *testing.T) {
    // সমস্যা: আমাকে real MongoDB connection দিতেই হবে
    db := &MongoDB{
        ConnectionString: "mongodb://localhost:27017",
    }

    // এই test run করার আগে MongoDB চালু থাকতে হবে!
    // তা না হলে test fail করবে

    handler := CreateUserHandler(db)

    req := httptest.NewRequest("GET", "/user/create?username=test&email=test@test.com", nil)
    w := httptest.NewRecorder()

    handler(w, req)

    // Test করছি response
    if w.Code != http.StatusCreated {
        t.Errorf("Expected status 201, got %d", w.Code)
    }

    // কিন্তু এই test actually MongoDB-তে data write করে ফেলছে!
    // এটা test না, এটা integration test হয়ে গেছে
}

এই test-এর problems:

প্রথম সমস্যা: MongoDB না চললে test fail করবে। মানে তোমার development machine-এ MongoDB install না থাকলে বা service down থাকলে test চলবে না।

দ্বিতীয় সমস্যা: এটা actually database-এ write করছে। Pure unit test হওয়া উচিত ছিল, কিন্তু এটা database-এর উপর depend করছে।

তৃতীয় সমস্যা: Test slow। একটা simple handler test করতে database connection, write operation সব করতে হচ্ছে।


সমাধান #২: Mock Repository দিয়ে Fast Testing

এবার দেখো কীভাবে interface ইউজ করে testing problem solve হয়। Interface থাকার কারণে তুমি খুব সহজেই একটা fake/mock implementation বানাতে পারো যেটা কোনো real database ছাড়াই কাজ করবে।

Mock Repository Implementation

package main

import (
    "fmt"
)

// Mock implementation যেটা memory-তে data রাখে
// এটা testing-এর জন্য perfect
type MockRepository struct {
    // Memory-তে user data রাখার জন্য map
    users map[string]string

    // Test করার জন্য track করি কতবার methods call হলো
    SaveUserCalled int
    GetUserCalled  int
}

// MockRepository-র constructor
func NewMockRepository() *MockRepository {
    return &MockRepository{
        users: make(map[string]string),
    }
}

// UserRepository interface implement করছি
func (m *MockRepository) SaveUser(username, email string) error {
    m.SaveUserCalled++ // Counter বাড়াচ্ছি
    m.users[username] = email // Memory-তে save করছি
    fmt.Printf("Mock: User saved in memory - %s (%s)\n", username, email)
    return nil
}

func (m *MockRepository) GetUser(username string) (string, error) {
    m.GetUserCalled++ // Counter বাড়াচ্ছি

    email, exists := m.users[username]
    if !exists {
        return "", fmt.Errorf("user not found")
    }

    fmt.Printf("Mock: User fetched from memory - %s\n", username)
    return email, nil
}

এবার Test লিখি Mock দিয়ে

package main

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestCreateUserHandler_WithMock(t *testing.T) {
    // Real database-এর বদলে mock ইউজ করছি
    // কোনো external dependency নেই!
    mockRepo := NewMockRepository()

    // Handler create করছি mock দিয়ে
    handler := CreateUserHandler(mockRepo)

    // HTTP request simulate করছি
    req := httptest.NewRequest("GET", "/user/create?username=ahmed&email=ahmed@test.com", nil)
    w := httptest.NewRecorder()

    // Handler call করছি
    handler(w, req)

    // Response check করছি
    if w.Code != http.StatusCreated {
        t.Errorf("Expected status 201, got %d", w.Code)
    }

    // Verify করছি যে SaveUser actually call হয়েছে কিনা
    if mockRepo.SaveUserCalled != 1 {
        t.Errorf("Expected SaveUser to be called once, called %d times", mockRepo.SaveUserCalled)
    }

    // Verify করছি data সঠিকভাবে save হয়েছে কিনা
    email, err := mockRepo.GetUser("ahmed")
    if err != nil {
        t.Errorf("User should exist in mock repository")
    }
    if email != "ahmed@test.com" {
        t.Errorf("Expected email ahmed@test.com, got %s", email)
    }

    // Response body check করছি
    var response map[string]string
    json.NewDecoder(w.Body).Decode(&response)

    if response["message"] != "User created successfully" {
        t.Errorf("Unexpected response message: %s", response["message"])
    }
}

func TestGetUserHandler_WithMock(t *testing.T) {
    mockRepo := NewMockRepository()

    // Test data setup - প্রথমে একটা user add করছি
    mockRepo.SaveUser("test_user", "test@example.com")

    handler := GetUserHandler(mockRepo)

    req := httptest.NewRequest("GET", "/user/get?username=test_user", nil)
    w := httptest.NewRecorder()

    handler(w, req)

    // Status check
    if w.Code != http.StatusOK {
        t.Errorf("Expected status 200, got %d", w.Code)
    }

    // GetUser call হয়েছে কিনা check
    if mockRepo.GetUserCalled != 1 {
        t.Errorf("Expected GetUser to be called once, called %d times", mockRepo.GetUserCalled)
    }

    // Response data verify
    var response map[string]string
    json.NewDecoder(w.Body).Decode(&response)

    if response["username"] != "test_user" {
        t.Errorf("Expected username test_user, got %s", response["username"])
    }
    if response["email"] != "test@example.com" {
        t.Errorf("Expected email test@example.com, got %s", response["email"])
    }
}

func TestGetUserHandler_UserNotFound(t *testing.T) {
    mockRepo := NewMockRepository()

    // কোনো user add করছি না, তাই "not found" error আসা উচিত

    handler := GetUserHandler(mockRepo)

    req := httptest.NewRequest("GET", "/user/get?username=nonexistent", nil)
    w := httptest.NewRecorder()

    handler(w, req)

    // এবার error response expect করছি
    if w.Code != http.StatusInternalServerError {
        t.Errorf("Expected status 500, got %d", w.Code)
    }
}

Mock Testing-এর সুবিধাগুলো

সুবিধা ২.১: কোনো External Dependency নেই

দেখো, test চালাতে আমার MongoDB, PostgreSQL কিছুই লাগছে না। MockRepository সব কিছু memory-তে করছে। এর মানে হলো যেকোনো machine-এ, যেকোনো সময় এই tests run করা যাবে।

সুবিধা ২.২: Tests অনেক Fast

Real database access নেই, তাই প্রতিটা test milliseconds-এ শেষ হয়ে যাচ্ছে। ১০০টা test থাকলেও ১ সেকেন্ডের মধ্যে শেষ হয়ে যাবে।

সুবিধা ২.৩: Test Behavior Control করা যায়

MockRepository-তে তুমি যেকোনো scenario simulate করতে পারো। ধরো তুমি test করতে চাও database connection fail হলে কী হয়। Mock-এ সহজেই error return করতে পারো:

func (m *MockRepository) SaveUser(username, email string) error {
    // Simulate database failure
    if username == "fail_test" {
        return fmt.Errorf("database connection failed")
    }
    m.users[username] = email
    return nil
}

এভাবে different scenarios test করা যায় real database ছাড়াই।

সুবিধা ২.৪: CI/CD-তে সহজে Run হয়

GitHub Actions, GitLab CI বা যেকোনো CI/CD pipeline-এ এই tests কোনো setup ছাড়াই run হবে। কারণ কোনো external service লাগছে না।


সমস্যা #৩: নতুন Feature Add করা কঠিন

এখন ধরো তোমাকে একটা নতুন feature add করতে বলা হলো: user-দের activity log করতে হবে। প্রতিবার কোনো user create বা fetch হলে সেটা log file-এ লিখতে হবে।

Coupled Code-এ এই Feature Add করতে গেলে

যদি তোমার code coupled থাকে, তাহলে তোমাকে প্রতিটা handler modify করতে হবে:

// ❌ এভাবে করতে হবে coupled code-এ
func CreateUserHandler(db *MongoDB, logger *FileLogger) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        username := r.URL.Query().Get("username")
        email := r.URL.Query().Get("email")

        // Database operation
        err := db.SaveUser(username, email)
        if err != nil {
            logger.LogError("Failed to save user", err)
            http.Error(w, "Error saving user", http.StatusInternalServerError)
            return
        }

        // Logging করতে হচ্ছে manually
        logger.LogInfo(fmt.Sprintf("User created: %s", username))

        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(map[string]string{
            "message": "User created successfully",
        })
    }
}

এই approach-এর সমস্যা:

সমস্যা ৩.১: প্রতিটা Handler Modify করতে হচ্ছে

তোমার যত handlers আছে সবগুলোতে logging logic add করতে হবে। এটা error-prone এবং repetitive।

সমস্যা ৩.২: Multiple Dependencies

এখন handler দুটো concrete type-এর উপর depend করছে - *MongoDB আর *FileLogger। আরও dependency add হলে code আরও messy হবে।

সমস্যা ৩.৩: Single Responsibility Principle ভাঙছে

Handler-এর responsibility হওয়া উচিত শুধু HTTP request handle করা। কিন্তু এখন সে logging-ও করছে, database-ও handle করছে।


সমাধান #৩: Decorator Pattern দিয়ে Feature Add করা

Interface ইউজ করলে তুমি decorator pattern apply করতে পারো। এর মানে হলো তুমি একটা repository-কে অন্য repository দিয়ে wrap করতে পারো যেটা extra functionality add করবে, কিন্তু original behavior change করবে না।

Logging Repository Decorator

package main

import (
    "fmt"
    "time"
)

// LoggingRepository যেটা অন্য repository-কে wrap করে
// এবং সব operations log করে
type LoggingRepository struct {
    // Wrapped repository - এটা যেকোনো UserRepository হতে পারে
    wrapped UserRepository
    logFile string
}

// Constructor যেটা যেকোনো repository wrap করতে পারে
func NewLoggingRepository(repo UserRepository) *LoggingRepository {
    return &LoggingRepository{
        wrapped: repo,
        logFile: "user_activity.log",
    }
}

// UserRepository interface implement করছি
// কিন্তু actual কাজ wrapped repository-তে delegate করছি
func (lr *LoggingRepository) SaveUser(username, email string) error {
    // Operation করার আগে log
    start := time.Now()
    lr.log(fmt.Sprintf("SaveUser called for username: %s", username))

    // Actual operation wrapped repository-কে দিয়ে করাচ্ছি
    err := lr.wrapped.SaveUser(username, email)

    // Operation শেষে log
    duration := time.Since(start)
    if err != nil {
        lr.log(fmt.Sprintf("SaveUser failed for %s: %v (took %v)", username, err, duration))
    } else {
        lr.log(fmt.Sprintf("SaveUser succeeded for %s (took %v)", username, duration))
    }

    return err
}

func (lr *LoggingRepository) GetUser(username string) (string, error) {
    start := time.Now()
    lr.log(fmt.Sprintf("GetUser called for username: %s", username))

    // Actual operation
    email, err := lr.wrapped.GetUser(username)

    duration := time.Since(start)
    if err != nil {
        lr.log(fmt.Sprintf("GetUser failed for %s: %v (took %v)", username, err, duration))
    } else {
        lr.log(fmt.Sprintf("GetUser succeeded for %s, email: %s (took %v)", username, email, duration))
    }

    return email, err
}

// Helper method logging-এর জন্য
func (lr *LoggingRepository) log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    logEntry := fmt.Sprintf("[%s] %s\n", timestamp, message)
    fmt.Print(logEntry)
    // এখানে actual file-এ write করতে পারো
}

এবার দেখো কত সহজে ইউজ করা যায়

func main() {
    // ১. প্রথমে base repository create করি
    baseRepo := &PostgreSQL{
        ConnectionString: "postgres://localhost:5432",
    }

    // ২. তারপর সেটাকে logging দিয়ে wrap করি
    // এখন সব operations automatically log হবে!
    loggedRepo := NewLoggingRepository(baseRepo)

    // ৩. Handlers-এ pass করি
    // Handlers-এর কোনো code change করতে হয়নি!
    http.HandleFunc("/user/create", CreateUserHandler(loggedRepo))
    http.HandleFunc("/user/get", GetUserHandler(loggedRepo))

    fmt.Println("Server চালু হয়েছে port 8080-তে")
    http.ListenAndServe(":8080", nil)
}

Decorator Pattern-এর Power

সুবিধা ৩.১: Zero Code Change in Handlers

লক্ষ করো, handlers-এর একটা লাইনও change করতে হয়নি। শুধু main() function-এ repository wrap করে দিয়েছি, সব logging automatically হচ্ছে।

সুবিধা ৩.২: Composable

তুমি চাইলে multiple decorators stack করতে পারো:

func main() {
    baseRepo := &PostgreSQL{ConnectionString: "..."}

    // প্রথমে logging wrap
    loggedRepo := NewLoggingRepository(baseRepo)

    // তারপর caching wrap
    cachedRepo := NewCachingRepository(loggedRepo)

    // তারপর retry logic wrap
    finalRepo := NewRetryRepository(cachedRepo, 3)

    // সব features একসাথে পেয়ে গেলে!
    http.HandleFunc("/user/create", CreateUserHandler(finalRepo))
}

সুবিধা ৩.৩: Easy to Test Individually

প্রতিটা decorator আলাদাভাবে test করা যায়:

func TestLoggingRepository(t *testing.T) {
    // Mock base repository
    mockRepo := NewMockRepository()

    // Wrap with logging
    loggedRepo := NewLoggingRepository(mockRepo)

    // Test logging behavior
    loggedRepo.SaveUser("test", "test@test.com")

    // Verify mock was called
    if mockRepo.SaveUserCalled != 1 {
        t.Error("Base repository should be called")
    }
}

সমস্যা #৪: Production আর Development Environment আলাদা

আরেকটা common problem হলো, development-এ তুমি হয়তো local SQLite database ইউজ করছ, কিন্তু production-এ AWS RDS PostgreSQL ইউজ করতে হবে। এছাড়া staging environment-এ আবার আলাদা setup থাকতে পারে।

Environment-Specific Configuration

Coupled code-এ এই scenario handle করা অনেক কঠিন। তোমাকে lots of if-else লিখতে হবে:

// ❌ Coupled approach
func setupDatabase(env string) interface{} {
    if env == "development" {
        return &SQLite{Path: "./dev.db"}
    } else if env == "staging" {
        return &PostgreSQL{ConnectionString: "staging-url"}
    } else if env == "production" {
        return &PostgreSQL{ConnectionString: "production-url"}
    }
    return nil
}

// Problem: return type interface{}, type safety নেই
// আরও problem: এই function ইউজ করতে গেলে type assertion করতে হবে

সমাধান #৪: Interface দিয়ে Clean Environment Handling

Interface থাকলে তুমি খুব সহজেই environment-specific implementations ইউজ করতে পারো:

package main

import (
    "fmt"
    "os"
)

// Factory function যেটা environment অনুযায়ী সঠিক repository return করে
func NewRepository(env string) UserRepository {
    switch env {
    case "development":
        // Development-এ in-memory mock ইউজ করি fast iteration-এর জন্য
        fmt.Println("Using mock repository for development")
        return NewMockRepository()

    case "staging":
        // Staging-এ PostgreSQL test database
        fmt.Println("Using PostgreSQL for staging")
        return &PostgreSQL{
            ConnectionString: os.Getenv("STAGING_DB_URL"),
        }

    case "production":
        // Production-এ actual PostgreSQL with connection pooling
        fmt.Println("Using PostgreSQL for production")
        prodRepo := &PostgreSQL{
            ConnectionString: os.Getenv("PRODUCTION_DB_URL"),
        }

        // Production-এ logging আর caching add করি
        loggedRepo := NewLoggingRepository(prodRepo)
        cachedRepo := NewCachingRepository(loggedRepo)

        return cachedRepo

    default:
        panic(fmt.Sprintf("Unknown environment: %s", env))
    }
}

func main() {
    // Environment variable থেকে পড়ি
    env := os.Getenv("APP_ENV")
    if env == "" {
        env = "development" // Default
    }

    // সঠিক repository পাই
    repo := NewRepository(env)

    // বাকি সব একই থাকে
    http.HandleFunc("/user/create", CreateUserHandler(repo))
    http.HandleFunc("/user/get", GetUserHandler(repo))

    fmt.Printf("Server চালু হয়েছে %s environment-এ\n", env)
    http.ListenAndServe(":8080", nil)
}

এই Approach-এর সুবিধা

সুবিধা ৪.১: Type Safety

Factory function UserRepository interface return করছে, তাই compile time-এই type check হবে। কোনো runtime type assertion লাগছে না।

সুবিধা ৪.২: Clear Configuration

Environment-specific logic একটা জায়গায় centralized। তোমার পুরো codebase-এ if-else scattered না।

সুবিধা ৪.৩: Easy to Add New Environments

নতুন environment add করতে চাইলে শুধু factory function-এ একটা case add করলেই হবে।

সুবিধা ৪.৪: Consistent Interface

Application code কখনও জানে না কোন environment-এ run হচ্ছে। সব জায়গায় same UserRepository interface ইউজ হচ্ছে।


সমস্যা #৫: Third-Party Library Integration করা কঠিন

এখন ধরো তুমি একটা third-party caching library ইউজ করতে চাও, যেমন Redis। কিন্তু সেই library-র API তোমার current code structure-এর সাথে match করছে না।

Third-Party API যেটা আমাদের Pattern-এ Fit করছে না

// ধরো Redis library-র API এরকম:
type RedisClient struct {
    host string
    port int
}

func (r *RedisClient) Connect() error { /* ... */ }
func (r *RedisClient) SetValue(key, value string) error { /* ... */ }
func (r *RedisClient) GetValue(key string) (string, bool) { /* ... */ }

// সমস্যা: এই API আমাদের UserRepository interface-এর সাথে match করে না
// SetValue/GetValue আছে, কিন্তু SaveUser/GetUser নেই

Coupled code-এ তোমাকে পুরো application modify করতে হবে এই library ইউজ করতে। কিন্তু interface থাকলে?


সমাধান #৫: Adapter Pattern দিয়ে Third-Party Integration

Interface-এর আরেকটা powerful use case হলো adapter pattern। তুমি যেকোনো third-party library-কে তোমার interface-এ adapt করতে পারো।

Redis Adapter বানানো

package main

import (
    "encoding/json"
    "fmt"
)

// Redis adapter যেটা RedisClient-কে UserRepository-তে convert করে
type RedisUserRepository struct {
    client *RedisClient
}

func NewRedisUserRepository(host string, port int) (*RedisUserRepository, error) {
    client := &RedisClient{
        host: host,
        port: port,
    }

    err := client.Connect()
    if err != nil {
        return nil, fmt.Errorf("failed to connect to Redis: %w", err)
    }

    return &RedisUserRepository{
        client: client,
    }, nil
}

// এবার UserRepository interface implement করি
// Redis API-কে আমাদের interface-এ adapt করছি
func (r *RedisUserRepository) SaveUser(username, email string) error {
    // Redis-এ JSON হিসেবে save করছি
    userData := map[string]string{
        "username": username,
        "email":    email,
    }

    jsonData, err := json.Marshal(userData)
    if err != nil {
        return err
    }

    // Redis library-র SetValue method ইউজ করছি
    key := fmt.Sprintf("user:%s", username)
    return r.client.SetValue(key, string(jsonData))
}

func (r *RedisUserRepository) GetUser(username string) (string, error) {
    key := fmt.Sprintf("user:%s", username)

    // Redis library-র GetValue method ইউজ করছি
    jsonData, exists := r.client.GetValue(key)
    if !exists {
        return "", fmt.Errorf("user not found")
    }

    // JSON parse করছি
    var userData map[string]string
    err := json.Unmarshal([]byte(jsonData), &userData)
    if err != nil {
        return "", err
    }

    return userData["email"], nil
}

এবার যেকোনো জায়গায় ইউজ করা যাবে

func main() {
    // Redis repository create করি
    redisRepo, err := NewRedisUserRepository("localhost", 6379)
    if err != nil {
        panic(err)
    }

    // Application code-এ কোনো change লাগছে না!
    // কারণ এটাও UserRepository interface implement করে
    http.HandleFunc("/user/create", CreateUserHandler(redisRepo))
    http.HandleFunc("/user/get", GetUserHandler(redisRepo))

    fmt.Println("Server চালু হয়েছে Redis-এর সাথে")
    http.ListenAndServe(":8080", nil)
}

Adapter Pattern-এর Power

সুবিধা ৫.১: Vendor Lock-in থেকে মুক্তি

তুমি যদি ভবিষ্যতে Redis থেকে Memcached-এ switch করতে চাও, শুধু adapter change করলেই হবে। Application logic touch করতে হবে না।

সুবিধা ৫.২: Multiple Implementations একসাথে ইউজ করা

func main() {
    // Primary database PostgreSQL
    primaryRepo := &PostgreSQL{ConnectionString: "..."}

    // Cache layer Redis
    cacheRepo, _ := NewRedisUserRepository("localhost", 6379)

    // দুটো একসাথে ইউজ করি - caching strategy implement করছি
    finalRepo := NewCachingRepository(primaryRepo, cacheRepo)

    http.HandleFunc("/user/create", CreateUserHandler(finalRepo))
}

সুবিধা ৫.৩: Library Upgrade সহজ

Redis library নতুন version-এ API change করলে শুধু adapter update করলেই হবে। Whole application rewrite করতে হবে না।


Interface-এর Actual Power কোথায়?

এতক্ষণ আমরা অনেকগুলো problem আর solution দেখলাম। এবার চলো summarize করি interface actually কী কী power দেয়:

Power #1: Dependency Inversion

High-level code (handlers) আর low-level code (database) এর মধ্যে abstraction layer তৈরি হয়। High-level code কখনও low-level details জানে না। এটাকে বলে Dependency Inversion Principle।

Traditional dependency:

Handler → PostgreSQL
(high-level depends on low-level)

With interface:

Handler → UserRepository Interface ← PostgreSQL
(both depend on abstraction)

Power #2: Polymorphism

একই interface multiple implementations থাকতে পারে, আর runtime-এ decide করা যায় কোনটা ইউজ হবে। এটা traditional object-oriented polymorphism-এর একটা form।

// একই handler, different implementations
var repo UserRepository

repo = &PostgreSQL{...}      // SQL database
repo = &MongoDB{...}          // NoSQL database
repo = NewMockRepository()    // Testing
repo = &RedisUserRepository{...} // Cache

Power #3: Open/Closed Principle

তোমার code "open for extension, closed for modification"। মানে নতুন functionality add করতে পারো existing code না ছুঁয়ে।

// নতুন feature? নতুন implementation লিখো
type AuditedRepository struct {
    wrapped UserRepository
}

// Existing handlers modify করতে হবে না!

Power #4: Testability

Testing অনেক সহজ হয়ে যায় কারণ তুমি real dependencies-র বদলে mocks/stubs inject করতে পারো।

Power #5: Flexibility

Future requirements change হলে easily adapt করা যায়। Database switch করতে চাও? New caching layer add করতে চাও? Monitoring add করতে চাও? সব easily possible।


শেষ কথা: Interface কখন ইউজ করবে?

Interface খুবই powerful, কিন্তু সব জায়গায় ইউজ করার দরকার নেই। এখানে কিছু guideline:

Interface ইউজ করো যখন:

১. Multiple Implementations সম্ভব যদি একটা functionality-র multiple way of implementation থাকতে পারে, তাহলে interface ইউজ করো। যেমন database, file storage, message queue ইত্যাদি।

২. Testing Dependency আছে যদি কোনো external system (database, API, file system) এর সাথে interaction করতে হয়, interface ইউজ করলে testing সহজ হয়।

৩. Behavior Abstraction দরকার যখন তুমি "কী করতে হবে" define করতে চাও, "কীভাবে করতে হবে" নয়। Interface হলো behavioral contract।

৪. Extensibility চাও Future-এ নতুন functionality add করার সম্ভাবনা থাকলে interface ইউজ করো।

Interface ইউজ করো না যখন:

১. শুধু একটা Implementation যদি নিশ্চিত যে কখনও alternate implementation লাগবে না, premature abstraction করো না।

২. Simple Helper Functions calculateTax(), formatDate() এরকম simple utility functions-এর জন্য interface overkill।

৩. Internal/Private Code Package-এর internal implementation details-এ interface unnecessary complexity add করতে পারে।


পুরো Example একসাথে

চলো শেষবারের মতো পুরো example একসাথে দেখি, সব concepts apply করে:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

// ========== Interface Definition ==========
type UserRepository interface {
    SaveUser(username, email string) error
    GetUser(username string) (string, error)
}

// ========== Implementations ==========

// PostgreSQL implementation
type PostgreSQL struct {
    ConnectionString string
}

func (db *PostgreSQL) SaveUser(username, email string) error {
    fmt.Printf("PostgreSQL: Saving %s (%s)\n", username, email)
    return nil
}

func (db *PostgreSQL) GetUser(username string) (string, error) {
    fmt.Printf("PostgreSQL: Fetching %s\n", username)
    return fmt.Sprintf("%s@example.com", username), nil
}

// Mock implementation for testing
type MockRepository struct {
    users map[string]string
}

func NewMockRepository() *MockRepository {
    return &MockRepository{users: make(map[string]string)}
}

func (m *MockRepository) SaveUser(username, email string) error {
    m.users[username] = email
    return nil
}

func (m *MockRepository) GetUser(username string) (string, error) {
    if email, ok := m.users[username]; ok {
        return email, nil
    }
    return "", fmt.Errorf("user not found")
}

// ========== Decorators ==========

// Logging decorator
type LoggingRepository struct {
    wrapped UserRepository
}

func NewLoggingRepository(repo UserRepository) *LoggingRepository {
    return &LoggingRepository{wrapped: repo}
}

func (lr *LoggingRepository) SaveUser(username, email string) error {
    fmt.Printf("LOG: SaveUser called for %s\n", username)
    err := lr.wrapped.SaveUser(username, email)
    if err != nil {
        fmt.Printf("LOG: SaveUser failed: %v\n", err)
    }
    return err
}

func (lr *LoggingRepository) GetUser(username string) (string, error) {
    fmt.Printf("LOG: GetUser called for %s\n", username)
    return lr.wrapped.GetUser(username)
}

// ========== HTTP Handlers ==========

func CreateUserHandler(repo UserRepository) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        username := r.URL.Query().Get("username")
        email := r.URL.Query().Get("email")

        err := repo.SaveUser(username, email)
        if err != nil {
            http.Error(w, "Error saving user", http.StatusInternalServerError)
            return
        }

        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(map[string]string{
            "message": "User created successfully",
        })
    }
}

func GetUserHandler(repo UserRepository) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        username := r.URL.Query().Get("username")

        email, err := repo.GetUser(username)
        if err != nil {
            http.Error(w, "Error fetching user", http.StatusInternalServerError)
            return
        }

        json.NewEncoder(w).Encode(map[string]string{
            "username": username,
            "email":    email,
        })
    }
}

// ========== Factory ==========

func NewRepository(env string) UserRepository {
    var baseRepo UserRepository

    switch env {
    case "development":
        baseRepo = NewMockRepository()
    case "production":
        baseRepo = &PostgreSQL{
            ConnectionString: os.Getenv("DB_URL"),
        }
    default:
        baseRepo = NewMockRepository()
    }

    // Production-এ logging add করি
    if env == "production" {
        return NewLoggingRepository(baseRepo)
    }

    return baseRepo
}

// ========== Main ==========

func main() {
    env := os.Getenv("APP_ENV")
    if env == "" {
        env = "development"
    }

    repo := NewRepository(env)

    http.HandleFunc("/user/create", CreateUserHandler(repo))
    http.HandleFunc("/user/get", GetUserHandler(repo))

    fmt.Printf("Server running in %s mode on :8080\n", env)
    http.ListenAndServe(":8080", nil)
}

এই final example-এ তুমি দেখতে পাচ্ছো কীভাবে:

  • Interface দিয়ে database abstraction করা হয়েছে

  • Multiple implementations (PostgreSQL, Mock) আছে

  • Decorator pattern দিয়ে logging add করা হয়েছে

  • Environment-specific configuration করা হয়েছে

  • Handlers পুরোপুরি decoupled এবং testable

Interface-এর মূল শক্তি হলো এটা তোমার code-কে flexible, maintainable এবং testable বানায়। এটা শুধু Go-তেই নয়, software engineering-এর একটা fundamental principle।

More from this blog

Low Level Design: গভীর থেকে বোঝা এবং আয়ত্ত করা

ভূমিকা: কেন এই Article? তুমি হয়তো programming শিখেছ। Variable, loop, function, data structure - সব জানো। কিন্তু যখন একটা বড় system বানাতে বসো, তখন মনে হয় কোথা থেকে শুরু করব? কীভাবে organize করব? Code লিখতে লিখতে হারিয়ে যাও একটা maze-এ। এই feeling...

Oct 15, 202520 min read71
Low Level Design: গভীর থেকে বোঝা এবং আয়ত্ত করা

তোমার Project-এ Coupled Code কীভাবে খুঁজে বের করবে?

Coupled code খোঁজা মানে হচ্ছে তোমার codebase-এ এমন জায়গা খুঁজে বের করা যেখানে একটা অংশ আরেকটার উপর বেশি depend করছে। এটা একটা detective work — তুমি clue খুঁজবে, pattern দেখবে, এবং সমস্যা চিহ্নিত করবে। চলো step by step শিখি কীভাবে এটা করতে হয়। কেন ...

Oct 14, 202510 min read2

Go-তে Object (Struct Instance) তৈরির সম্পূর্ণ গাইড

Go programming শেখার সময় একটা জিনিস খুব তাড়াতাড়ি বুঝতে হয় - কীভাবে object তৈরি করতে হয়। অন্য language যেমন Java বা Python এ class আছে, কিন্তু Go-তে আছে struct। আর struct এর instance বানানোই হলো object তৈরি করা। আজকের এই blog এ আমরা দেখব Go-তে ob...

Oct 13, 202524 min read3
Go-তে Object (Struct Instance) তৈরির সম্পূর্ণ গাইড
I

Imran Hasan

61 posts

Full-stack developer with experience in developing and managing web applications. Skilled in React, Node.js, HTML, CSS, and JavaScript. Experience in managing website hosting and security.