Writing Better Code with Custom Subscripts in Swift
Hiện tại mình đang tìm hiểu về Swift thì thấy có khá nhiều nguồn, nhưng hầu hết bằng tiếng anh. Đây là nguồn tham khảo khá thú vị ngoài ebook Swift mà Apple đã cung cấp. Hôm nay mình xin đi dịch lại 1 bài viết liên quan tới Subscript, hy vọng có ích cho ai đó đọc =)) Bài viết dựa trên ngữ cảnh ...
Hiện tại mình đang tìm hiểu về Swift thì thấy có khá nhiều nguồn, nhưng hầu hết bằng tiếng anh. Đây là nguồn tham khảo khá thú vị ngoài ebook Swift mà Apple đã cung cấp. Hôm nay mình xin đi dịch lại 1 bài viết liên quan tới Subscript, hy vọng có ích cho ai đó đọc =))
Bài viết dựa trên ngữ cảnh Lập trình viên cần viết unit test cho 1 sản phẩm đã hoàn thiện được viết bằng Swift. Mục tiêu của Lập trình viên là custom Subscript để unit test trở nên dễ đọc và dễ kiểm soát hơn nhằm nâng cao chất lượng code.
Một trong những mục tiêu cuối cùng của việc kiểm thử là nâng cao chất lượng code. Bao gồm sử dụng subscripts trong Swift. Tôi làm sản phẩm game có tên Tic Tac Toe, và đây là đối tượng board.
// Board.swift /* The board is: 0,0 | 0,1 | 0,2 1,0 | 1,1 | 1,2 2,0 | 2,1 | 2,2 */ struct Board { let positions: [[Position]] init() { var positions = [[Position]]() for rowIndex in 0..<3 { var rowPositions = [Position]() guard let row = Position.Row(rawValue: rowIndex) else { fatalError("Row must be valid") } for columnIndex in 0..<3 { guard let column = Position.Column(rawValue: columnIndex) else { fatalError("Column must be valid") } let position = Position(row: row, column: column, state: .Empty) rowPositions.append(position) } positions.append(rowPositions) } self.positions = positions } }
Tôi thực hiện bước kiểm thử cho trường việc khởi tạo Board - nhằm chắc chắn rằng tất cả các vị trí thực sự rỗng, dưới đây là unit test:
// BoardTests.swift import XCTest @testable import SwiftTicTacToe class BoardTests: XCTestCase { func testBoardInitializationAllEmpty() { let board = Board() for row in 0..<3 { for column in 0..<3 { // This is ugly! let position = board.positions[row][column] XCTAssertEqual(position.state, Position.State.Empty) } } } }
Bằng cách này tôi có thể lấy vị trí trên bảng một cách khá thô thiển. Tôi muốn phải thực hiện lại việc đó như let position = board[row][column]. Thật may mắn có rất nhiều thứ có thể dễ dàng được thực hiện với subscript trong Swift
// BoardTests.swift struct Board { // note that my positions array can now be private // I want it to be accesses via the subscript only! private let positions: [[Position]] init() { // truncated, see above } // The Custom Subscript Function! subscript(row: Int) -> [Position] { get { return positions[row] } // if your array is mutable, you can also have a setter: // set { // positions[row] = newValue // } } }
Và ngay bây giờ tôi có thể dễ dàng truy cập vào vị trí một cách dễ dàng
// BoardTests.swift import XCTest @testable import SwiftTicTacToe class BoardTests: XCTestCase { func testBoardInitializationAllEmpty() { let board = Board() for row in 0..<3 { for column in 0..<3 { // Much more intuitive! let position = board[row][column] XCTAssertEqual(position.state, Position.State.Empty) } } } }
Bằng cách biến đổi subscript thì giờ đây việc nhìn vào phần kiểm thử có cảm giác trực quan hơn.
UPDATE
Giôgns như phần kiểm thử đã viết, tôi tin tưởng rằn cả row và column nên được gộp lại với nhau. Sau thất cả, tôi không thực sự muốn phải có 1 mảng column. Ý định của tôi với subscript là lấy được vị trí trên bảng. Vì vậy tôi cập nhật lại subscript thành:
// Board.swift subscript(row: Int, column: Int) -> Position { get { return positions[row][column] } }
Và unit test của tôi sẽ là:
// BoardTests.swift func testBoardInitializationAllEmpty() { let board = Board() for row in 0..<3 { for column in 0..<3 { // an even cleaner way to get the position! let position = board[row, column] XCTAssertEqual(position.state, Position.State.Empty) } } }
Nhưng đây chưa phải là tốt nhất. Trong những trường hợp kiểm thử khác, tôi cần lấy row và column từ vị trí danh sach của row và column:
// BoardTests.swift // in another test let row = Position.Row.Middle let column = Position.Column.Middle // this is ugly again :( let initialPosition = board[row.rawValue, column.rawValue]
Tôi thực sự gét phải sử dụng răValue cho những row và column - chúng thực sự nhìn khá khó chịu. Vì vậy tôi đã tạo subscript khác:
// Board.swift subscript(row: Position.Row, column: Position.Column) -> Position { get { return positions[row.rawValue][column.rawValue] } }
Bây giờ unit test của tôi trở lên sáng sủa hơn:
// BoardTests.swift // in another test let row = Position.Row.Middle let column = Position.Column.Middle // no more rawValue here let initialPosition = board[row, column]
Sự thật, tôi sẽ xoá bỏ subscript - cái mà lấy vị trí thông qua row và coloumn. Bằng việc sử dụng enum sẽ làm code của tôi nhanh hơn. Cuối cùng unit test của tôi trở thành:
import XCTest @testable import SwiftTicTacToe class BoardTests: XCTestCase { func testBoardInitializationAllEmpty() { let board = Board() for rowIndex in 0..<3 { guard let row = Position.Row(rawValue: rowIndex) else { XCTFail("Row must be valid!") return } for columnIndex in 0..<3 { guard let column = Position.Column(rawValue: columnIndex) else { XCTFail("Column must be valid!") return } let position = board[row, column] XCTAssertEqual(position.state, Position.State.Empty) } } } }