Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 2)

by cuongpt
422 views

Table of contents

  • Magic number & Duplicate code
  • Code Organization
  • Scene Delegate
  • Computed Property
  • Optional
  • Multi-line String
  • Bonus

Magic number & Duplicate code

  • Khi code ta không nên dùng những con số vô định, hay còn gọi là magic number, gây khó hiểu cho người khác. Điều này sẽ ảnh hưởng đến quá trình maintain sau này. Ta có thể thay thế những con số magic này bằng cách tạo ra các constant với tên gọi clear nhất có thể, làm sao để khi người khác đọc code của bạn, họ cũng có thể hiểu được vì sao bạn lại dùng đến con số đó. Hoặc ít nhất trước khi dùng magic number, ta phải thêm comment để giải thích lý do tại sao sử dụng chúng.
  • Ta cũng không nên để những đoạn code giống nhau được lặp đi lặp lại trong source code của mình. Nếu nhận thấy có những đoạn code cùng thực hiện một chức năng nhất định, hoặc cùng được apply cho 1 đối tượng nhất định, ta có thể nghĩ đến việc grouping chúng lại thành các function để tiện cho việc implement cũng như maintain sau này.

Not Preferred

class SignUpViewController: UIViewController {

    @IBOutlet private weak var firstNameTextField: UITextField!
    @IBOutlet private weak var lastNameTextField: UITextField!
    @IBOutlet private weak var accountTextField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Có thể thấy đoạn code này đang set borderWidth, borderColor, cornerRadius cho
        // lần lượt 3 TextField khác nhau. Tưởng tượng nếu sau này màn hình được update
        // thêm nhiều textfield khác nữa, hoặc trong app cũng có nhiều textfield cần phải 
        // setup các thuộc tính tương tự như trên. Mỗi textfield cần 3 dòng code x số lượng
        // textfield cả app = ...
        // -> Ta nghĩ đến việc tạo ra các function common để dùng chung cho các đối tượng 
        // UITextField, sẽ thuận tiện hơn cho việc implement và maintain sau này, sửa 1 hàm 
        // có thể apply được toàn bộ
        firstNameTextField.layer.borderColor = UIColor.orange.cgColor
        firstNameTextField.layer.borderWidth = 1.0
        firstNameTextField.layer.cornerRadius = 25
        
        lastNameTextField.layer.borderColor = UIColor.orange.cgColor
        lastNameTextField.layer.borderWidth = 1.0
        lastNameTextField.layer.cornerRadius = 25
        
        accountTextField.layer.borderColor = UIColor.orange.cgColor
        accountTextField.layer.borderWidth = 1.0
        accountTextField.layer.cornerRadius = 25
    }
}

Preferred

class SignUpViewController: UIViewController {

   @IBOutlet private weak var firstNameTextField: UITextField!
   @IBOutlet private weak var lastNameTextField: UITextField!
   @IBOutlet  weak var accountTextField: UITextField!
   
   override func viewDidLoad() {
       super.viewDidLoad()
       
       firstNameTextField.setupLayer()
       lastNameTextField.setupLayer()
       accountTextField.setupLayer()
   }
}
// Phần extension cho các compoment như UILabel, UITextField, UIButton, ... thường 
// được tách ra thành các file riêng. Xem thêm phần Code Organization
extension UITextField {
   func setupLayer(borderWidth: CGFloat = 1.0,
                   borderColor: CGColor = UIColor.orange.cgColor,
                   cornerRadius: CGFloat = 25) {
       layer.borderWidth = borderWidth
       layer.borderColor = borderColor
       layer.cornerRadius = cornerRadius
   }
}

Code Organization

  • Ta không nên gộp tất cả các property và các function vào trong một block code ở trong một file. Vì đối với những file có số lượng dòng code lớn, việc làm này sẽ khiến ta khó hình dung được cấu trúc tổ chức của file và mục đích sử dụng của các function trong file đó. Ta nên chia nhỏ file thành nhiều block code, mỗi block code giải quyết một nhiệm vụ khác nhau và chứa các function liên quan tới nhiệm vụ đó. Ta có thể thêm từ khoá // MARK: – dosomething vào trên mỗi block code

Not Preferred

// 1 block code không nên adopt quá nhiều protocol, delegate như vậy
class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    @IBOutlet private weak var salaryTableView: UITableView!
    @IBOutlet private weak var avatarImageView: UIImageView!
    @IBOutlet private weak var widthConstraintAvatarImageView: NSLayoutConstraint!
    
    private var dataSalaryArray: [Salary] = [Salary(income: 123456, month: "1/2021"),
                                            Salary(income: 654321, month: "2/2021")]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        salaryTableView.delegate = self
        salaryTableView.dataSource = self
        salaryTableView.register(UINib(nibName: "SalaryCell", bundle: nil), forCellReuseIdentifier: "SalaryCell")
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSalaryArray.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell: SalaryCell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                         for: indexPath) as? SalaryCell else {
            return UITableViewCell()
        }
        cell.setupData(data: dataSalaryArray[indexPath.row])
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
    }

    func loadData() {
        // Do something
    }
}

Preferred

class HomeViewController: UIViewController {

   // MARK: - Outlet
   @IBOutlet private weak var salaryTableView: UITableView!
   @IBOutlet private weak var avatarImageView: UIImageView!
   @IBOutlet private weak var widthConstraintAvatarImageView: NSLayoutConstraint!
   
   // MARK: - Property
   private var dataSalaryArray: [Salary] = [Salary(income: 123456, month: "1/2021"),
                                           Salary(income: 654321, month: "2/2021")]
   
   override func viewDidLoad() {
       super.viewDidLoad()
       salaryTableView.delegate = self
       salaryTableView.dataSource = self
       salaryTableView.register(UINib(nibName: "SalaryCell", bundle: nil), forCellReuseIdentifier: "SalaryCell")
   }
}

// MARK: - TableView DataSource
extension HomeViewController: UITableViewDataSource {
   func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
       return dataSalaryArray.count
   }
   
   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       guard let cell: SalaryCell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                        for: indexPath) as? SalaryCell else {
           return UITableViewCell()
       }
       cell.setupData(data: dataSalaryArray[indexPath.row])
       return cell
   }
}

// MARK: - TableView Delegate
extension HomeViewController: UITableViewDelegate {
   func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
       
   }
}

// MARK: - Do something
extension HomeViewController {
   func loadData() {
       // Do something
   }
}
  • Khi thêm từ khoá // MARK: – dosomething vào trước các block code, Xcode sẽ tự gen cho ta đường line phân cách giữa các block đó. Thêm vào đó, khi tap vào thanh công cụ như trên ảnh, ta cũng có thể overview được trong file đang có những block code nào, các block đảm nhận nhiệm vụ gì, các function liên quan đến nhiệm vụ đó. Việc này rất có ích khi file của bạn có số lượng dòng code lớn và có nhiều người cùng phát triển

  • Tương tự, ta cũng nên chia project thành nhiều các folder, mỗi folder có chức năng riêng và chứa các file liên quan đến chức năng đó.
Preferred Not Preferred

Scene Delegate

  • Khi tạo một project mới trong Xcode, hệ thống sẽ mặc định cho rằng app của bạn sẽ chỉ support cho version iOS cao nhất mà bản Xcode đó support trở lên (ví dụ: Xcode 11.3 sẽ support cho version iOS 13.6 trở lên, Xcode 12.0 sẽ support cho version iOS 14.0 trở lên, Xcode 12.5 là iOS 14.5 trở lên, …).
  • Từ Xcode 11.0 trở về trước, khi tạo mới 1 project, sẽ có 1 vài file mặc định được tạo như: ViewController.swift, Main.storyboard, AppDelegate.swift, Info.plist… Từ Xcode 11.0 trở lên, để phục vụ cho iOS 13, ngoài những file mặc định vừa liệt kê ở trên, còn có thêm 1 file nữa là SceneDelegate.swift (Tìm hiểu thêm tại đây).

  • Vì vậy, nếu muốn project có thể được build trên device (hoặc simulator) chạy version < iOS 13, ta sẽ phải tiến hành xoá file SceneDelegate.swift và thực hiện 1 vài config như sau.
  • Ngoại trừ trường hợp là bạn muốn làm việc với SwiftUI, việc xoá file SceneDelegate.swift và giảm target version build của app dường như là 1 việc bắt buộc bởi 1 vài lý do chính sau:
    • Khi phát triển 1 ứng dụng, chắc chắn chúng ta đều muốn đông đảo người dùng có thể tiếp cận được với ứng dụng đó. Nhưng không phải người dùng nào cũng sẵn sàng update version iOS mới nhất cho device của họ. Vì vậy việc để target vesion build của app ở mức “phổ thông” như iOS 11.0, 12.0, … sẽ làm tăng tính thương mại cho app của bạn.
    • Về mặt technical, khi build app trên những device “thấp” hoặc trên những iOS version thấp, ta sẽ có cơ hội để test nhiều hơn. Vì thực tế sẽ có rất nhiều bug rất “dị”, chúng chỉ xảy ra trên những “môi trường” thấp mà không xảy ra ở trên “môi trường” cao, hoặc cũng có nhiều trường hợp ngược lại. Đối với 1 developer, ta cần phải đảm bảo những dòng code của ta phải chạy ngon trên nhiều môi trường khác nhau.
    • Cũng về mặt technical, khi bạn muốn chia sẻ source code của mình và người khác muốn clone về. Trong trường hợp bạn dùng bản Xcode mới nhất còn họ dùng bản Xcode thấp hơn, nếu bạn không giảm target version build thì người khác sẽ không thể build source của bạn được.

Computed Property

  • Để cho ngắn gọn, nếu 1 computed property thuộc kiểu read-only, ta có thể bỏ qua mệnh đề get. Mệnh đề này chỉ cần thêm vào khi có thêm cả mệnh đề set

Preferred

var diameter: Double {
  return radius * 2
}

Not Preferred

var diameter: Double {
  get {
    return radius * 2
  }
}

Optional

  • Khi truy cập giá trị optional, nếu giá trị đó chỉ được truy cập 1 lần hoặc có nhiều optional trong chuỗi, ta có thể dùng 1 chuỗi optional liên tiếp
textContainer?.textLabel?.setNeedsDisplay()
  • Trong trường hợp giá trị optional được truy cập nhiều lần, nên sử dụng if…let để mở ra 1 block code rồi thao tác trong đó
if let textContainer = textContainer {
  // do many things with textContainer
}
  • Khi đặt tên cho các biến và các property optinal, không cần thiết phải đặt kiểu như optionalNameLabel hay couldAvatarImageView vì trạng thái optional (?) đã có trong khi khai báo biến rồi.
  • Khi unwrapp biến optional, cũng không cần thiết phải đặt các tên như unwrappedView hay realLabel mà hãy dùng chính tên gốc của biến đó

Preferred

var subview: UIView?
var volume: Double?

// later on...
if let subview = subview, let volume = volume {
  // do something with unwrapped subview and volume
}

// another example
resource.request().onComplete { [weak self] response in
  guard let self = self else { return }
  let model = self.updateModel(response)
  self.updateUI(model)
}

Not Preferred

var optionalSubview: UIView?
var volume: Double?

if let unwrappedSubview = optionalSubview {
  if let realVolume = volume {
    // do something with unwrappedSubview and realVolume
  }
}

// another example
UIView.animate(withDuration: 2.0) { [weak self] in
  guard let strongSelf = self else { return }
  strongSelf.alpha = 1.0
}
  • Nếu có nhiều biến optional được unwrapp với guard let hoặc if…let, hãy ghép chúng lại với nhau thành 1 câu lệnh để giảm thiếu việc lồng điều kiện. Khi ghép, hãy đặt guard trên 1 dòng riêng, đặt các điều kiện trên từng dòng riêng và thụt lề cho chúng, cuối cùng mệnh đề else được căn lề thẳng với guard

Preferred

guard 
  let number1 = number1,
  let number2 = number2,
  let number3 = number3 
else {
  fatalError("impossible")
}
// do something with numbers

Not Preferred

if let number1 = number1 {
  if let number2 = number2 {
    if let number3 = number3 {
      // do something with numbers
    } else {
      fatalError("impossible")
    }
  } else {
    fatalError("impossible")
  }
} else {
  fatalError("impossible")
}

Multi-line String

  • Khi muốn viết 1 văn bản dài nhiều dòng, nên sử dụng cú pháp Multi-line String Literal. Đó là mở văn trên 1 dòng rồi bắt đầu văn bản từ dòng thứ 2 trở đi, kết hợp với việc thụt lè và căn lề thằng các dòng tiêp theo

Preferred

let message: String = """
  You cannot charge the flux \
  capacitor with a 9V battery.
  You must use a super-charger \
  which costs 10 credits. You currently \
  have \(credits) credits available.
  """

Not Preferred

let message: String = """You cannot charge the flux \
  capacitor with a 9V battery.
  You must use a super-charger \
  which costs 10 credits. You currently \
  have \(credits) credits available.
  """

Bonus

  • Không giống như 1 số ngôn ngữ khác (C/C++, JavaScript, …), Swift không yêu cầu phải có dấu chấm phảy ở cuối mỗi dòng code

Preferred

let swift = "not a scripting language"

Not Preferred

let swift = "not a scripting language";
  • Dấu ngoặc đơn bao quanh các điều kiện là không bắt buộc và nên được bỏ qua

Preferred

if name == "Hello" {
  print("World")
}

Not Preferred

if (name == "Hello") {
  print("World")
}
  • Tuy nhiên với những biểu thức phức tạp, có dấu ngoặc đơn bao quanh sẽ làm code trở nên clear hơn

Preferred

let playerMark: String = (player == current) ? "X" : "O"

  • Ngoài ra, còn 1 lỗi nữa mà người viết thấy đa phần các bạn newbie rất hay mắc phải. Tuy rằng lỗi này liên quan đến logic nhưng cũng xin liệt kê vào đây để anh em newbie tiện theo dõi. Đó là khi ta muốn truy xuất các phần tử trong 1 collection data (set, dictionary, array, … và ở đây xin lấy ví dụ là array), việc đầu tiên ta cần làm là phải check xem array đó có phần tử hay không rồi mới tiến hành việc truy xuất. Dưới đây là 1 ví dụ:

Preferred

private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {<br />    if PlistManager().getPlist(withName: name)?.count == 0 {<br />        return nil<br />    }<br />    let data = PlistManager().getPlist(withName: name)?[0]<br />    return data<br />}

Chúng ta có thể viết clear hơn như sau (dĩ nhiên đây là cách được recommend):

Preferred

private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {<br />    if let data = PlistManager().getPlist(withName: name)?.first {<br />        return data<br />    }<br />}

Tuyệt đối không được truy xuất trực tiếp vào các phần tử của array khi chưa check điều kiện, vì ta không thể chắc chắn được là array đó luôn có phần tử hay là không. Điều này sẽ tiềm ẩn nguy cơ bug rất cao.

Not Preferred

private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {<br />    let data = PlistManager().getPlist(withName: name)?[0]<br />    return data<br />}

Reference

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

You may also like