Kiến trúc ứng dụng: Nên bắt đầu như thế nào ? - Phần 1

- 14 mins

Cứ học code là phải biết về Kiến Trúc Ứng Dụng (KTUD) ?!?

Nhớ lại năm 2015, 2016 lúc đó mình có thành lập một cộng đồng các iOS Developers (iOS Tek Talk) nho nhỏ ở Sài Gòn. Mỗi tuần là mỗi chủ đề quan trọng và hay gặp trong iOS. Lúc đó các bạn vote chủ đề kiến trúc ứng dụng, cụ thể là MVVMVIPER rất nhiều. Nhưng tới buổi chia sẻ thì luôn bắt đầu là sự hào hứng tột cùng và sau 15p chỉ còn là những gương mặt sắp gục xuống vì buồn ngủ.

Thật ra những hôm đó mình rất buồn vì nghĩ bản thân công lực chưa đủ nên đã diễn đạt vấn đề không đủ tốt và dễ hiểu cho mọi người. Vì thế mình quyết tâm đào sâu lĩnh vực này, một mặt giúp bản thân củng cố kiến thức, mặt khác là giúp cho những bạn đang có cùng mối quan tâm tham khảo.

Quan trọng hơn là chính vào lúc đó mình nhận ra một điều rất quan trọng nữa là hầu hết những bạn hỏi về KTUD đều thiếu kiến thức nền tảng. Đây là những “nấc thang” cực kỳ quan trọng để có thể bước vào thế giới của những người làm về kiến trúc.

Trong khi đó cũng rất nhiều các bạn trẻ mới vừa học lập trình đã muốn học ngay vào KTUD. Đây là một tin đáng mừng vì chứng tỏ các bạn hiểu tầm quan trọng của KTUD, tuy nhiên cũng đáng lo vì KTUD không phải là kiến thức thông thường, hay nói đúng hơn nó như một triết lý cần có thời gian để thẩm thấu, nghiền ngẫm rồi mới giác ngộ được. Thật đáng tiếc không ít bạn đã vì chướng ngại vật này mà bỏ cuộc.

Vì thế nếu bạn là người mới học lập trình, đừng quá bận tâm vì mình đang chưa hiểu được nó. Mình cũng có chia sẻ về việc này thông qua bài viết: Kiến trúc ứng ụng tại sao lại khó như vậy.

Trong bài viết này mình sẽ chia sẻ lại một hướng tiếp cận KTUD từ cơ bản nhất cho các bạn mới bắt đầu tìm hiểu Kiến Trúc Ứng Dụng.

Code phải có thể nhìn vào là biết nó sẽ chạy thế nào

Mình giả sử các bạn khi đọc bài này đã biết về các syntax trong lập trình cũng như các quy ước để có format code đẹp. Thế như format đẹp là chưa đủ vì nó chỉ giúp ta dễ nhìn chứ không đủ để giúp ta dễ hiểu.

Tại sao lại cần điều này, không phải ta chỉ cần máy tính hiểu là được sao ?!

Đúng. Lập trình thì quan trọng là cho máy hiểu và chạy được không lỗi là mừng rồi. Tuy nhiên các bạn nên luyện tập tư duy code có thể chạy được trong đầu của ta trước khi run thử và đoán mò. Khi chúng ta có thể hiểu code, ta có thể dự đoán được kết quả rồi từ đó chạy ứng dụng chỉ để kiểm tra lại. Điều này cũng giúp các bạn sau này tiếp cận với unit test tốt hơn.

Để làm được việc này có nhiều cách nhưng chung quy tập trung vào cách đặt tên là nhiều. Đặt tên thông thường dùng cho tên biến (variable/property)tên hàm(function/method).

// Swift Code

func multiply(number1:Int, number2: Int) -> Int {
  return number1 * number2
}

// khi gọi hàm sẽ là
let result = multiply(number1: 5, number2: 2)

// Có vẻ chưa hay lắm, chúng ta có thể đặt tên tham số lại như sau
func multiply(number:Int, by number2: Int) -> Int {
  return number1 * number2
}

// bây giờ khi gọi sẽ là
let result = multiply(number: 5, by: 2)

// Cách gọi hàm thế này sẽ gần với ngôn ngữ người: "multiply number 5 by 2"

Sử dụng hàm một cách hiệu quả

Hàm là cách để chúng ta có thể phân chia một logic lớn, phức tạp, thành nhiều logic nhỏ hơn. Cách chia để trị này nhằm giúp chúng ta đặt tên cho các logic cũng như chia sẻ logic tốt hơn cho nhiều công việc cần đến chúng.

Tuy nhiên để việc chia sẻ được dễ dàng thì logic của chúng phải thật cụ thể, đúng với tên hàm, và quan trọng là càng ít lệ thuộc vào context càng tốt. Context ở đây chính là object chứa chúng hoặc tệ hơn là trong thân hàm có gọi tới các biến của context. Lúc này ta sẽ quay lại khái niệm hàm trong trường lớp mà các thầy cô hay dặn là: hàm sẽ có các tham số và trả về kết quả. Vì thế nếu được chúng ta nên bám sát vào điều này.

Ví dụ trường hợp logic trong LoginViewController sau:

  class LoginViewController: UIViewController {
    
    @IBOutlet weak var usernameTextField: UITextField!
    @IBOutlet weak var passwordTextField:UITextField!
    
    override func viewDidLoad() {
      super.viewDidLoad()
        
      usernameTextField.backgroundColor = UIColor.gray
      usernameTextField.layer.borderColor = UIColor.white.cgColor
      usernameTextField.layer.borderWidth = 1
      usernameTextField.layer.cornerRadius = 10
      
      passwordTextField.backgroundColor = UIColor.gray
      passwordTextField.layer.borderColor = UIColor.white.cgColor
      passwordTextField.layer.borderWidth = 1
      passwordTextField.layer.cornerRadius = 10
    }

    // ...
  }

Đoạn code trên cũng rất tự nhiên thôi, ta dùng viewDidLoad để cấu hình thêm cho 2 text field. Cả 2 là text field, có cấu hình giao diện như nhau nên thường được các bạn copy và paste lại “cho lẹ”. Điều này khiến hình thành một thói quen không tốt, lâu dần chúng ta thành copy-and-paste-er hơn là một developer thật thụ. Ta có thể viết lại như sau:

class LoginViewController: UIViewController {
    
  @IBOutlet weak var usernameTextField: UITextField!
  @IBOutlet weak var passwordTextField:UITextField!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    self.setBorderAndBackgroundColor(for: usernameTextField)
    self.setBorderAndBackgroundColor(for: passwordTextField)
  }
  
  func setBorderAndBackgroundColor(for view:UIView) {
    view.backgroundColor = UIColor.gray
    view.layer.borderColor = UIColor.white.cgColor
    view.layer.borderWidth = 1
    view.layer.cornerRadius = 10
  }
}

Hàm setBorderAndBackgroundColor sẽ giúp ta làm việc này tốt hơn, chống code lặp lại (duplicate) và chúng cũng độc lập với context bên ngoài, nó chỉ tác động vào tham số mà thôi.

Bây giờ tới hàm xử lý việc login. Sẽ không ít những bạn mới viết code như sau:

  // ...
  func login() {
    let username = usernameTextField.text
    let password = passwordTextField.text
    
    // login logic ...
    // if login success {
    //    To do: login success (push to some viewcontroller)
    // } else {
    //    To do: login fail (present error)
    // }
  }
  // ...

Nhiều bạn viết tới đây rất happy vì các bạn đã biết đưa logic login vào hàm và gọi tới nó. Tuy nhiên các bạn quên mất vài điều:

Cả 2 điều trên dẫn tới một hệ lụy rất tệ trong tương lai: Khi bạn cần dùng lại hàm này bạn sẽ copy và thay lại 2 biến trên ở một context khác. Hơn nữa logic login thành công và thất bại cũng phải được edit lại tùy mục đích. Vậy sẽ khác gì ta viết lại hàm login này.

Vì thế ở đây ta chỉ cần đảm bảo logic login thì chỉ nên có login mà thôi. Hàm này chỉ nên quan tâm tới usernamepassword chứ không quan tâm control cung cấp chúng. Việc login thành công hay không ta sẽ dùng kết quả return của hàm:

typealias LoginToken = String
    
func login(username:String, password:String) -> LoginToken? {
  // login logic
  // if login success {
  //    get login token and return
  // } else {
  //    return nil
  // }
}

Bây giờ hàm login chỉ là đúng nhiệm vụ của nó. Ở đây mình muốn minh họa thêm rằng thông thường các API Login sẽ trả về Token nếu thành công. Đây là 1 String, tuy nhiên ta cũng có thể đặt tên lại cho kiểu dữ liệu này để làm rõ String trả về này là cái gì. Như vậy vai trò của việc đặt tên vẫn là rất hữu ích.

Dữ liệu phải luôn được model hóa

Đây là kỹ năng tiếp theo mà các bạn cần có. Mình tin rằng với những bạn xuất thân từ các trường ĐH/CĐ ngành công nghệ thông tin đều đã có được học qua (môn “Cấu trúc dữ liệu và giải thuật” và các môn OOP). Tuy nhiên từ học tốt chuyển qua ứng dụng tốt lại có một khoảng cách rất xa.

Vậy “model hóa” là cái quái gì ?! Đó là phương pháp phối hợp các kiểu dữ liệu nguyên thủy của ngôn ngữ lập trình cho sẵn để tạo ra các kiểu dữ liệu mới. Ôi nghe hàn lâm quá. Thật ra chúng đơn giản lắm !

Chúng ta biết rằng muốn lập trình app bán Sách (Book) thì ta cần lập 1 số model chính như là:

Đúng rồi đó các model cần thiết để ứng dụng hoạt động. Nhưng bên cạnh đó ta cũng có thể lập thêm các model/đối tượng khác nữa ví dụ như:

Điều đó có nghĩa là chúng ta xem thông số hiển thị trên giao diện cho các model chính cũng là những cấu trúc ta có thể định ra được. Khi cần hiển thị model chính ta pass luôn cả model view tương ứng của nó qua nữa, từ đó việc thiết lập sẽ dễ dàng hơn rất nhiều. Nếu các bạn thấy nó quen quen thì đây chính là một phần của kiến trúc MVVM.

Có một cách khác để minh họa cho phần code login trên, ta có thể lập ra một cấu trúc dữ liệu cho username và password:

struct LoginForm {
  let username:String
  let password:String
  
  var errorString:String?
  
  init(username:String, password:String) {
    self.username = username
    self.password = password
  }
  
  mutating func validate() -> Bool {
    if username.characters.count == 0 {
        self.errorString = "Username is required."
        return false
    }
    
    if password.characters.count < 6 {
        self.errorString = "Password must be greater than 6 characters."
        return false
    }
    
    return true
  }
}

// Hàm login có thể pass vào struct LoginForm thay vì là username và password
func login(formData:LoginForm) -> LoginToken? {

Trong một struct/class chúng sẽ có thể chứa cả các methods, chúng ta nên tận dụng điều này để đặt vào các logic cần thiết. Trong ví dụ trên mình đã thêm vào một method validate để validate tất cả data trong form (đây chỉ là ví dụ, không phải best practice).

Trên thực tế khi chúng ta đã quen với việc model hóa này, chúng ta có thể model hóa cả tất cả dữ liệutrạng thái của App, thường được gọi là App State.

  struct AppState {
    let currentUserLogged:User?
    let books:[Book]
    let categories:[Category]
    
    let currentTabIndex:Int = 0
    let currentUserLocation:CLLocationCoordinate2D?
    // ...
  }

App State Changing là một trong những vấn đề mà các kỹ sư đang tập trung giải quyết. Vì thế để hiểu được KTUD đang vận hành để giúp gì cho ta, hoặc ngồi chung bàn để giải quyết vấn đề với họ thì ta phải dùng và trải nghiệm qua các problem liên quan tới nó. Và đương nhiên bước đầu tiên là chúng ta phải biết tới sự tồn tại của App State.

Khoan đã, như vậy KTUD ngoài việc giúp tổ chức source code tốt hơn, dễ maintain và tái sử dụng thì nó cũng như một công cụ để giúp mình quản lý App State tốt hơn ??? Phải. Nói một cách đơn giản thì đó là những gì KTUD mang lại.

Chào mừng các bạn đã đến “bãi giữ xe” của thế giới Kiến Trúc Ứng Dụng.

Còn tiếp …

Viet Tran

Viet Tran

A Man who is developing apps with Red Bull

comments powered by Disqus
rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora