+29

Golang Design Patterns - Prototype. Khởi tạo đối tượng một cách nhanh chóng

Mẫu pattern cuối cùng thuộc Creational Pattern trong series Design Patterns mà mình muốn giới thiệu. Cũng xoay quanh việc khởi tạo đối tượng, nhưng không phức tạp như các mẫu thiết kế cùng loại, ta tận dụng lại những đối tượng đã có sẵn để phục vụ cho việc khởi tạo. Chi tiết về mẫu thiết kế này mình sẽ nói sau đây.

I. Prototype - Creational Pattern

Mục tiêu của Prototype hướng tới những đối tượng đã được khởi tạo trước đó (như tại thời điểm compile) và cho phép chúng ta clone đối tượng đó không giới hạn số lần vào thời điểm runtime chẳng hạn. Lấy ví dụ khi build một trang web gồm 3 thành phần header-body-footer, sẽ có rất nhiều trang chỉ cần thay đổi nội dung ở phần body, và phần header và footer là như nhau. Khi đó prototype sẽ giúp chúng ta tiết kiệm chi phí và thời gian cho việc khởi tạo lại header và footer.

II. Prototype mang lại cho developers những gì?

Tránh lặp đi lặp lại việc khởi tạo không đáng có, đó là những gì mà mẫu thiết kế này mang lại. Thử tưởng tượng header của trang chứa rất nhiều thông tin lấy từ API, việc chuyển trang đồng nghĩa phải đi fetch lại toàn bộ thông tin thì quả là dư thừa không đáng có. Một cách tóm tắt, Prototype hướng đến:

  • Những đối tượng thường được tái sử dụng có chi phí khởi tạo cao và (có thể), cung cấp default value cho chúng
  • Giảm tiêu tốn tài nguyên cho việc khởi tạo phức tạp (CPU, resources...)

III. Ví dụ thực tế

Chúng ta lấy luôn ví dụ ở trên để triển khai nhé. Bài toán đặt ra là 1 trang web bao gồm 3 routes:

  • /login (không header và footer)
  • /home (có header và footer)
  • /profile (có header và footer) Dễ nhận thấy ở đây có 2 dạng layout Blank Và Main (mình hay đặt tên vậy 😂). Blank thì đơn thuần là một layout chỉ chứa content (body). Main là một layout chứa 3 phần header - body (dynamic) - footer.

Mẫu Prototype áp dụng vào trường hợp này cần tuân thủ những quy tắc sau:

  • Có 2 layouts (Blank và Main), layout luôn yêu cầu tham số truyền vào là body (content của trang)
  • Khi yêu cầu chuyển trang trong cùng 1 layout (home -> profile), đối tượng trang mới được tạo ra nhưng header và footer là những instance được tận dụng lại
  • Một Page Info sẽ bao gồm các thông tin của page là route, header, body, footer (mặc định là nil)

IV. Implementation

Khởi tạo struct Layout như sau, layout bao gồm 3 thành phần (header, body, footer). Một Layout bao gồm các methods SetBody, GetInfo và 2 layouts được phép clone là BlankMain

package prototype

import "fmt"

type Layout struct {
	Header *string
	Body   string
	Footer *string
}

const (
	BLANK_LAYOUT = "BLANK"
	MAIN_LAYOUT  = "MAIN"
)

func (s *Layout) SetBody(body string) {
	s.Body = body
}

func (s *Layout) GetInfo() string {
	header, footer := "empty", "empty"
	if s.Header != nil {
		header = *s.Header
	}
	if s.Footer != nil {
		footer = *s.Footer
	}
	return fmt.Sprintf("Layout: Header: %s, Body: %s, Footer: %s", header, s.Body, footer)
}

  • Blank Layout bao gồm các thành phần từ Layout và một instance mặc định (header và footer nil)
package prototype

type BlankLayout struct {
	Layout
}

var BlankLayoutIns *Layout = &Layout{
	Header: nil,
	Body:   "Blank Body",
	Footer: nil,
}

  • Với Main Layout thì phức tạp hơn, để khởi tạo header của Main Layout phải trả qua một vài công đoạn như fetch data từ API, xử lý logic...và việc khởi tạo mất tầm 1s:
package prototype

import "fmt"

type MainLayout struct {
	Layout
}

var MainLayoutIns *Layout = &Layout{
	Header: getHeader(),
	Body:   "Main Body",
	Footer: getFooter(),
}

func getHeader() *string {
	fmt.Println("Getting header data, it took 1 second...")
	header := "Main Header"
	return &header
}

func getFooter() *string {
	footer := "Main Footer"
	return &footer
}

  • Sau cùng là định nghĩa struct cho Page, bao gồm route, layout và các method NewPage và GetInfo. Ở đây NewPage nhận vào tham số là route và pageLayout, ở đây mình đưa công việc clone layout vào:
package prototype

import (
	"errors"
	"fmt"
)

type Page struct {
	Route string
	Layout
}

func NewPage(route, layout string) *Page {
	layoutClone, err := cloneLayout(layout)
	if err != nil {
		panic(err)
	}
	return &Page{
		Route:  route,
		Layout: *layoutClone,
	}
}

func cloneLayout(layout string) (*Layout, error) {
	switch layout {
	case BLANK_LAYOUT:
		newLayout := *BlankLayoutIns
		return &newLayout, nil
	case MAIN_LAYOUT:
		newLayout := *MainLayoutIns
		return &newLayout, nil
	default:
		return nil, errors.New("Layout not found")
	}
}

func (s *Page) GetInfo() string {
	return fmt.Sprintf("Page route: %s, %s", s.Route, s.Layout.GetInfo())
}

  • Bây giờ mình sẽ run đoạn code theo yêu cầu bài toán, bao gồm 3 route: /home, /profile/login. Đảm bảo layout instance không bị ảnh hưởng trong quá trình clone:
/*
    Example Prototype
*/
fmt.Println("*** Example Prototype ***")
homePage := prototype.NewPage("/home", prototype.MAIN_LAYOUT)
homePage.SetBody("Home Body")
fmt.Println(homePage.GetInfo())

profilePage := prototype.NewPage("/profile", prototype.MAIN_LAYOUT)
fmt.Println(profilePage.GetInfo())
profilePage.SetBody("Profile Body")
fmt.Println(profilePage.GetInfo())

loginPage := prototype.NewPage("/login", prototype.BLANK_LAYOUT)
fmt.Println(loginPage.GetInfo())
fmt.Print("*** End of Prototype ***\n\n\n")
  • Kết quả:

image.png

V. Lời kết

Prototype design pattern được xem như một công cụ hỗ trợ mạnh mẽ cho việc khởi tạo nhanh những đối tượng mà tiết kiệm được rất nhiều thời gian và tài nguyên, cũng có thể xem prototype như một build caches. Có thể thấy được sự trùng lặp trong cách triển khai giữa các mẫu creational design patterns, nhưng vẫn có những sự khác biệt nhỏ khiến các pattern này thể hiện sức mạnh nếu được sử dụng đúng cách.

Mình cũng giới thiệu qua toàn bộ 5 mẫu thiết kế thuộc nhóm khởi tạo thường được developers sử dụng nhất. Hi vọng gặp các bạn ở các chapers khác

Cảm ơn các bạn đã xem bài viết 😄

VI. References

Go Design Patterns (Mario Castro Contreras)


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí