14/10/2025
একটা 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("%[email protected]", 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("%[email protected]", 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("%[email protected]", 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=[email protected]", 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=[email protected]", 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 != "[email protected]" {
t.Errorf("Expected email [email protected], 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", "[email protected]")
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"] != "[email protected]" {
t.Errorf("Expected email [email protected], 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", "[email protected]")
// 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("%[email protected]", 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।
Credit: Imran Hasan