معماری تمیز(Clean) در گولنگ

امروزه هر برنامه نویسی میدونه که اگر میخواد پروژه اش در آینده به بن بست نخوره، و بتونه به راحتی اون رو توسعه بده باید کدش منظم و قابل خواندن باشه و اگر هم بخواد در تیمی فعالیت بکنه همه ی اعضای تیم باید بر اساس یک قانون واحد کد نویسی کنن . اما بین این همه معماری و قانون و دستورالعمل های مختلف کدوم رو باید انتخاب کرد که هم درعین سادگی ، منظم و قدرتمند باشه ؟ پاسخ این سوال معماری تمیز هست.معماری تمیز توسط Robert C. Martin ( یا همون عمو باب معروف! ) توی کتاب :Clean Architecture: A Craftsman’s Guide to Software Structure and Designمعرفی شد.توی این مقاله میخوایم باهم مفاهیم این معماری توی یه اپلیکیشن گولنگی پیاده سازی کنیم.قوانین پایهقبل از شروع پیاده سازی معماری تمیز، ما یکسری قوانین کلی و بیسیک داریم که باید اپلیکیشنمون رو طبق این موارد طراحی کنیم :مستقل و بدون وابستگی به فریمورک، برنامه مون نباید زیاد به پکیج ها وفریمورک های خارجی وابسته باشه و صرفا باید به عنوان ابزار از این لایبرری ها استفاده کنیم.قابل تست، لاجیک برنامه باید به تنهایی و بدون UI ، دیتابیس و وب سرور قابل تست باشه.مستقل از UI، رابطه کاربری برنامه باید به راحتی و بدون وابستگی به خود اپلیکیشن قابل تغیر باشه.مستقل از دیتابیس، برنامه شما نباید به دیتابیس خاصی وابسته باشه و باید بتونید دیتابیس برنامه رو مثلا از mysql به مونگو تغیر بدین.مستقل از هر چیز خارجی، برنامه شما باید از هرچیز خارجی مستقل باشه درواقع برنامه شما هیچ چیزی بیرون از خودش رو نمیبینه.اگر دوست دارین بیشتر راجب موارد بالا بخونین میتونین این مقاله رو ببینین.نکته : تمام لایه های برنامه ما باید بدون وابستگی قابل تست باشن.Clean Architectureخب عمو باب اومده 4 تا لایه توی معماری خودش آورده:Entities ( موجودیت های برنامه )Usecase ( یوز کیس ها )Controller ( کنترولر ها )Framework & Driver ( فریمورک و لایبرری ها )ما هم توی برنامه خودمون 4 تا لایه پیاده سازی میکنیم:Models Repository Usecase Delivery که در ادامه درمورد هرکدوم توضیح خواهم داد.مدل ها ( Models )مدل ها مرکزی ترین لایه داخل برنامه مون هستن و توی تمام لایه ها استفاده میشن، این لایه تمام موجودیت های برنامه رو شامل میشه مثل: Article, Student, Book.import &quottime&quot type Article struct { ID int64 `json:&quotid&quot` Title string `json:&quottitle&quot` Content string `json:&quotcontent&quot` UpdatedAt time.Time `json:&quotupdated_at&quot` CreatedAt time.Time `json:&quotcreated_at&quot` } در واقع هر شیِ و آبجکتی اینجا ذخیره میشه.ریپازیتوری ( Repository )تمام عملیات های مربوط به دیتابیس CRUD( ساخت، مشاهده، ویرایش و حذف)، اینجا انجام میشه و هیچ منطقی از برنامه توی این لایه اتفاق نمیفته. صرفا این لایه برای اتصال به دیتابیس کاربرد داره.نکته : این لایه وابسته به دیتابیس هستش.یوزکیس ( Usecase )تمام پردازش های بیزینسی توی این لایه انتفاق میفته، وظیفه این لایه محاسبه و پردازش داده ها برای ارائه به لایه Delivery هستش.و بعد این لایه با توجه به داده هایی که از لایه 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(&quotFetch&quot, mock.AnythingOfType(&quotstring&quot), mock.AnythingOfType(&quotint64&quot)).Return(mockListArtilce, nil) u := ucase.NewArticleUsecase(mockArticleRepo) num := int64(1) cursor := &quot12&quot 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, &quotFetch&quot, mock.AnythingOfType(&quotstring&quot), mock.AnythingOfType(&quotint64&quot)) }لایه تحویل:با توجه به نیاز این لایه به لایه مرکزی ترش یعنی یوز کیس برای تست این لایه باید لایه یوز کیس رو طبق اینترفیس تعریف شده ماک کنیم و به این لایه پاس یدیم.مثلا: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(&quotService RUN on DEBUG mode&quot) } } 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(&quot%s:%s@tcp(%s:%s)/%s&quot, dbUser, dbPass, dbHost, dbPort, dbName) val := url.Values{} val.Add(&quotparseTime&quot, &quot1&quot) val.Add(&quotloc&quot, &quotAsia/Tehran&quot) dsn := fmt.Sprintf(&quot%s?%s&quot, connection, val.Encode()) dbConn, err := sql.Open(`mysql`, dsn) if err != nil && config.GetBool(&quotdebug&quot) { 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(&quotserver.address&quot)) } امیدوارم این مقاله براتون مفید بوده باشه. اگه خوشتون اومد، لایک یادتون نره! و اگر دوست داشتین نظرتونو این زیر بهم بگین.