Bạn có biết rằng trên thế giới có một cộng đồng anti if, else. Trên đường đi tìm đường cách mệnh, coi như cũng tạo chút thử thách cho bản thân, mình đã tìm ra một số phương pháp tương đối hữu ích.

1. Thủ thuật refactor

Các phương pháp này có thể coi như là trick để code trông gọn hơn, đồng thời loại bỏ if.

Dictionary

Đây là phương pháp mình sử dụng từ lâu. Với phương pháp này thì ta có thể refactor hàm sau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func getEpisode(_ id: String) -> Episode? {
    if id = "E1" {
    	return episode_1
    } else if id = "E2" {
    	return episode_2
    } else if {
    	...
    }

    ...
}

Thành dạng như sau

1
2
3
4
5
6
7
let dict = ["E1": episode_1,
            "E2": episode_2,
            "E3": episode_3]

func getEpisode(_ id: String) -> Episode? {
    return dict[id]
}

Enum

Nên sử dụng enum với những cái nào có chung format như ở trường hợp sau

Thay vì

1
2
3
4
5
6
7
func play(type: Format) {
    switch type {
    	case .video: self.playVideo()
    	case .audio: self.playAudio()
    	...
    }
}

Tên các method ở trên đều có chung format play**%TYPE%**. Ta có thể gọi nó thông qua *selector* để loại bỏ hẳn *if*

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
enum Format: String {
    case video
    case audio
    case image
}

class Player: NSObject {
    func play(type: Format) {
        let selector = Selector.init("play\(type.rawValue.capitalized)")
        self.perform(selector)
    }

    ...
}

// Result
Player().play(type: .video)

Để sử dụng selector, các phương thức cần gắn thêm tiền tố @objc và class cần kế thừa NSObject.

2. Sử dụng functional programming

Filter

Đây là phương thức trong bộ tam filter, map, reduce ra đời kèm ngay ở phiên bản đầu của swift. Param truyền vào filter có dạng tổng quát như sau:

(T) -> Bool // hàm truyền vào 1 giá trị và return bool

Như vậy ta có thể truyền vào tên hàm thay vì định nghĩa ra nó.

Giả sử bài toán ta đang giải quyết là tìm số nguyên dương trong mảng, thay vì viết:

1
2
3
arr.filter { (value) -> Bool in
    return value > 0
}

Ta có thể truyền tên hàm vào trong filter để code ngắn gọn, rõ nghĩa hơn.

1
2
3
4
5
6
7
// (Int) -> Bool
func positive(x: Int) -> Bool {
	return x > 0
}

arr.filter(positive)

Nếu có nhiều hơn một điều kiện, ta có thể tổng hợp lại các hàm làm một. Giả sử ta muốn tìm số vừa lớn hơn 0 vừa nhỏ hơn 100, ta viết thêm điều kiện:

1
2
3
func lessThan100(x: Int) -> Bool {
    return x < 100
}

Tiếp theo, ta sử dụng phương pháp composite để có 1 hàm dạng như: lessThan100AndPositive = lessThan100 & positive. Lúc đó code sẽ gọn gàng ngăn nắp như này:

arr.filter(lessThan100AndPositive)

Nhưng trước hết, ta sẽ định nghĩa hai phương thức helper để tổng hợp điều kiện thuận tiện hơn và dễ dàng tái sử dụng lại code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class func allPass<A>(_ array: [(A) -> Bool], value: A) -> Bool {
    let predicate = array.map({ V.bind(value: value, to: $0) }) // [() -> Bool]
    return checkAll(predicate)
}

public class func allPass<A>(_ array: [(A) -> Bool]) -> (A) -> Bool {
    return { v in
        let predicate = array.map({ V.bind(value: v, to: $0) }) // [() -> Bool]
        return checkAll(predicate)
    }
}

Mình tạo 2 phương thức đều có tên allPass, phương thức đầu trả về Bool, còn phương thức thứ hai có dạng curry function (hàm nhận vào 1 param và trả ra 1 hàm khác). Cả 2 hàm khác nhau về cách thức tạo nhưng cùng chung 1 mục đích sử dụng (tạm thời không bàn đến bindcheckAll)

Với 2 phương thức nêu trên ta có thể composite 2 điều kiện positivelessThan100 theo cách như sau

allPass([positive,lessThan100]) // (Int) -> Bool

Và hàm filter sẽ được viết ngắn gọn như sau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func doFilterAll(_ value: Int) -> Bool {
    // open comments below to use curry function
    // let and = V.allPass([positive,lessThan100]) // (Int) -> Bool
    // return and(value)
    
    // using function return bool
    return V.allPass([positive,lessThan100], value: value)
}

arr.filter(doFilterAll)

Làm tương tự, ta có thể tạo hàm Or để tìm giá trị thỏa mãn 1 điều kiện trong tất cả các điều kiện được cung cấp.

Bạn thấy đó, với functional ta dễ dàng tách các đoạn kiểm tra điều kiện ra thành hàm riêng, dễ dàng tổng hợp chúng, đồng thời dễ test và tái sử dụng.

Để hiểu rõ hơn về hàm bindcheckAll, mình có đưa nó vào trong code ví dụ ở cuối bài viết.

Refinement type

Thế nào là refinement type

refinement type = value + predicates

Như định nghĩa trên, refinement type sẽ có 2 thành phần:

+ Giá trị
+ Điều kiện

Hai thành phần này có mối liên hệ chặt chẽ, còn liên kết thế nào thì có thể xem đoạn code ngay sau đây.

1
2
3
4
Positive<Double>.of(100.0)?.value // 100
Positive<Int>.of(-1)?.value // nil
Both<Positive<Float>,LessThan100<Float>>.of(99)?.value // 99
Both<Positive<Float>,LessThan100<Float>>.of(101)?.value // nil

Như bạn thấy đã thấy, 100 thỏa mãn điều kiện lớn hơn 0; 99 thỏa mãn điều kiện nhỏ hơn 100 nên ta có thể sử dụng. Trong trường hợp không thỏa mãn (-1,101), giá trị nil được trả về.

Thậm chí ta chưa dùng tí if nào :D

Đoạn code định nghĩa refinement type như sau

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public protocol Refinement {
    associatedtype RefinedType
    static func pass(_ value: RefinedType) -> Bool
}

public struct Refined<A,R: Refinement> where A == R.RefinedType {
    public let value: A
    public init?(_ value: A) {
        guard R.pass(value) else { return nil }
        self.value = value
    }
}

public extension Refinement {
    static func of(_ value: RefinedType) -> Refined<RefinedType,Self>? {
        return Refined(value)
    }
}

Sử dụng sơ đồ khối để dễ hình dung

img

Code ví dụ source

3. Mở rộng

Ngoài những cách trên thì còn rất nhiều cách để giảm thiểu if else:

  • Sử dụng đa hình
  • Tách hàm
  • switch case :v
  • Sử dụng hàm cond (như trong Lisp)
  • Blah blah

Tham khảo