Design Pattern: Builder pattern trong iOS
Nghe đến Design Pattern, chắc hẳn mỗi lập trình viên đều biết đến kỹ thuật quan trọng này và đã từng áp dụng nó ít nhất một lần. Design pattern giúp bạn giải quyết vấn đề một cách tối ưu nhất, cung cấp cho bạn các giải pháp trong lập trình hướng đối tượng (OOP). Phần lớn các ngôn ngữ lập trình đều có thể áp dụng Design pattern và Swift cũng không ngoại lệ!
Design pattern có 3 nhóm chính:
- Creational Pattern bao gồm: Abstract Factory, Factory Method, Singleton, Builder, Prototype.
- Structural Pattern bao gồm: Adapter, Bridge, Composite, Decorator, Facade, Proxy và Flyweight.
- Behavioral Pattern bao gồm: Interpreter, Template Method, Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy và Visitor.
Hôm nay, chúng ta sẽ cùng tìm hiểu một design pattern trong nhóm Creational Pattern (Nhóm khởi tạo) là Builder pattern.
Nội dung
- Builder pattern là gì?
- Vấn đề
- Sử dụng nó như thế nào
- Áp dụng trong iOS
- Tổng kết
Builder pattern là gì?
Trong OOP, Builder pattern là một loại pattern thuộc nhóm Creational pattern (Tạo dựng), vì vậy nó sẽ giúp chúng ta giải quyết các vấn đề liên quan tới tạo dựng một object bằng cách:
- Chia nhỏ các hàm khởi tạo của một object.
- Đặt cấu hình mặc định và logic xử lý liên quan tới việc khởi tạo một object từ trong class ra bên ngoài và đẩy vào Builder class.
Và việc chia nhỏ và đẩy vào Builder class như thế nào thì chúng ta hãy cùng tìm hiểu tiếp về vấn đề và áp dụng Builder Pattern trong một bài toán.
Vấn đề
Giả sử, chúng ta có một chương trình đặt bánh pizza, và mục tiêu của chúng ta là tạo ra chương trình đặt bánh để lấy yêu cầu từ khách hàng.
Chúng ta có đối tượng Pizza, vả khởi tạo các đối tượng Pizza:
class Pizza {
var smell: String
var base: String
var size: String
var isChiliSauce: Bool
var isKetchup: Bool
init(smell: String, base: String, size: String, isChiliSauce: Bool, isKetchup: Bool) {
self.smell = smell
self.base = base
self.size = size
self.isChiliSauce = isChiliSauce
self.isKetchup = isKetchup
}
}
let sizeM = Pizza(smell: "Bò", base: "Mỏng", size: "M", isChiliSauce: true, isKetchup: true)
let sizeMChicken = Pizza(smell: "Gà", base: "Mỏng", size: "M", isChiliSauce: true, isKetchup: true)
let sizeL = Pizza(smell: "Gà", base: "Dày", size: "L", isChiliSauce: true, isKetchup: true)
Chúng ta có thể thấy hàm tạo khá nhiều thuộc tính, và có những thuộc tính mặc định ít thay đổi. Vì những thuộc tính đó ít thay đổi nên sẽ không tránh khỏi việc duplicate code. Do đó, người ta sử dụng Builder Pattern.
Sử dụng nó như thế nào
Class diagram
Cùng phân tích một chút nhé:
- Director: Đại diện cho module cần kết quả từ việc khởi tạo Pizza.
- PizzaBuilder: Một class implement InterfaceBuilder, có nhiệm vụ khởi tạo ra Pizza, thông qua hàm build().
- Pizza: Là object cần khởi tạo.
Demo
Một protocol nhằm khai báo các phương thức
protocol PizzaBuilderProtocol {
func build() -> Pizza
func setSmell(_ smell: String) -> PizzaBuilder
func setBase(_ base: String) -> PizzaBuilder
func setSize(_ size: String) -> PizzaBuilder
func withChiliSauce(_ isChiliSauce: Bool) -> PizzaBuilder
func withKetchup(_ isKetchup: Bool) -> PizzaBuilder
}
PizzaBuilder triển khai các phương thức đã khai báo ở PizzaBuilderProtocol
class PizzaBuilder: PizzaBuilderProtocol {
private var pizza = Pizza()
func build() -> Pizza {
return self.pizza
}
func setSmell(_ smell: String) -> PizzaBuilder {
pizza.smell = smell
return self
}
func setBase(_ base: String) -> PizzaBuilder {
pizza.base = base
return self
}
func setSize(_ size: String) -> PizzaBuilder {
pizza.size = size
return self
}
func withChiliSauce(_ isChiliSauce: Bool) -> PizzaBuilder {
pizza.isChiliSauce = isChiliSauce
return self
}
func withKetchup(_ isKetchup: Bool) -> PizzaBuilder {
pizza.isKetchup = isKetchup
return self
}
}
Cart ở đây giữ vai trò tương đương Director trong hình bên trên.
class Cart {
let orderId: String
let products: [Pizza]
init(orderId: String, products: [Pizza]) {
self.orderId = orderId
self.products = products
}
}
let pizza1 = PizzaBuilder()
.setSize("s")
.setSmell("Gà")
.setBase("Dày")
.build()
let cart1 = Cart(orderId: "OrderId1", products: [pizza1])
Cách thực hiện thật đơn giản phải không? Bản chất của nó là chia nhỏ các properties trong Object ra thành các hàm getter, setter trong Builder và sau đó thực hiện set giá trị cho chúng và khởi tạo Object qua hàm build(). Và đó chính là cách viết theo Builder Pattern.
Chắc hẳn khi các bạn đọc đến đây sẽ có một suy nghĩ rằng: Vậy tại sao không dùng default value? Đúng vậy, khi Builder Pattern được giới thiệu, ngôn ngữ họ sử dụng khi đó là C++. Nhưng, Swift của chúng ta đã cung cấp tính năng default value, và chúng ta có thể hoàn toàn loại bỏ Builder. Nói như vậy thì Builder Pattern sẽ trở nên vô dụng trong Swift?
Chắc chắn là không. Những tài liệu trên mạng về Builder Pattern khá nhiều, và nó đang hướng người đọc vào vấn đề default value trong các ngôn ngữ như: C++, Java…Chúng ta hãy cùng nhìn lại về những thứ mà Builder làm, đó là giảm thiệu sự phức tạp khi khởi tạo thông qua hàm builder(). Bản chất chính là nó đang đóng gói việc khởi tạo một Object. Khi đó, chúng ta sẽ có thể xử lý nhiều hiện object, do something… trước khi trả về một Object hoàn chỉnh.
Áp dụng trong iOS
Chúng ta có thể áp dụng Builder Pattern ở rất nhiều trường hợp trong lập trình iOS. Mình, mình áp dụng trong việc customize một style cho Label, Button…
Thường khi, chúng ta customize một Label theo một style nào đó, chúng ta sẽ thường khởi tạo như thế này, nhìn có vẻ khá rối và không được thú vị cho lắm.
let attrString = NSMutableAttributedString(string: title)
attrString.addAttributes([NSMutableAttributedString.Key.foregroundColor: UIColor.red],
range: attrString.mutableString.range(of: title))
attrString.addAttributes([NSMutableAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 13)],
range: attrString.mutableString.range(of: title))
attrString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 2,
range: NSMakeRange(0, attrString.length))
titleLabel.attributedText = attrString
Chúng ta có thể áp dụng Builder pattern trong trường hợp này để nhìn code trở lên dễ đọc hơn và cũng dễ dàng phát triển trong tương lai.
Tạo một builder và protocol
protocol AttributedStringBuilderProtocol {
func build() -> NSMutableAttributedString
func setFont(_ font: UIFont, forSubString subString: String) -> AttributedStringBuilder
func setColor(_ color: UIColor, forSubString subString: String) -> AttributedStringBuilder
func withStrikeThrough() -> AttributedStringBuilder
}
class AttributedStringBuilder: AttributedStringBuilderProtocol {
private var attrString: NSMutableAttributedString
init(string: String) {
self.attrString = NSMutableAttributedString(string: string)
}
func build() -> NSMutableAttributedString {
return self.attrString
}
func setFont(_ font: UIFont, forSubString subString: String) -> AttributedStringBuilder {
self.attrString.addAttributes([NSMutableAttributedString.Key.font: font],
range: self.attrString.mutableString.range(of: subString))
return self
}
func setColor(_ color: UIColor, forSubString subString: String) -> AttributedStringBuilder {
self.attrString.addAttributes([NSMutableAttributedString.Key.foregroundColor: color],
range: self.attrString.mutableString.range(of: subString))
return self
}
func withStrikeThrough() -> AttributedStringBuilder {
self.attrString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 2,
range: NSMakeRange(0, self.attrString.length))
return self
}
}
Sử dụng
let atrString = AttributedStringBuilder(string: title)
.setFont(.boldSystemFont(ofSize: 13), forSubString: title)
.setColor(.red, forSubString: title)
.withStrikeThrough()
.build()
titleLabel.attributedText = attrString
Như vậy, với trường hợp trên, việc sử dụng Builder Pattern sẽ giúp những dòng code của chúng ta trở nên dễ nhìn hơn và thuận lợi trong việc phát triển tiếp những properties của nó.
Tổng kết
Tóm lại, Design pattern: Builder Pattern là một trong những pattern dễ thực hiện. Nhưng khi sử dụng pattern này chúng ta cần phải mất thời gian để xác định được trường hợp nào cần nó, nó có thể trở nên rườm rà hơn khi sử dụng trong Swift. Nhưng chúng ta hãy đọc, tìm hiểu để áp dụng những tư duy và ý tưởng của một pattern vào một bài toán phù hợp. Đầu tư thời gian vào việc chuẩn bị xây dựng một sản phẩm sẽ luôn sẽ giúp việc bảo trì code trở nên dễ dàng hơn.