What is SOLID?
SOLID là một số các nguyên tắc thiết kế phần mềm được Uncle Bob sáng tạo. Mục tiêu chính của các nguyên tắc này là nhằm giúp cho code rõ ràng, dễ hiểu hơn từ đó trở nên dễ dàng mở rộng và bảo trì.
Đối với hầu hết các developer thì SOLID giống như viên kim cương vầy, nghe nói đến thì rất nhiều nhưng chưa bao giờ được nhìn và cầm nắm =))). Hầu hết thì chỉ biết các lí thuyết cơ bản được truyền dạy từ thời đại học, một số thì chỉ đọc lý thuyết từ các nguồn trên mạng mà chưa bao giờ đươc áp dụng thực tế vì nhiều lí do…. Điều này cũng dễ hiểu thôi, vì chính tác giả Uncle Bob cũng đã thừa nhận là ông chưa bao giờ áp dụng hoàn chỉnh SOLID trong các project của ông mà (yaoming).
Để có thể áp dụng được SOLID vào dự án thực tế không hề dễ dàng, nó yêu cầu tất cả các thành viên trong team có kĩ năng code tương đối “cứng”, phải “thấm nhuần” tư tưởng SOLID và đặc biệt là cùng chung một lí tưởng, một mục tiêu của SOLID.
SOLID cũng có mặt trái, nó khiến code của bạn dài hơn, luồng xử lý loằng ngoằng hơn, có thể gây khó khăn khi debug. Tuy nhiên nhìn về tổng thể, lợi ích mà SOLID đem lại lớn hơn rất nhiều so với mặt trái của nó, do đó nó mãi mãi tồn tại vững chãi và đi sâu vào mind-set của các lập trình viên. Trong bài post này mình sẽ cố gắng trình bài SOLID trong lập trình iOS, hi vọng phần nào đó sẽ giúp các bạn áp dụng được các nguyên lí này vào project.
S.O.L.I.D
SOLID là tập hợp 5 nguyên tắc sau đây:
- [S]ingle Responsibility Principle (SRP): Nguyên lý đơn chức năng
- [O]pen Close Principle (OCP): Nguyên lý mở rộng và che giấu.
- [L]iskov Subsitution Principle (LSP): Nguyên lý thay thế Liskov
- [I]nterface Segregation Principle (ISP): Nguyên lý phân tách các “Interface”
- [D]ependancy Inversion Principle (DIP): Nguyên lý đảo ngược “Dependancy”
#1 Single Responsibility Principle (SRP):
Chữ S trong nguyên lý có nội dung như sau:
A class should have only one reason to change.
Single là một, Responsibility là trách nhiệm, A class should have only one reason to change là mỗi class chỉ nên có một lí do để thay đổi —> cái tên và nguyên lý khá là hại não =))).
Nhưng tóm lại, nguyên tắc “S” này nghĩa là:
Một class chỉ nên giữ 1 trách nhiệm duy nhất (nhớ là duy nhất nhé :v)
Cùng theo dõi ví dụ sau:1
2
3
4
5
6class AirConditioner {
func turnOn() { }
func turnOff() { }
func changeMode() { }
func changeSpeed() { }
}
Đoạn code trên mô tả về các chức năng của máy điều hoà, và class AirConditioner đã vi phạm nguyên lý SRP vì chứa qúa nhiều chức năng.
Để không bị vi phạm SRP thì ta dùng protocol để tách biệt các chức năng như sau:1
2
3
4
5
6
7
8
9
10
11
12
13
14protocol SwitchOption {
func turnOn()
func turnOff()
}
class Switch: SwitchOption {
func turnOn() {
print("turnOn")
}
func turnOff() {
print("turnOff")
}
}
1 | protocol ModeOption { |
1 | protocol FanSpeedOption { |
Sau khi tách biệt các chức năng thì class AirConditioner đã đảm bảo được các quy tắc của nguyên lý SRP:
1 | class AirConditioner: SwitchOption, ModeOption, FanSpeedOption { |
#2 Open Close Principle (OCP):
Chữ “O” trong nguyên lý có nội dung như sau:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
Wtf open rồi còn close =))) . Nhưng để ý kĩ thì sẽ dễ dàng thấy nội dung có 2 ý riêng biệt open for extension và closed for modification .
- open for extension - closed for modification : khi thiết kế các (classes, modules, functions… thì phải đảm bảo chúng có khả năng mở rộng, có nghĩa là người khác có thể sử dụng lại các chức năng mà bạn đã làm, đồng thời có thể thêm một số chức năng của họ mà không làm ảnh hưởng, thay đổi đến các chức năng trước đó.
Xem lại ví dụ ở nguyên lý Single Responsibility Principle ở phần trên. Giả sử, người khác muốn thêm một chức năng mới là chỉnh độ ẩm của máy lạnh. Chúng ta có thể làm việc này dễ dàng bằng cách thêm function changeHumidity vào class AirConditioner, tuy nhiên sẽ làm thay đổi phần code cũ và vi phạm nguyên tắc OCP. Để đảm bảo nguyên lý OCP thì ta có thể dùng extensions như sau:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16protocol Humidable {
func changeHumidity(_ value: Int)
}
class Humidity: Humidable {
func changeHumidity(_ value: Int) {
print("changeHumidity \(value)")
}
}
extension AirConditioner: Humidable {
func changeHumidity(_ value: Int) {
let humadity = Humidity()
humadity.changeHumidity(value)
}
}
Như trên ta có thể thêm mới một chức năng mà không làm ảnh hưởng đến code cũ.
#3 Liskov substitution principle (LSP):
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
Dịch ra là : “Trong một chương trình, các object của class con có thể thay thế class cha mà không làm thay đổi tính đúng đắn của chương trình.” => Nói dễ cho dễ hiểu là : các phương thức của class cha thì phải hoạt động được và đúng trên các class con kế thừa từ chúng.
1 | class Bird { |
Kiểm tra đoạn code trên, class Bird là class cha đại diện cho các loại chim, trong class Bird có func fly đại diện cho hành vi “bay” của các loại chim. class Eagle(đại bàng) kế thừa từ class Bird và có nó có khả năng thực hiện hành vi bay như lớp cha, mọi thứ đến đây đều ổn. Bây giờ mình tạo thêm một class Penguin(chim cánh cụt) vì cũng là một loài chim nên Penguin vẫn kế thừa từ class Bird -> có khả năng “bay”. =>> Đệt mợ chim cánh cụt méo biết bay => làm thay đổi tính đúng đắn của chương trinhg => vi phạm nguyên lý Liskov substitution principle.
#4 Interface segregation principle (ISP):
The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.
Tạm dịch: Clients không nên bị phụ thuộc vào những “Interfaces” mà nó không sử dụng. => Thay vì dùng 1 interface lớn, ta nên tách thành nhiều interface nhỏ, với nhiều mục đích cụ thể.
Trong coding khi implement một interface thì cần xem xét các phương thức để chia nhỏ, không nên để nảy sinh các phương thức không cần dùng đến. Cùng xem xét ví dụ sau:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19protocol Animal {
func eat()
func sleep()
func drink()
}
class Dog: Animal {
func eat() {
print("eat")
}
func sleep() {
print("sleep")
}
func drink() {
print("drink")
}
}
Khi ta muốn thêm mới 1 động vật có một hoặc nhiều tính năng mới (như bay, bơi…) thì ta phải thêm vào interface => làm cho interface phình to ra. Và khi một loai động vật khác kế thừa interface này, nó phải implement cả những tính năng không sử dụng tới:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38protocol Animal {
func eat()
...
func swim()
func fly()
}
class Fish: Animal {
func eat() {
print("eat")
}
...
func swim() {
print("swim")
}
func fly() {
fatalError("đệt mợ tao méo biêt bay =))")
}
}
class Bird: Animal {
func eat() {
print("eat")
}
...
func swim() {
fatalError("đệt mợ tao méo biêt bới =))")
}
func fly() {
print("fly")
}
}
Giải pháp với tình huống trên là ta sẽ tách nhở interface Animal ra như sau:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37protocol Animal {
func eat()
func sleep()
func drink()
}
protocol CanSwim {
func swim()
}
protocol CanFly {
func fly()
}
class Fish: Animal, CanSwim {
func eat() {
print("eat")
}
...
func swim() {
print("swim")
}
}
class Bird: Animal, CanFly {
func eat() {
print("eat")
}
...
func fly() {
print("fly")
}
}
#5 Dependancy Inversion Principle (DIP):
Trước tiên thì bạn phải hiểu được Dependancy là gì? => Dependency là một thuật ngữ phổ biến trong ngành lập trình, biểu thị qua hệ phụ thuộc giữa 2 đối tượng A và B (class, module, function..), khi A được sử dụng trên B hoặc ngược lại.
ví dụ: khi một class AManager được sử dụng trong ViewController A, thì ta nói AManager là dependancy của ViewController A và ngược lại.1
2
3
4
5
6
7
8
9
10
11
12class AManager {
func hello() { }
}
class ViewControllerA: UIViewController {
var a = AManager()
override func viewDidLoad() {
super.viewDidLoad()
a.hello()
}
}
Dependancy Inversion Principle được gọi là nguyên lý đảo ngược Dependency được phát biểu như sau:
- High-level modules should not depend on low-level modules. Both should depend on abstractions. (Các modules cấp cao không nên phụ thuộc vào các module cấp thấp, cả 2 nên phụ thuộc vào abstraction.)
- Abstractions should not depend on details. Details should depend on abstractions.(Abstraction không nên phụ thuộc vào details, ngược lại, detail nên phụ thuộc vào abstraction.)
Theo cách code thông thường, các module cấp cao sẽ gọi các module cấp thấp. Module cấp cao sẽ phụ thuộc và module cấp thấp, điều đó tạo ra các dependency. Khi module cấp thấp thay đổi, module cấp cao phải thay đổi theo. Một thay đổi sẽ kéo theo hàng loạt thay đổi, giảm khả năng bảo trì của code.
Nếu tuân theo Dependendy Inversion principle, các module cùng phụ thuộc vào 1 interface không đổi. Ta có thể dễ dàng thay thế, sửa đổi module cấp thấp mà không ảnh hưởng gì tới module cấp cao.
1 | class Car { |
Trong khởi tạo của class Driver chúng ta tạo ra một cái xe và gán nó cho người lái xe => class Driver ràng buộc chặt chẽ với class Car. Người lái xe có thể lái nhiều loại xe khác nhau, nếu thay thế class Car bằng class khác, ứng dụng sẽ không chạy. Chúng ta tạo ra một định nghĩa trìu tượng (abstract class) hoặc một interface1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27protocol Car {
func run()
}
class BMW: Car {
func run() {
print("BMW go go !!!")
}
}
class Mazda: Car {
func run() {
print("Mazda go go !!!")
}
}
class Driver {
let car: Car
init(car: Car) {
self.car = car
}
func drive() {
car.run()
}
}
Như vậy, những người lái xe và những cái xe ô tô đều phụ thuộc vào interface Car, thứ hai là các class cụ thể như BMV, Mazda phụ thuộc vào interface Car, do đó nếu có bất kỳ loại xe nào mà thực hiện theo định nghĩa trừu tượng Car thì người lái xe lái được tất.