[৮.৪] প্রজেক্ট তৈরির রোডম্যাপ এবং বিস্তারিত (Detailed project roadmap)
[৮.৪.১] এনভায়রনমেন্ট ভ্যারিয়েবল ঠিক করা
কিছু কিছু ইনফরমেশন সবার জন্য আলাদা হওয়াটা স্বাভাবিক। পাশাপাশি আমরা কিছু প্রজেক্ট ইনফরমেশন শেয়ার ও করতে চাইবো না। যেমন, আমাদের MySQL প্রোফাইলের যাবতীয়। তাছাড়া আমরা চাইবো প্রজেক্ট রান করতে প্রয়োজনীয় পরিবর্তনশীল ভ্যালুগুলোকে এমন ভাবে সেট করতে, যাতে করে যে কেও আমাদের প্রজেক্ট নামিয়ে রান করতে পারে তাদের নিজেদের ডিভাইসে। এনভায়রনমেন্ট ভ্যারিয়েবল সেই সুবিধাটাই আমাদের দিয়ে থাকে। শুরুতেই একদম রুটে একটি app.env ফাইল তৈরি করি :
book-crud/app.env
DBUSER=root
DBPASS=191491
DBIP=tcp(127.0.0.1:3306)
DBNAME=bookstore
PORT=9030
DBIP নির্দেশ করে ডাটাবেসের কোন পোর্টে যোগাযোগ করব, DBNAME হচ্ছে যে ডাটাবেসটা আমরা ব্যবহার করব। সবশেষে PORT হচ্ছে আমাদের ব্যাকএন্ডের সার্ভারের সাথে যোগাযোগের পথ।
কিছু বিষয় আমাদের খেয়ালে থাকতে হবে –
- যেন ডাটাবেসটা আগে থেকেই তৈরি করা থাকে।
- ব্যাকএন্ড সার্ভারের পোর্টটা যেন অন্য কোথাও ব্যবহৃত না থাকে।
[৮.৪.২] Viper এর সাহায্যে এনভায়রনমেন্ট গ্লোবালি সেট করা
শুধু app.env ফাইলে ভ্যালু গুলো সেট করলেই হবেনা। সেটাকে যেন আমাদের প্রয়োজনে আমরা প্রজেক্টের যেকোনো জায়গা থেকে অ্যাক্সেস করতে পারি সেটার ব্যাবস্থাও করতে হবে। এই উদ্দেশ্যে আমরা viper প্যাকেজটা ব্যবহার করব। ধাপে ধাপে এই সেটআপ প্রণালী নিচে ব্যাখ্যা করা হলো –
এখন আমরা যা কোড করব তার সবই হবে config/config.go তে :
- প্রথমেই আমরা viper প্যাকেজটি নামিয়ে নেবো। এজন্য টার্মিনাল থেকে নিচের কমান্ডটি রান করতে হবে go get github.com/spf13/viper
- একটি Struct তৈরি করে নেবো যা go get github.com/spf13/viper তে আমরা সকল এনভাইরনপমেন্ট ভ্যারিয়েবল গুলো ম্যাপ করব । নামকরণের ক্ষেত্রে ভ্যারিয়েবলগুলোর নাম বড় হাতের অক্ষর দিয়ে শুরু করতে হবে নইলে অন্যান্য জায়গা থেকে এর অ্যাক্সেস পাওয়া যাবে না –
type Config struct {
DBUser string `mapstructure:"DBUSER"`
DBPass string `mapstructure:"DBPASS"`
DBIP string `mapstructure:"DBIP"`
DbName string `mapstructure:"DBNAME"`
Port string `mapstructure:"PORT"`
}
3. এবারে আমরা একটি ফাংশন তৈরি করব InitConfig() নামে যেখানে আমাদের যাবতীয় কনফিগারেশনের প্রয়োজনীয় ভ্যালু গুলো Config Struct সেট করে দিবো
func InitConfig() (*Config) {
viper.AddConfigPath(".")
/* যদি একদম রুটে app.env ফাইল থেকে থাকে তাহলে শুধু (.) দিয়েই এর অবস্থান নির্দেশ করা যাবে। অন্যথা অবস্থান অনুযায়ী পাথ দিতে হবে */
viper.SetConfigName("app") //কনফিগ ফাইলের নাম
viper.SetConfigType("env") //কনফিগ ফাইলের এক্সটেনশন (env,json,yaml)
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
log.Fatal("Error reading env file", err)
}
var config *Config
if err := viper.Unmarshal(&config); err != nil {
//config স্ট্রাক্টে সবগুলো ভ্যালু ম্যাপ হয়ে গেলো
log.Fatal("Error reading env file", err)
}
return config
}
4. এবারে আমরা একটি ফাংশন তৈরি করব SetConfig() নামে যেটা একবার কল করার মাধ্যমে আমরা একটি গ্লোবাল ভ্যারিয়েবলে প্রথমেই সব ভ্যালু নিয়ে নিবো এবং প্রয়োজন অনুসারে ব্যবহার করব ।
var LocalConfig *Config
func SetConfig() {
LocalConfig = InitConfig()
}
5. সবশেষে আমাদের কোড টা তাহলে দাঁড়াচ্ছে –
config/config.go
package config
import (
"log"
"github.com/spf13/viper"
)
var LocalConfig *Config
type Config struct {
DBUser string `mapstructure:"DBUSER"`
DBPass string `mapstructure:"DBPASS"`
DBIP string `mapstructure:"DBIP"`
DbName string `mapstructure:"DBNAME"`
Port string `mapstructure:"PORT"`
}
func initConfig() *Config {
viper.AddConfigPath(".")
viper.SetConfigName("app")
viper.SetConfigType("env")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
log.Fatal("Error reading env file", err)
}
var config *Config
if err := viper.Unmarshal(&config); err != nil {
log.Fatal("Error reading env file", err)
}
return config
}
func SetConfig() {
LocalConfig = initConfig()
}
[৮.৪.৩] ডাটাবেস টেবিলের মডেল তৈরি
এখন আমরা যে মডেলটি বানাবো তা models/book.go তে থাকবে :
- একটি Book নামের Struct তৈরি করব, এটাই হবে আমাদের টেবিলের স্কিমা –
models/book.go
package models
type Book struct {
ID uint `gorm:"primaryKey;autoIncrement"`
BookName string
Author string
Publication string
}
2. এখানে ID এর পাশে gorm এর ট্যাগ primaryKey এবং autoIncrement ব্যবহার করা হয়েছে। এগুলোর কাজ এদের নামেই প্রকাশ পাচ্ছে।
[৮.৪.৪] ডাটাবেসের কানেকশন স্থাপন
এখন আমরা যা কোড করব তার সবই হবে connection/connection.go তে :
- প্রথমেই gorm এবং MySQL এর জন্য প্রয়োজনীয় প্যাকেজগুলো নামিয়ে নেই। এজন্য টার্মিনালে নিচের কোড গুলো রান করব –
সাথে আমাদের আরো ২ টি লোকাল প্যাকেজ এক্সপোর্ট করতে হবে। সেগুলো হচ্ছে একটু আগের বানানো “Go-bootcamp/pkg/config” ও “Go-bootcamp/pkg/models” প্যাকেজ।
2. এবারে আমরা একটি *gorm.DB ভ্যারিয়েবল এবং Connect() নামে ফাংশন তৈরি করব, যেটি config/config.go এর LocalConfig এর ফিল্ড ভ্যালু গুলো ব্যবহার করে ডাটাবেস কানেকশন স্থাপন করবে। সবশেষে তৈরিকৃত ডাটাবেসের ইন্সট্যান্সটি *gorm.DB ভ্যারিয়েবলটিতে রাখবে এবং এই ইন্সট্যান্স ব্যবহার করেই আমরা পরবর্তীতে ডাটাবেসের নানান অপারেশন করব –
go get gorm.io/gorm
go get gorm.io/driver/mysql
সাথে আমাদের আরো ২ টি লোকাল প্যাকেজ এক্সপোর্ট করতে হবে। সেগুলো হচ্ছে একটু আগের বানানো “go-bootcamp/pkg/config” ও “go-bootcamp/pkg/models” প্যাকেজ।
3. এবারে আমরা একটি *gorm.DB ভ্যারিয়েবল এবং Connect() নামে ফাংশন তৈরি করব, যেটি config/config.go এর LocalConfig এর ফিল্ড ভ্যালু গুলো ব্যবহার করে ডাটাবেস কানেকশন স্থাপন করবে। সবশেষে তৈরিকৃত ডাটাবেসের ইন্সট্যান্সটি *gorm.DB ভ্যারিয়েবলটিতে রাখবে এবং এই ইন্সট্যান্স ব্যবহার করেই আমরা পরবর্তীতে ডাটাবেসের নানান অপারেশন করব –
var db *gorm.DB
func connect() {
dbConfig := config.LocalConfig
dsn := fmt.
Sprintf("%s:%s@%s/%s?charset=utf8mb4&parseTime=True&loc=Local",
dbConfig.DBUser, dbConfig.DBPass, dbConfig.DBIP, dbConfig.DbName)
d, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
fmt.Println("error connecting to DB")
panic(err)
}
fmt.Println("Database Connected")
db = d
}
4. এবারে একটি Migrate() ফাংশন বানাব যার মাধ্যমে আমাদের মডেল অনুযায়ী ডাটাবেসের টেবিল তৈরি বা আপডেট হবে প্রতিবার প্রোগ্রাম রান করার সময়।
func migrate() {
db.Migrator().AutoMigrate(&models.Book{})
}
5. সবশেষে একটি GetDB() ফাংশন বানাবো, যেটি connect() ও migrate() ফাংশন চালিয়ে আপডেটেড *gorm.DB এর ইন্সট্যান্স রিটার্ন করবে।
func GetDB() *gorm.DB {
if db == nil {
connect()
}
migrate()
return db
}
6. তাহলে আমাদের ফাইনাল কোডটি দাঁড়াচ্ছে –
connection/connection.go
package connection
import (
"fmt"
"go-bootcamp/pkg/config"
"go-bootcamp/pkg/models"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var db *gorm.DB
func connect() {
dbConfig := config.LocalConfig
dsn := fmt.
Sprintf("%s:%s@%s/%s?charset=utf8mb4&parseTime=True&loc=Local",
dbConfig.DBUser, dbConfig.DBPass, dbConfig.DBIP, dbConfig.DbName)
d, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
fmt.Println("error connecting to DB")
panic(err)
}
fmt.Println("Database Connected")
db = d
}
func migrate() {
db.Migrator().AutoMigrate(&models.Book{})
}
func GetDB() *gorm.DB {
if db == nil {
connect()
}
migrate()
return db
}
৮.৪.৫ main ফাংশন এবং ECHO ইন্সট্যান্স তৈরি
এখন আমরা যা কোড করব তা হবে main.go তে :
- main.go তে main() ফাংশনের মধ্যে আমরা echo.New() ফাংশন কল করে ডিফল্ট কনফিগারেশন সহ ECHO ফ্রেমওয়ার্কের একটি নতুন ইন্সট্যান্স তৈরি করি। তার আগে আমাদের echo এর প্যাকেজটি নামানোর জন্য টার্মিনালে নিচে কোডটি রান করব – go get github.com/labstack/echo/v4 এরপর –
func main() {
e := echo.New()
}
2. এবারে আমরা এই echo.Echo ইন্সট্যান্স টি container/serve.go তে পাঠিয়ে দিবো এবং বাকি সবধরনের ইন্সট্যান্স কন্ট্রোলের কাজ ওখানেই হবে। এর জন্য আমাদের “Go-bootcamp/pkg/containers” লোকাল প্যাকেজটি ইমপোর্ট করতে হবে।
package main
import (
"Go-bootcamp/pkg/containers"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
containers.Serve(e)
}
[৮.৪.৬] Routes কন্ট্রোল এবং সার্ভার চালানো
এখন আমরা container/serve.go তে কোড করব :
- এখানে একটি ফাংশন Serve() তৈরি করব যা main.go থেকে আসা echo.Echo ইন্সট্যান্সটিকে রিসিভ করবে এবং তা routes/book.go তে পাঠিয়ে দিবে আমাদের প্রয়োজনীয় সকল routes তৈরির জন্য । এর জন্য আমাদের লোকাল প্যাকেজ “Go-bootcamp/pkg/routes” ইমপোর্ট করা লাগবে। পাশাপাশি “github.com/labstack/echo/v4” প্যাকেজটিও ইমপোর্ট করা লাগবে।
- সাথে সার্ভার রান করার জন্য config/config.go এর LocalConfig ভ্যারিয়েবলটি ব্যবহার করব। তার আগে আমরা SetConfig() ফাংশনটা চালাবো, কারণ আমাদের পুরো প্রজেক্টের সবধরনের Initialization এর কাজ এই Serve() ফাংশনের মাধ্যমে করব । এর জন্য আমাদের লোকাল প্যাকেজ “Go-bootcamp/pkg/config” ইমপোর্ট করা লাগবে।
- এর আগে যে আমরা ডাটাবেসের সংযোগ স্থাপনের জন্য LocalConfig এর ভ্যালু গুলো ব্যবহার করেছি সেটা সম্ভব হবে না Serve() ফাংশনটি রান করা ব্যতিত।
- তাহলে আমাদের ফাইনাল কোডটি দাঁড়াচ্ছে –
container/serve.go
package containers
import (
"fmt"
"Go-bootcamp/pkg/config"
"Go-bootcamp/pkg/routes"
"log"
"github.com/labstack/echo/v4"
)
func Serve(e *echo.Echo) {
config.SetConfig()
// config initialization
routes.BookRoutes(e)
log.Fatal(e.Start(fmt.Sprintf(":%s", config.LocalConfig.Port)))
// সার্ভার স্টার্ট
}
বিঃদ্রঃ অন্যান্য সবকিছুর মতো ডাটাবেসের ইন্সট্যান্স তৈরি ও ম্যানেজ করার জন্য আমরা আবার container/serve.go তে ফেরত আসব।
[৮.৪.৭] Routes তৈরি
এখন আমরা যা কোড করব তা হবে routes/book.go তে :
- প্রথমেই আমরা একটি গ্রুপ করব echo.Echo ইন্সট্যান্সটি ব্যবহার করে এবং আমাদের অন্যান্য সকল route, এই গ্রুপের মধ্যে পড়বে। অর্থাৎ গ্রুপের route যদি /bookstore হয়ে থাকে তাহলে এই গ্রুপের মধ্যের /book – route টি আসলে /bookstore/book – route টি নির্দেশ করবে। পাশাপাশি আমাদের “github.com/labstack/echo/v4” প্যাকেজটি ইমপোর্ট করা লাগবে :
book := e.Group("/bookstore")
2. এবারে আমাদের route গুলো book গ্রুপের নিচে তৈরি করব এবং প্রত্যেক route ভিন্ন ভিন্ন রিকোয়েস্টের জন্য এন্ডপয়েন্ট হিসেবে কাজ করবে।
3. প্রত্যেক এন্ডপয়েন্ট হ্যান্ডেল করার জন্য একটি করে ফাংশন ব্যবহৃত হবে, যেগুলোকে বলা হয় হ্যান্ডলার ফাংশন এবং এই ফাংশনগুলো controllers/book.go তে থাকবে। সুতরাং এক্ষেত্রে আমাদের “Go-bootcamp/pkg/controllers” প্যাকেজটি ইমপোর্ট করতে হচ্ছে।
4. পুরো কোডটি তাহলে দাঁড়াচ্ছে –
package routes
import (
"Go-bootcamp/pkg/controllers"
"github.com/labstack/echo/v4"
)
func BookRoutes(e *echo.Echo) {
book := e.Group("/bookstore")
book.POST("/book", controllers.CreateBook)
book.GET("/book", controllers.GetBooks)
book.PUT("/book/:bookID", controllers.UpdateBook)
book.DELETE("/book/:bookID", controllers.DeleteBook)
}
5. এখানে POST, GET, PUT, DELETE গুলোই হচ্ছে HTTP রিকোয়েস্টের ধরণ এবং এই অনুযায়ী প্রত্যেকের হ্যান্ডলার ফাংশন গুলো রেসপন্স তৈরি করবে।
[৮.৪.৮] কন্ট্রোলারের ছোট্ট বিবরণী
যেহেতু routes/book.go এ controllers/book.go এর উল্লেখ করেছি সুতরাং এখানে আমরা আপাতত ফাংশনগুলো তৈরি করে রেখে যাই। রিকোয়েস্ট হ্যান্ডেল করা এবং রেসপন্সের বিস্তারিত খানিকটা পরে যেয়ে দেখব –
package controllers
import "github.com/labstack/echo/v4"
func CreateBook(e echo.Context) error
func GetBooks(e echo.Context) error
func UpdateBook(e echo.Context) error
func DeleteBook(e echo.Context) error
[৮.৪.৯] ইন্টারফেস বাইন্ডিং
এই অংশটা পুরো প্রজেক্টের অত্যন্ত গুরুত্বপূর্ণ একটি অংশ। ইন্টারফেসের সাথে আমরা পার্ট ৩ থেকেই পরিচিত। সহজ ভাষায়, ইন্টারফেস হলো কিছু ফাংশনের সমষ্টি। এক্ষেত্রে আমাদের ডাটাবেস এবং সার্ভিসে Create, Read, Update এবং Delete অপারেশনের জন্য কিছু ফাংশন অবশ্য প্রয়োজন।
অনেক গুলো ফাংশন নিয়ে কাজ করতে গেলে যে সমস্যার সম্মুখীন হতে হয় সেটা হচ্ছে কখন কোন ফাংশন নিয়ে কাজ করছি সেটা পরিষ্কার থাকে না। কিন্তু আমরা এই সমস্যার সমাধান হিসেবে যা করতে পারি সেটা হচ্ছে, এই অবশ্য ফাংশনগুলোর ইমপ্লিমেন্ট করাটাকে আবশ্যক বানিয়ে ফেলা। সহজ ভাষায়, যদি আমরা ইন্টারফেসের কোনো ফাংশন ইমপ্লিমেন্ট না করি বা সঠিক উপায়ে ইমপ্লিমেন্ট না করি, সেক্ষেত্রে আমরা এররের সম্মুখীন হবো।
এই ইন্টারফেসের ইমপ্লিমেন্টেশনকে বাধ্যবাধকতায় রূপান্তর করার পদ্ধতিই হলো ইন্টারফেস বাইন্ডিং ।
প্রথমেই আমরা domain/book.go তে আমাদের ইন্টারফেসগুলো লিখে ফেলি :
package domain
import (
"Go-bootcamp/pkg/models"
"Go-bootcamp/pkg/types"
)
// ডাটাবেস ইন্টারফেস
type IBookRepo interface {
GetBooks(bookID uint) []models.Book
CreateBook(book *models.Book) error
UpdateBook(book *models.Book) error
DeleteBook(bookID uint) error
}
// সার্ভিস ইন্টারফেস
type IBookService interface {
GetBooks(bookID uint) ([]types.BookRequest, error)
CreateBook(book *models.Book) error
UpdateBook(book *models.Book) error
DeleteBook(bookID uint) error
}
-
ইন্টারফেস বাইন্ডিং এর প্রয়োগ
[৮.৪.১০] প্রয়োজনীয় Struct তৈরি
ইন্টারফেস বাইন্ডিং অংশেরর IBookService ইন্টারফেসের রিটার্নটাইপে আমরা types.BookRequest এর একটি অ্যারে দেখতে পাচ্ছি। আমরা models.Book এর অ্যারে রিটার্ন না করে types.BookRequest এর অ্যারে রিটার্ন করার কারণ হলো, আমরা models.Book এর ব্যবহার শুধু ডাটাবেসের কাজে করব, যেহেতু এটা ডাটাবেস টেবিলের স্কিমা। আর সার্ভিস প্যাকেজে যেহেতু আমরা রেসপন্স তৈরি করে কন্ট্রোলারে পাঠাবো সেহেতু সেই রেসপন্সটার জন্য আমরা আলাদা Struct ব্যবহার করব।
তাহলে types/structures.go তে আমরা এই কাস্টম Struct গুলো রাখব – types/structures.go
package types
type BookRequest struct {
ID uint `json:"bookID"`
BookName string `json:"bookname"`
Author string `json:"author"`
Publication string `json:"publication,omitempty"`
}
পরবর্তীতে আমাদের আরো কাস্টম Struct এর প্রয়োজন পরতে পারে। সেগুলোর সবই আমরা types/structures.go তে রাখব।
[৮.৪.১১] ডাটাবেস ফাংশনগুলোর ইন্টারফেস বাইন্ডিং
domain/book.go তে উল্লেখিত IBookRepo ইন্টারফেস টাই হলো ডাটাবেসের ফাংশন গুলোর ইন্টারফেস, অর্থাৎ এই ফাংশনগুলো দ্বারা আমরা সরাসরি ডাটাবেসের সাথে যোগাযোগ করব। এজন্য আমাদের “Go-bootcamp/pkg/domain” প্যাকেজটি ইমপোর্ট করা লাগবে।
যেহেতু আমাদের ডাটাবেসের অপারেশন গুলোর জন্য *gorm.DB বা ডাটাবেসের ইন্সট্যান্সের প্রয়োজন হবে, সুতরাং ডাটাবেস ইন্সট্যান্সটি একটি অবজেক্টের মধ্যে রেখে আমরা উপরোক্ত ডাটাবেস ইন্টারফেসটি উক্ত অবজেক্টের সাথে বাইন্ড করে দিতে পারি :
type bookRepo struct {
db *gorm.DB
}
func BookDBInstance(d *gorm.DB) domain.IBookRepo {
return &bookRepo{
db: d,
}
}
উপরোক্ত কোডে কি হয়েছে তা ধাপে ধাপে বুঝিয়ে দিচ্ছি –
- প্রথমত একটি Struct নেয়া হয়েছে, bookRepo নামে, যার ভেতরে একটি ফিল্ড db রাখা হয়েছে *gorm.DB টাইপ।
- পরবর্তী ধাপে BookDBInstance() ফাংশনে বাইরে থেকে কোনো *gorm.DB ইন্সট্যান্স আসার পর তা bookRepo এর db তে রেখে রিটার্ন টাইপে domain/book.go এর IBookRepo ইন্টারফেসটি দেয়া হয়েছে।
- এটা করার সাথে সাথেই আমরা দেখতে পাবো পুরো ফাংশনটায় এরর দেখাচ্ছে। কারণ, এখন এই bookRepo অবজেক্টের সাথে ইন্টারফেসটির বাইন্ডিং হয়ে গিয়েছে এবং ইন্টারফেসের ফাংশনগুলো অবজেক্টটির মেথড হিসেবে ইমপ্লিমেন্ট না করা পর্যন্ত এই এরর দেখাতে থাকবে।
- এখন আমরা ফাংশনগুলোর ইমপ্লিমেন্টেশন দেখতে পাবো। বিস্তারিত ইমপ্লিমেন্টেশন কিছুক্ষণ পরে দেখা যাবে :
repositories/book.go
package repositories
import (
"Go-bootcamp/pkg/domain"
"Go-bootcamp/pkg/models"
"gorm.io/gorm"
)
type bookRepo struct {
db *gorm.DB
}
func BookDBInstance(d *gorm.DB) domain.IBookRepo {
return &bookRepo{
db: d,
}
}
func (repo *bookRepo) GetBooks(bookID uint) []models.Book
func (repo *bookRepo) CreateBook(book *models.Book) error
func (repo *bookRepo) UpdateBook(book *models.Book) error
func (repo *bookRepo) DeleteBook(bookID uint) error
উপরোক্ত কোডে আমরা দেখতে পাচ্ছি ইন্টারফেসের ৪ টি ফাংশনকে bookRepo এর মেথড হিসেবে ইমপ্লিমেন্ট করা হয়েছে। এছাড়াও যেহেতু ডাটাবেসের কাজ করার জন্য models/book.go এর Book Struct ব্যবহার করা লাগবে সুতরাং “Go-bootcamp/pkg/models” ও ডাটাবেসের জন্য “gorm.io/gorm” প্যাকেজটি ইমপোর্ট করা লাগবে।
[৮.৪.১২] সার্ভিস ফাংশনগুলোর ইন্টারফেস বাইন্ডিং
ইতোমধ্যেই ডাটাবেসের সাথে IBookRepo ইন্টারফেস বাইন্ড করে ফেলেছি আমরা। সুতরাং সার্ভিসের জন্য IBookService ইন্টারফেস বাইন্ড করাটা কঠিন কিছু হবে না। এখানে আবার একটু মাথা খাটাবো আমরা –
ডাটাবেসের জন্য *gorm.DB ইন্সট্যান্সের অবজেক্টের সাথে আমরা ইন্টারফেস বাইন্ড করেছিলাম কেননা ওখানে আমাদের কাজ ছিল ডাটাবেস নিয়ে। তাহলে সার্ভিস থেকে আমরা যেহেতু ডাটাবেসের ফাংশনগুলো কল করবো, সেহেতু এখানে আমরা IBookService ইন্টারফেসটি IBookRepo এর সাথে বাইন্ড করে দেবো। এতে করে আমরা সার্ভিস থেকে IBookRepo এর ফাংশনগুলো কল করতে পারবো যা repositories/book.go এ আছে। এজন্য আমাদের “Go-bootcamp/pkg/domain” প্যাকেজটি ইমপোর্ট করা লাগবে।
তাহলে শুরু করা যাক সার্ভিস ইন্টারফেস বাইন্ডিং :
type BookService struct {
repo domain.IBookRepo
}
func BookServiceInstance(bookRepo domain.IBookRepo) domain.IBookService {
return &BookService{
repo: bookRepo,
}
}
- উপরোক্ত কোডে কি হয়েছে টা ধাপে ধাপে বুঝিয়ে দিচ্ছি –
- প্রথমত একটি Struct নেয়া হয়েছে, BookService নামে, যার ভেতরে একটি ফিল্ড repo রাখা হয়েছে যা domain.IBookRepo টাইপ।
- পরবর্তী ধাপে BookServiceInstance() ফাংশনে বাইরে থেকে কোনো domain.IBookRepo ইন্সট্যান্স আসার পর তা BookService এর repo তে রেখে রিটার্ন টাইপে domain/book.go এর IBookService ইন্টারফেসটি দেয়া হয়েছে।
- এটা করার সাথে সাথেই আমরা দেখতে পাবো পুরো ফাংশনটায় এরর দেখাচ্ছে। কারণ, এখন এই BookService অবজেক্টের সাথে ইন্টারফেসটির বাইন্ডিং হয়ে গিয়েছে এবং ইন্টারফেসের ফাংশনগুলো অবজেক্টটির মেথড হিসেবে ইমপ্লিমেন্ট না করা পর্যন্ত এই এরর দেখাতে থাকবে।
- এখন আমরা ফাংশনগুলোর ইমপ্লিমেন্টেশন দেখতে পাবো। বিস্তারিত ইমপ্লিমেন্টেশন কিছুক্ষণ পরে দেখা যাবে :
service/book.go
package services
import (
"Go-bootcamp/pkg/domain"
"Go-bootcamp/pkg/models"
"Go-bootcamp/pkg/types"
)
type BookService struct {
repo domain.IBookRepo
}
func BookServiceInstance(bookRepo domain.IBookRepo) domain.IBookService {
return &BookService{
repo: bookRepo,
}
}
func (service *BookService) GetBooks(bookID uint) ([]types.BookRequest, error)
func (service *BookService) CreateBook(book *models.Book) error
func (service *BookService) UpdateBook(book *models.Book) error
func (service *BookService) DeleteBook(bookID uint) error
উপরোক্ত কোডে আমরা দেখতে পাচ্ছি ইন্টারফেসের ৪ টি ফাংশনকে BookService এর মেথড হিসেবে ইমপ্লিমেন্ট করা হয়েছে। এছাড়াও যেহেতু রেসপন্সের কাজ করার জন্য types/structures.go এর BookRequest Struct ব্যবহার করা লাগবে এবং ডাটাবেসের ফাংশনগুলোতে পাস করার জন্য models/book.go এর Book Struct ব্যবহার করা লাগবে সুতরাং “Go-bootcamp/pkg/types” ও “Go-bootcamp/pkg/models” প্যাকেজ ইমপোর্ট করা লাগবে।
[৮.৪.১৩] কন্ট্রোলারে সার্ভিস ইন্টারফেসের ইন্সট্যান্স
আগের অংশের আমরা দেখেছি, IBookRepo এর ইন্সট্যান্স কিভাবে সার্ভিসে নিয়ে তার সাথে IBookService ইন্টারফেসটি বাইন্ড করা হয়েছে। এতে করে আমরা পরবর্তীতে কোড করার সময় সার্ভিস থেকে IBookRepo এর ফাংশনগুলো কল করতে পারবো যা repositories/book.go এ আছে। একইভাবে আমাদের সার্ভিস ইন্টারফেসের ও একটি ইন্সট্যান্স কন্ট্রোলারে প্রয়োজন, যাতে controllers/book.go থেকে আমরা সার্ভিসের ফাংশনগুলোকে কল করতে পারি। এজন্য আমাদের “Go-bootcamp/pkg/domain” প্যাকেজটি ইমপোর্ট করা লাগবে :
controller/book.go
var BookService domain.IBookService
func SetBookService(bService domain.IBookService) {
BookService = bService
}
এখন আমরা সার্ভিসের সকল ফাংশন BookService ব্যবহার করে কল করতে পারব ।
[৮.৪.১৪] ফেরত যাই container এ
- আমাদের ডাটাবেসের টেবিল তৈরির সবকিছু connection/connection.go তে থাকলেও আমরা কিন্তু কোথাও থেকে তা কল করিনি অর্থাৎ ডাটাবেসের কোন ইন্সট্যান্স তৈরি করিনি। কারণ, যখন আমরা connection/connection.go এর কাজ শেষ করেছিলাম তখনো ডাটাবেস ব্যবহার করা হচ্ছিল না কোথাও। আমরা জানি, ব্যবহার না করে কোনো ইন্সট্যান্স ফেলে রাখা যায়না Go তে।
আগেই বলেছিলাম আমরা ফিরে আসব containers/serve.go তে :
- যখন আমরা *gorm.DB এর ইন্সট্যান্সের সাথে repositories/book.go এ IBookRepo ইন্টারফেস বাইন্ড করেছিলাম BookDBInstance() ফাংশনের দ্বারা, তখন সেখানে *gorm.DB এর একটি ইন্সট্যান্স রিসিভ করেছিলাম। কিন্তু সেটা কোথা থেকে আসছিলো তা জানা ছিল না।
- এই মুহূর্তে Serve() ফাংশনে আমরা *gorm.DB এর একটি ইন্সট্যান্স তৈরি করবো (যার জন্য আমাদের “Go-bootcamp/pkg/connection” প্যাকেজটি ইমপোর্ট করতে হবে) এবং সেটা repositories/book.go এর BookDBInstance() ফাংশনে পাঠাবো । এজন্য আমাদের “Go-bootcamp/pkg/repositories” প্যাকেজটি ইমপোর্ট করতে হবে –
db := connection.GetDB()
bookRepo := repositories.BookDBInstance(db)
repositories/book.go তে *gorm.DB এর ইন্সট্যান্স পাঠানোর কাজ শেষ। আমরা রিটার্ন হিসেবে IBookRepo ইন্টারফেস বাইন্ড করা অবস্থায় পেয়েছি এবং সেটা bookRepo তে স্টোর করেছি।
এবার আসি সার্ভিসের ক্ষেত্রে। services/book.go তে আমরা BookServiceInstance() ফাংশনে একটি IBookRepo ইন্টারফেসের ইন্সট্যান্স রিসিভ করেছিলাম। এখন আমরা আগের লাইনে রিটার্ন পাওয়া IBookRepo এর ইন্সট্যান্স bookRepo কে services.BookServiceInstance() ফাংশনে পাস করে দেবো। এজন্য আমাদের “Go-bootcamp/pkg/services” প্যাকেজটি ইমপোর্ট করতে হবে। নিচের কোডটি দেখলে আরেকটু পরিষ্কার হবে ব্যাপারটা –
bookRepo := repositories.BookDBInstance(db)
bookService := services.BookServiceInstance(bookRepo)
3. সবশেষে, Serve() এ প্রাপ্ত IBookService এর ইন্সট্যান্স bookService কে controllers/book.go এর SetBookService() তে পাঠিয়ে দেবো যেটা সার্ভিসের ইন্টারফেস বাইন্ডিং এর একটি ইন্সট্যান্স রিসিভ করার জন্য রেখেছিলাম। এজন্য আমাদের “Go-bootcamp/pkg/controllers” প্যাকেজটি ইমপোর্ট করতে হবে –
bookRepo := repositories.BookDBInstance(db)
bookService := services.BookServiceInstance(bookRepo)
controllers.SetBookService(bookService)
4. তাহলে আমাদের ফাইনাল কোডটি দাঁড়াচ্ছে :
containers/server.go
package containers
import (
"fmt"
"Go-bootcamp/pkg/config"
"Go-bootcamp/pkg/connection"
"Go-bootcamp/pkg/controllers"
"Go-bootcamp/pkg/repositories"
"Go-bootcamp/pkg/routes"
"Go-bootcamp/pkg/services"
"log"
"github.com/labstack/echo/v4"
)
func Serve(e *echo.Echo) {
config.SetConfig()
db := connection.GetDB()
bookRepo := repositories.BookDBInstance(db)
bookService := services.BookServiceInstance(bookRepo)
controllers.SetBookService(bookService)
routes.BookRoutes(e)
log.Fatal(e.Start(fmt.Sprintf(":%s", config.LocalConfig.Port)))
}
5. এখানেই আমাদের container/serve.go এর কাজ শেষ। আমরা আর ফেরত আসব না এই প্যাকেজে, কথা দিচ্ছি…