امروزه هر برنامه نویسی میدونه که اگر میخواد پروژه اش در آینده به بن بست نخوره، و بتونه به راحتی اون رو توسعه بده باید کدش منظم و قابل خواندن باشه و اگر هم بخواد در تیمی فعالیت بکنه همه ی اعضای تیم باید بر اساس یک قانون واحد کد نویسی کنن .
اما بین این همه معماری و قانون و دستورالعمل های مختلف کدوم رو باید انتخاب کرد که هم درعین سادگی ، منظم و قدرتمند باشه ؟ پاسخ این سوال معماری تمیز هست.
معماری تمیز توسط Robert C. Martin ( یا همون عمو باب معروف! ) توی کتاب :

معرفی شد.
توی این مقاله میخوایم باهم مفاهیم این معماری توی یه اپلیکیشن گولنگی پیاده سازی کنیم.
قبل از شروع پیاده سازی معماری تمیز، ما یکسری قوانین کلی و بیسیک داریم که باید اپلیکیشنمون رو طبق این موارد طراحی کنیم :
اگر دوست دارین بیشتر راجب موارد بالا بخونین میتونین این مقاله رو ببینین.
نکته : تمام لایه های برنامه ما باید بدون وابستگی قابل تست باشن.

خب عمو باب اومده 4 تا لایه توی معماری خودش آورده:
ما هم توی برنامه خودمون 4 تا لایه پیاده سازی میکنیم:
که در ادامه درمورد هرکدوم توضیح خواهم داد.
مدل ها مرکزی ترین لایه داخل برنامه مون هستن و توی تمام لایه ها استفاده میشن، این لایه تمام موجودیت های برنامه رو شامل میشه مثل: Article, Student, Book.
import "time" type Article struct { ID int64 `json:"id"` Title string `json:"title"` Content string `json:"content"` UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"` }
در واقع هر شیِ و آبجکتی اینجا ذخیره میشه.
تمام عملیات های مربوط به دیتابیس CRUD( ساخت، مشاهده، ویرایش و حذف)، اینجا انجام میشه و هیچ منطقی از برنامه توی این لایه اتفاق نمیفته. صرفا این لایه برای اتصال به دیتابیس کاربرد داره.
نکته : این لایه وابسته به دیتابیس هستش.
تمام پردازش های بیزینسی توی این لایه انتفاق میفته، وظیفه این لایه محاسبه و پردازش داده ها برای ارائه به لایه Delivery هستش.
و بعد این لایه با توجه به داده هایی که از لایه Delivery میگیره مشخص میکنه که باید داخل دیتابیس چیزی اضافه بشه و یا داده ای واکشی و برگدونده بشه.
نکته :با توجه به نیاز این لایه به عملیات های دیتابیسی وابسته به لایه ریپازیتوری هستش.
این لایه با توجه به اسمش وظیفه تحویل داده به کاربر رو داره که میتونه به صورت یک REST API یا یه صفحه HTM و یا هر نوع تایپی باشه.
و همچنین این لایه میتونه داده هم از کاربر بگیره که برای پردازش به لایه Usecase بفرسته.
نکته : این لایه به لایه Usecase وابسته هستش.
به غیر از لایه مدل که وابستگی خاصی نداره بقیه لایه ها نیاز به برقراری ارتباط بین هم دیگه دارن ما برای برقراری ارتباط بین لایه ها نیاز به یه قراردادی برای هر لایه داریم که اون لایه بر اساس قرارداد مشخص شده باید عمل کنه.
برای مثال ما توی لایه Usecase نیاز داریم که عملیاتی سمت دیتابیس انجام بشه و باید با لایه Repository ارتباط بگیریم. خب ما توی لایه Repository یه اینترفیس تعریف میکنیم و ازش بعنوان یک قرارداد برای برقراری ارتباط استفاده میکنیم:
type ArticleRepository interface { Fetch(cursor string, num int64) ([]*models.Article, error) GetByID(id int64) (*models.Article, error) GetByTitle(title string) (*models.Article, error) Update(article *models.Article) (*models.Article, error) Store(a *models.Article) (int64, error) Delete(id int64) (bool, error) }
ما باید طبق این قرارداد این ریپازیتوری رو پیاده سازی کنیم.
همانطور که بالاتر هم گفتیم تمام لایه ها باید بدون وابستگی به هم دیگه قابل تست باشن.
این لایه فقط درصورتی نیاز به تست نویسی داره که فانکشن یا متود خاصی درش تعریف شده باشه.
و در کل تست های این لایه خیلی ساده هستند.
برای تست این لایه با توجه به نیاز این لایه به دیتابیس بهترین راه برای این کار استفاده از ماکینگ( Mocking ) هستش. برای ماک کردن فانکشنالیتی mysql توی گولنگ میتونین از این پکیج استفاده کنین.
مثلا :
func TestGetByID(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf(“an error ‘%s’ was not expected when opening a stub database connection”, err) } defer db.Close() rows := sqlmock.NewRows([]string{ “id”, “title”, “content”, “updated_at”, “created_at”}). AddRow(1, “title 1”, “Content 1”, time.Now(), time.Now()) query := “SELECT id,title,content,updated_at, created_at FROM article WHERE ID = \\?” mock.ExpectQuery(query).WillReturnRows(rows) a := articleRepo.NewMysqlArticleRepository(db) num := int64(1) anArticle, err := a.GetByID(num) assert.NoError(t, err) assert.NotNil(t, anArticle) }
برای تست این لایه نیاز به یه ریپازیتوری داریم. ولی با توجه به اینکه نباید لایه ها توی تست وابسته باشن ما میتونیم طبق اینترفیسی که توی لایه ریپازیتوری ایجاد کردیم یه ماک از ریپازیتوری بسازیم و موقع تست بجای ریپازیتوری اصلی ریپازیتوری فیک رو تحویل لایه یوز کیس میدیم.
مثلا :
func TestFetch(t *testing.T) { mockArticleRepo := new(mocks.ArticleRepository) var mockArticle models.Article err := faker.FakeData(&mockArticle) assert.NoError(t, err) mockListArtilce := make([]*models.Article, 0) mockListArtilce = append(mockListArtilce, &mockArticle) mockArticleRepo.On("Fetch", mock.AnythingOfType("string"), mock.AnythingOfType("int64")).Return(mockListArtilce, nil) u := ucase.NewArticleUsecase(mockArticleRepo) num := int64(1) cursor := "12" list, nextCursor, err := u.Fetch(cursor, num) cursorExpected := strconv.Itoa(int(mockArticle.ID)) assert.Equal(t, cursorExpected, nextCursor) assert.NotEmpty(t, nextCursor) assert.NoError(t, err) assert.Len(t, list, len(mockListArtilce)) mockArticleRepo.AssertCalled(t, "Fetch", mock.AnythingOfType("string"), mock.AnythingOfType("int64")) }
با توجه به نیاز این لایه به لایه مرکزی ترش یعنی یوز کیس برای تست این لایه باید لایه یوز کیس رو طبق اینترفیس تعریف شده ماک کنیم و به این لایه پاس یدیم.
مثلا:
func TestGetByID(t *testing.T) { var mockArticle models.Article err := faker.FakeData(&mockArticle) assert.NoError(t, err) mockUCase := new(mocks.ArticleUsecase) num := int(mockArticle.ID) mockUCase.On(“GetByID”, int64(num)).Return(&mockArticle, nil) e := echo.New() req, err := http.NewRequest(echo.GET, “/article/” + strconv.Itoa(int(num)), strings.NewReader(“”)) assert.NoError(t, err) rec := httptest.NewRecorder() c := e.NewContext(req, rec) c.SetPath(“article/:id”) c.SetParamNames(“id”) c.SetParamValues(strconv.Itoa(num)) handler:= articleHttp.ArticleHandler{ AUsecase: mockUCase, Helper: httpHelper.HttpHelper{} } handler.GetByID(c) assert.Equal(t, http.StatusOK, rec.Code) mockUCase.AssertCalled(t, “GetByID”, int64(num)) }
برای ماک کردن Struct ها در گولنگ میتونین از این پکیج استفاده کنین.
بعد از اینکه کار همه لایه ها تموم شد و تست هاشون هم پاس میشه، حالا ما باید این لایه هارو بیاریم داخل فایل main.go و بهم وصلشون کنیم تا یک اپلیکیشن کامل ساخته بشه.
توی این فایل وابستگی ها و کانفیگ های مورد نیاز اپلیکیشن تعریف میشن و درواقع برنامه ما از این فایل شروع میشه.
برای نوشتن این فایل مثل زیر میتونین عمل کنین:
package main var config cfg.Config func init() { config = cfg.NewViperConfig() if config.GetBool(`debug`) { fmt.Println("Service RUN on DEBUG mode") } } func main() { dbHost := config.GetString(`database.host`) dbPort := config.GetString(`database.port`) dbUser := config.GetString(`database.user`) dbPass := config.GetString(`database.pass`) dbName := config.GetString(`database.name`) connection := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName) val := url.Values{} val.Add("parseTime", "1") val.Add("loc", "Asia/Tehran") dsn := fmt.Sprintf("%s?%s", connection, val.Encode()) dbConn, err := sql.Open(`mysql`, dsn) if err != nil && config.GetBool("debug") { fmt.Println(err) } defer dbConn.Close() e := echo.New() middL := middleware.InitMiddleware() e.Use(middL.CORS) ar := articleRepo.NewMysqlArticleRepository(dbConn) au := articleUcase.NewArticleUsecase(ar) httpDeliver.NewArticleHttpHandler(e, au) e.Start(config.GetString("server.address")) }
امیدوارم این مقاله براتون مفید بوده باشه. اگه خوشتون اومد، لایک یادتون نره! و اگر دوست داشتین نظرتونو این زیر بهم بگین.