۰
در این سری مقالات، قصد دارم تجربه خودم را در ساخت اپلیکیشنها با زبان Golang با استفاده از Echo Framework و اصولی مثل DDD (Domain-Driven Design) و CQRS (Command Query Responsibility Segregation) به اشتراک بگذارم. انتخاب این تکنولوژیها و رویکردها اتفاقی نیست. Golang به خاطر کارایی و سادگیاش شناخته شده است، Echo به بهبود سرعت و سهولت در توسعه برنامههای وب کمک میکند، و DDD و CQRS امکان ساخت سیستمهای مقیاسپذیر و قابل نگهداری را میدهند.
در عمل، بارها دیدم که خیلی از توسعهدهندگان به درستی اصول DDD را درک نکردهاند، بهخصوص در بخش تفکیک لایهها. خطای رایج این است که لایه دامنه (domain) و لایه زیرساخت (infrastructure) به خوبی از هم جدا نمیشوند و Entityهای دامنه بهعنوان مدلهای لایه زیرساخت اشتباه گرفته میشوند.
در این مقاله روی پیادهسازی عملی تمرکز میکنیم: ساختار پوشهها و راهاندازی یک سرور HTTP برای پروژه – بکاند یک اپلیکیشن. در اینجا زیاد وارد نظریههای DDD و CQRS نمیشویم، چون پیشتر مطالب زیادی نوشته شده و اطلاعات براحتی در دسترس است.
اول، ساختار پوشههای پروژه آیندهمان را بررسی کنیم. این ساختار به ما کمک میکند تا کدها را طبق اصول DDD و CQRS به خوبی سازماندهی کنیم.
1. 2├── cmd # رابط خط فرمان و نقطه شروع اصلی اپلیکیشن 3│ └── myapp # پوشه اصلی اپلیکیشن 4│ ├── cli # کد مختص CLI، شامل تعریف دستورات و پارسرها 5│ │ ├── cli.go # تنظیم CLI و پیکربندی دستورات 6│ └── main.go # نقطه ورود برنامه 7├── internal # کد داخلی برنامه – دسترسی از بیرون ندارد 8│ ├── application # لایه Application: هماهنگی جریان برنامه، پیکربندی و پیادهسازی CQRS 9│ │ ├── app.go # تنظیمات و مقداردهی اولیه اپلیکیشن 10│ │ ├── command # بخش Command در CQRS: اجرای دستورات 11│ │ │ └── command.go # هندلکردن Command در CQRS 12│ │ └── query # بخش Query در CQRS: درخواستهای خواندن داده 13│ │ └── query.go # هندلکردن Query در CQRS 14│ ├── domain # منطق اصلی دامنه: Entityها، Aggregateها، سرویسها و اینترفیس Repository 15│ │ ├── aggregate # Aggregateهای دامنه، مجموعهای از Entityها که با هم پردازش میشوند 16│ │ ├── entity # Entityهای دامنه، نهادهای اصلی کسبوکار 17│ │ ├── repository # اینترفیسهای Repository دامنه، تعریفهای انتزاعی برای دسترسی به داده 18│ │ └── service # سرویسهای دامنه: منطق کسبوکار که بهخوبی در Entity یا Aggregate جای نمیگیرد 19│ ├── genmocks.go # تولید mock برای تستهای واحد و یکپارچه 20│ ├── infrastructure # لایه زیرساخت: فریمورکها، درایورها و ابزارهای فنی 21│ │ ├── api # APIها، بهخصوص HTTP برای تعامل وب 22│ │ │ └── rest # پیادهسازیهای اختصاصی REST: سرورها، هندلرها، middleware 23│ │ │ ├── handler # هندلرهای REST، جهت پردازش درخواستهای HTTP ورودی 24│ │ │ ├── middleware # میانافزار REST، مثلاً لاگینگ یا تأیید هویت 25│ │ │ └── validator # اعتبارسنجی درخواستهای REST 26│ │ ├── decorator # دکوراتورها برای افزودن یا تغییر رفتار (مثلاً لاگینگ، متریکها) 27│ │ │ ├── decorator.go # دکوراتورهای پایه، برای موضوعات سراسری 28│ │ │ └── logging.go # دکوراتور لاگینگ 29│ │ └── pgsql # پیادهسازی PostgreSQL: مدلها و Repositoryهای دیتابیس 30│ │ ├── model # مدلهای داده برای PostgreSQL 31│ │ └── repository # پیادهسازی Repository دامنه برای PostgreSQL 32│ └── mocks # Mockهای تست، خودکار یا دستی برای تست واحد 33└── pkg # کتابخانهها و ابزارهای مشترک، قابل استفاده مجدد در پروژههای دیگر
کد این ساختار روی گیتهاب من موجود است: https://github.com/i4erkasov/go-ddd-cqrs
برای شروع توسعه سرور HTTP، ابتدا یک فایل main.go
در مسیر ./cmd/myapp
میسازیم. فعلاً این فایل را خالی میگذاریم و روی کار اصلی تمرکز میکنیم.
Echo Framework اساس سرور HTTP ماست. در معماری DDD، سرور HTTP در لایه زیرساخت قرار دارد. یعنی رابطی بین دنیای بیرون و اپلیکیشن ما است.
برای ساماندهی کد سرور، ساختار زیر را در ./internal/infrastructure/api/rest
ایجاد میکنیم:
server.go
— پیادهسازی سرورroute.go
— تعریف مسیرها (routes)در پوشههای handler
، middleware
و validator
، کامپوننتهای مربوطه گذاشته میشوند:
Handler (handler/handler.go
): فایل اصلی هندلرها؛ هر هندلر پاسخ به یک درخواست HTTP را بر عهده دارد.
Middleware (middleware/middleware.go
): شامل میدلورها، مثلاً برای لاگینگ یا احراز هویت.
Validator (validator/validator.go
): اعتبارسنجهای سفارشی که درخواستهای ورودی HTTP را با شرایط مورد انتظار مقایسه میکنند.
ساختار دایرکتوری rest
به شکل زیر است:
1. 2├── handler 3│ └── handler.go 4├── middleware 5│ └── middleware.go 6├── routes.go 7├── server.go 8└── validator 9 └── validator.go
در handler.go
ساختار و سازنده زیر را تعریف میکنیم (در ادامه پروژه گسترشش میدهیم):
1package handler 2 3type Handler struct {} 4 5func New() *Handler { 6 return &Handler{} 7}
مشابه همین کار را برای middleware.go
انجام میدهیم:
1package middleware 2 3type Middleware struct {} 4 5func New() *Middleware { 6 return &Middleware{} 7}
در validator.go
از کتابخانه https://github.com/go-playground/validator استفاده میکنیم اما باید آن را با اینترفیس echo.Validator
سازگار کنیم. این کار را با یک wrapper انجام میدهیم:
1package validator 2 3import ( 4 "github.com/go-playground/validator/v10" 5 "github.com/labstack/echo/v4" 6) 7 8// پیادهسازی wrapper 9type wrapper struct { 10 validator *validator.Validate 11} 12 13func New() echo.Validator { 14 return &wrapper{ 15 validator: validator.New(), 16 } 17} 18 19// اعتبارسنجی دادهها 20func (v *wrapper) Validate(i interface{}) error { 21 return v.validator.Struct(i) 22}
حالا سراغ server.go
میرویم.
قبلاً بگویم که در پیادهسازی سرور از این پکیجها استفاده میکنیم:
ساختار سرور به صورت زیر است:
1type Server struct { 2 echo *echo.Echo 3 log *zap.Logger 4 cfg *viper.Viper 5}
ما به دو متد عمومی نیاز داریم: New
به عنوان سازندهی سرور و Start
که سرور را اجرا میکند. قبل از آن، سرور را با استفاده از Viper پیکربندی میکنیم.
یک پوشه .bin
در ریشه پروژه بسازید و داخل آن فایل کانفیگ config.dev.yaml
را ایجاد و تنظیمات زیر را اضافه کنید:
1app: 2 environment: "development" 3 api: 4 rest: 5 host: "127.0.0.1" 6 port: "8088" 7 setting: 8 debug: true 9 hide_banner: false 10 hide_port: false
حالا بیایید route.go
را تعریف کنیم:
1package rest 2 3import ( 4 "net/http" 5 6 "github.com/labstack/echo/v4" 7) 8 9func (s *Server) routes(h *handler.Handler, m *middleware.Middleware) { 10 11}
اکنون که مقدمات آماده است، سرور را پیادهسازی میکنیم. برای خوانایی کد، پیادهسازی را به چند متد خصوصی دستهبندی کردم: start
، configure
و handleErrors
. کد نهایی به این شکل است:
1package rest 2 3import ( 4 "context" 5 "net" 6 "net/http" 7 8 "github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest/handler" 9 "github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest/middleware" 10 "github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest/validator" 11 "github.com/juju/errors" 12 "github.com/labstack/echo/v4" 13 "github.com/spf13/viper" 14 "go.uber.org/zap" 15) 16 17type Server struct { 18 echo *echo.Echo 19 log *zap.Logger 20 cfg *viper.Viper 21} 22 23func New(cfg *viper.Viper, log *zap.Logger) (*Server, error) { 24 server := &Server{ 25 echo: echo.New(), 26 log: log, 27 cfg: cfg, 28 } 29 30 server.configure(cfg.Sub("setting")) 31 32 server.routes( 33 handler.New(), 34 middleware.New(), 35 ) 36 37 return server, nil 38} 39 40func (s *Server) Start(ctx context.Context) error { 41 errorChan := make(chan error, 1) 42 43 go s.start(errorChan) 44 45 select { 46 case <-ctx.Done(): 47 s.log.Info("Shutting down the server") 48 if shutdownErr := s.echo.Shutdown(ctx); shutdownErr != nil { 49 s.log.Error("Error shutting down the server", zap.Error(shutdownErr)) 50 return shutdownErr 51 } 52 case err := <-errorChan: 53 s.log.Fatal("Failed to start HTTP server", zap.Error(err)) 54 return err 55 } 56 57 return nil 58} 59 60func (s *Server) start(errorChan chan<- error) { 61 defer close(errorChan) 62 63 if err := s.echo.Start( 64 net.JoinHostPort( 65 s.cfg.GetString("host"), 66 s.cfg.GetString("port"), 67 ), 68 ); err != nil && !errors.Is(err, http.ErrServerClosed) { 69 errorChan <- err 70 } 71} 72 73func (s *Server) configure(cfg *viper.Viper) { 74 if cfg.IsSet("debug") { 75 s.echo.Debug = cfg.GetBool("debug") 76 } 77 78 if cfg.IsSet("hide_banner") { 79 s.echo.HideBanner = cfg.GetBool("hide_banner") 80 } 81 82 if cfg.IsSet("hide_port") { 83 s.echo.HidePort = cfg.GetBool("hide_port") 84 } 85 86 s.echo.Validator = validator.New() 87 s.echo.HTTPErrorHandler = handleErrors(s.log, cfg.GetBool("debug")) 88} 89 90func handleErrors(log *zap.Logger, debug bool) echo.HTTPErrorHandler { 91 return func(err error, c echo.Context) { 92 var ( 93 code = http.StatusInternalServerError 94 msg string 95 errorStack any 96 ) 97 98 if he, ok := err.(*echo.HTTPError); ok { 99 code = he.Code 100 msg = he.Message.(string) 101 } else { 102 msg = err.Error() 103 switch true { 104 case errors.Is(err, errors.BadRequest): 105 code = http.StatusBadRequest 106 case errors.Is(err, errors.Forbidden): 107 code = http.StatusForbidden 108 case errors.Is(err, errors.Unauthorized): 109 code = http.StatusUnauthorized 110 case errors.Is(err, errors.NotFound): 111 code = http.StatusNotFound 112 case errors.Is(err, errors.AlreadyExists): 113 code = http.StatusConflict 114 } 115 116 if debug { 117 errorStack = errors.ErrorStack(err) 118 } 119 } 120 121 if !c.Response().Committed { 122 if err != nil && code == http.StatusInternalServerError { 123 log.Error("An error occurred", zap.Error(err)) 124 } 125 126 if c.Request().Method == echo.HEAD { 127 err = c.NoContent(code) 128 } else { 129 m := echo.Map{"error": msg} 130 if errorStack != nil { 131 m["errorStack"] = errorStack 132 } 133 err = c.JSON(code, m) 134 } 135 } 136 } 137}
حالا اولین هندلر خودمان را در پوشه handler
، فایلی به نام hello_world.go
بسازیم:
1package handler 2 3import ( 4 "net/http" 5 6 "github.com/labstack/echo/v4" 7) 8 9func (h *Handler) HelloWold(c echo.Context) error { 10 return c.String(http.StatusOK, "Hello, World!") 11}
برای این هندلر، یک مسیر (route) هم در route.go
اضافه میکنیم:
1package rest 2 3import ( 4 "github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest/handler" 5 "github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest/middleware" 6) 7 8func (s *Server) routes(h *handler.Handler, m *middleware.Middleware) { 9 s.echo.GET("/hello", h.HelloWold) 10}
حالا که سرورمان آماده است، یک دستور برای اجرای آن مینویسیم. برای مدیریت CLI از پکیج https://github.com/spf13/cobra استفاده میکنیم. در ./cmd/myapp/cli
فایلی به نام cli.go
میسازیم:
1package cli 2 3import ( 4 "github.com/spf13/cobra" 5 "github.com/spf13/viper" 6) 7 8var ( 9 config string 10 cfg *viper.Viper 11) 12 13var cmd = &cobra.Command{ 14 Use: "cmd", 15 Short: "ShortDescription ", 16 Long: `Long Description`, 17 PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 18 cfg = viper.New() 19 cfg.AddConfigPath(".") 20 cfg.AutomaticEnv() 21 cfg.SetConfigFile(config) 22 return cfg.ReadInConfig() 23 }, 24} 25 26func Execute() error { 27 return cmd.Execute() 28} 29 30func init() { 31 cmd.PersistentFlags().StringVarP( 32 &config, "config", "c", "config.yml", 33 "path to config file", 34 ) 35}
در همان مسیر، فایل http_server.go
را ایجاد و کد اجرای سرور را قرار میدهیم. توجه کنید که اینجا لاگر بهصورت موقت ایجاد شده و بعداً اصلاح میشود:
1package cli 2 3import ( 4 "os" 5 "time" 6 7 "github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest" 8 "github.com/spf13/cobra" 9 "go.uber.org/zap" 10 "go.uber.org/zap/zapcore" 11) 12 13const HttpServerCommand = "http-server" 14const VersionHttpServer = "1.0.0" 15 16var httpServer = &cobra.Command{ 17 Use: HttpServerCommand, 18 Short: "Start http server", 19 Version: VersionHttpServer, 20 RunE: func(cmd *cobra.Command, args []string) (err error) { 21 bws := &zapcore.BufferedWriteSyncer{ 22 WS: os.Stderr, 23 Size: 512 * 1024, 24 FlushInterval: time.Minute, 25 } 26 defer bws.Stop() 27 consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) 28 core := zapcore.NewCore(consoleEncoder, bws, zapcore.DebugLevel) 29 log := zap.New(core) 30 31 cnf := cfg.Sub("app.api.rest") 32 33 var server *rest.Server 34 if server, err = rest.New(cnf, log); err != nil { 35 return err 36 } 37 38 return server.Start(cmd.Context()) 39 }, 40} 41 42func init() { 43 cmd.AddCommand(httpServer) 44}
و در نهایت، در ./cmd/myapp/main.go
که ابتدا ساختیم، کد زیر را وارد کنید:
1package main 2 3import ( 4 "fmt" 5 "os" 6 7 "github.com/i4erkasov/go-ddd-cqrs/cmd/myapp/cli" 8) 9 10func main() { 11 if err := cli.Execute(); err != nil { 12 _, _ = fmt.Fprintf(os.Stderr, "Some error occurred during execute app. Error: %v\n", err) 13 14 os.Exit(2) 15 } 16}
حالا میتوانید سرور را اجرا کنید. وارد دایرکتوری ./cmd/myapp
شوید و دستور زیر را اجرا کنید (مسیر فایل کانفیگتان را به جای <path to the configuration file>
قرار دهید):
1go run main.go http-server -c <path to the configuration file>
برای من چنین بود:
1go run main.go http-server -c ~/go/src/github.com/i4erkasov/go-ddd-cqrs/.bin/config.dev.yaml
در کنسول باید پیام زیر را ببینید:
سرور با موفقیت اجرا شده است. حالا میتوانید در مرورگر به آدرس http://127.0.0.1:8088/hello بروید و پاسخ "Hello, World!" را ببینید.
در این مقاله با ساختار دایرکتوری پروژه آشنا شدیم و راهاندازی سرور HTTP REST را شروع کردیم. این مراحل پایهای مهم برای توسعه بعدی اپلیکیشن بر اساس اصول DDD و CQRS هستند. در مقاله بعدی، اضافه کردن دستورات مرتبط با migrationها، بررسی لایه application و اهمیت پوشه pkg
را دنبال خواهیم کرد.
کد تمام آنچه اینجا پیادهسازی شده در گیتهاب موجود است: https://github.com/i4erkasov/go-ddd-cqrs/tree/part-1
۰
کد با می متعهد است که بالاترین سطح کیفی آموزش را در اختیار شما بگذارد. هدف به اشتراک گذاشتن دانش فناوری اطلاعات و توسعه نرم افزار در بالاترین سطح ممکن برای درستیابی به جامعه ای توانمند و قدرتمند است. ما باور داریم هر کسی میتواند با استمرار در یادگیری برنامه نویسی چالش های خود و جهان پیرامون خود را بر طرف کند و به موفقیت های چشم گیر برسد. با ما در این مسیر همراه باشید. کد با می اجتماع حرفه ای برنامه نویسان ایرانی.