Factory pattern là pattern thường được sử dụng trong ngôn ngữ lập trình hướng đối tượng. Trong Go có rất nhiều cách khác nhau trong đó bạn có thể dùng Factory Pattern để làm cho code của bạn trở nên sạch hơn và ngắn gọn hơn.
Go không có class nhưng chúng ta có thể sử dụng struct. Ví dụ struct của Person
, cùng với phương thức Greet
sẽ như sau
type Person struct { Name string Age int } func (p Person) Greet() { fmt.Printf("Hi! My name is %s", p.Name) }
Factory đơn giản
Cách đơn giản và phổ biến nhất thường được sử dụng là một hàm nhận các số tham số đầu vào và trả về instance Person
func NewPerson(name string, age int) Person { return Person{ Name: name, Age: age } }
Chúng ta cũng có thể trả về con trỏ Person
như sau
func NewPerson(name string, age int) *Person { return &Person{ Name: name, Age: age } }
Các hàm Factory thay thế tốt hơn cho việc khởi tạo các instance sử dụng như p := &Person{}
, bởi vì ký hiệu của hàm đảm bảo rằng tất cả mọi người sẽ cung cấp các thuộc tính yêu cầu. Ví dụ, một người có thể dễ dàng quên việc khởi tạo thuộc tính Age
khi sử dụng struct để khởi tạo. Ký hiệu hàm là NewPerson(name string, age int)
đảm bảo rằng cả name
và age
được cung cấp để có thể tạo một Person
mới.
Interface factory
Hàm Factory có thể trả về một interface thay vì một struct. Interface cho phép định nghĩa các hành vi (behavior) mà không cần đưa ra các thực thi bên trong. Điều này nghĩa là chúng ta có thể tạo một private struct, trong khi chỉ để lộ interface (ở đây là các hành vi) ra bên ngoài.
type Person interface { Greet() } type person struct { name string age int } func (p person) Greet() { fmt.Printf("Hi! My name is %s", p.name) } // Ở đây, NewPerson trả về một interface, và không phải là chính person struct func NewPerson(name string, age int) Person { return person{ name: name, age: age } }
Do con trỏ cũng có thể thực thi các interface, chúng ta có thể trả về hàm giống nhau với con trỏ thay thế
func NewPerson(name string, age int) Person { return &person{ name: name, age: age } }
Nhiều phần thực thi
Bằng việc trả về các interface, chúng ta có thể có nhiều hàm Factory trả về các thực thi khác nhau của các interface. Một trường hợp sử dụng thực tế dùng để sinh ra các mock. Ví dụ, chúng ta có thể tạo một interface Doer
, http client thực thi và tạo một hàm Factory để trả về mock http client.
// Chúng ta định nghĩa Doer interface, có phương thức // `http.Client` struct `Do` type Doer interface { Do(req *http.Request) (*http.Response, error) } // sử dụng HTTP client thông thường từ package `net/http` func NewHTTPClient() Doer { return &http.Client{} } type mockHTTPClient struct{} func (*mockHTTPClient) Do(req *http.Request) (*http.Response, error) { // Phương thức `NewRecorder` của package httptest giúp chúng ta // sinh ra mock request mới res := httptest.NewRecorder() // Gọi phương thức `Result` cho phép chúng ta // tạo object rỗng mặc định *http.Response return res.Result(), nil } // cho phép tạo mock HTTP client, trả về // response rỗng cho bất kỳ request nào gửi đến nó func NewMockHTTPClient() Doer { return &mockHTTPClient{} }
Do NewHTTPClient
và NewMockHTTPClient
cả hai cùng trả về một kiểu, chúng có thể được sử dụng hoán đổi cho nhau, điều này thực sự hữu ích bạn muốn test code sử dụng Http client mà không cần tạo bất kỳ cuộc gọi HTTP ra bên ngoài.
Factory Generator
Facetory Generator tức là Factory của Factory. Mặc dù nghe có vẻ hơi phức tạp, nhưng Factory Generator là vô cùng hữu ích, khi bạn muốn khởi tạo các instance của các struct khác nhau hoặc các interface không loại trừ lẫn nhau, hoặc bạn muốn nhiều Factory với các giá trị mặc định khác nhau
Phương thức Factory
Chúng ta có thể sử dụng các phương thức của struct Factory Generator. Ví dụ, xem xét một animal factory
mà tạo các instance của một animal và cũng là nơi nuôi động vật.
type Animal struct { species string age int } type AnimalHouse struct { name string sizeInMeters int } type AnimalFactory struct { species string houseName string } // NewAnimal là một factory `Animal` cố định // species như một giá trị của `AnimalFactory` instance func (af AnimalFactory) NewAnimal(age int) { return Animal{ species: af.species, age: age, } } // NewHouse là một factory `AnimalHouse` cố định // name như một giá trị của `AnimalFactory` instance func (af AnimalFactory) NewHouse(sizeInMeters int) { return Animal{ name: af.houseName, sizeInMeters: sizeInMeters, } }
Tiếp theo, chúng ta có thể tạo Factory Dog
và Factory Horse
sử dụng Generator AnimalFactory
dogFactory := AnimalFactory{ species: "dog", houseName: "kennel" } dog := dogFactory.NewAnimal(2) kennel := dogFactory.NewHouse(3) horseFactory := AnimalFactory{ species: "horse", houseName: "stable" } horse := horseFactory.NewAnimal(12) stable := horseFactory.NewHouse(30)
Hàm Factory
Chúng ta có thể sử dụng hàm Factory để tạo ra các Factory với một vài mặc định được tích hợp sẵn. Ví dụ chúng ta muốn tạo một Baby
Factory và Teenager
Factory thay vì tạo một Person
Factory chung, chúng ta sử dụng hàm Generator
type Person struct { name string age int } func NewPersonFactory(age int) func(name string) Person { return func(name string) Person { return Person{ name: name, age: age, } } }
Chúng ta sử dụng hàm này để tạo các Factory với age
mặc định được tích hợp sẵn
newBaby := NewPersonFactory(1) baby := newBaby("john") newTeenager := NewPersonFactory(16) teen := newTeenager("jill")
Thuộc tính age
được trừu tượng hoá trong đóng gói NewPersonFactory
Khi nào không nên sử dụng Factory
Factory có thể sử dụng một cách lạm dụng. Nếu bạn đang khởi tạo một struct, giá trị mặc định của Go cũng phù hợp trong hầu hết các trường hợp, thì Factory không thực sự hữu ích. Giả sử nếu bạn muốn tạo một counter
type Counter struct { count int } func main(){ c := Counter{} }
Giá trị mặc định của c.count
là 0, về cơ bản là chấp nhận được. Tạo một hàm Factory sẽ không mang lại nhiều giá trị