はじめに
前回は、TDDの概要と、簡単なinitをサンプルに、
レッド(テストを書く) > グリーン (プロダクトコードを書く) > リファクタリング(コードを整理する)
まで行いました。引き続き、TDDを行なっていきます。
開発環境
- Xcode 10.3
- Swift
- XCTest
作るもの
ポーカー
現在のTODOリスト
[TODOリスト]
- Cardを定義して、インスタンスを作成する
- CardはSuitを持つ
- CardはRankを持つ
- Cardのインスタンスから文字列表記(notation)を取得する
それでは、最下段のTODOから再開していきましょう。
実装
まずは、レッドからです。TDDPokerBySwiftTests.swiftに以下を追加します。
class TDDPokerBySwiftTests: XCTestCase {
(省略)
func testCardNotation() {
let card = Card(suit: .heart, rank: .three)
XCTAssertEqual(card.notation, "3♥")
}
}
Cardにnotationプロパティが存在していないため、当然エラーになります。これはテストエラーではなく、単純にコンパイルエラーです。
それでは、早速、Cardにnotationプロパティを追加して、テストをグリーンにしていきましょう。
仮実装
Card.swiftに以下を追加します。
struct Card {
(省略)
var notation: String {
return "3♥"
}
}
Command + U でテストを実行してみましょう。
テストが成功すると思います。このnotationプロパティは、initで代入されるrankとsuitから生成されるものなので、外から代入された値を保持するStored Propertyではなく、Computed Propertyによる実装になっています。
Stored Property (保持型プロパティ)
今回で言う所の、Cardクラスのsuitとrankがそうですね。willSet、didSetなどで監視もでき、lazy修飾詞も使用可能です。
let suit: Suit
let rank: Rank
Computed Property (計算型プロパティ)
それ自体は値を保持せず、setter/getterで値を計算し(set)または返す(get)プロパティです。上記notationの定義がそれになるのですが、上記は、getのみのreadonly定義になっており、get{}が省略されています。省略しない場合には、以下のようになります。
var notation: String {
get {
return "3♥"
}
}
setterを記述する場合には、
var notation: String {
get {
return "3♥"
}
set(t) {
print("セット!\(t)");
}
}
や、
var notation: String {
get {
return "3♥"
}
set {
print("セット!\(newValue)");
}
}
で、notationの値がセットされた時に、setが呼ばれます。set{}内では、Stored Propertyを含めた計算なども可能です。
具体的な使い方の例です。
class Person {
var birthday: NSDate // 生年月日
var age: Int { // 年齢
get {
let components = self.calendar.components(.CalendarUnitYear, fromDate: birthday, toDate: NSDate.date(), options: .allZeros)
return components.year
}
set(newAge) {
let diff = self.age - newAge
if diff != 0 {
self.birthday = self.calendar.dateByAddingUnit(.CalendarUnitYear, value: diff, toDate: self.birthday, options: .allZeros)
}
}
}
let dateFormatter: NSDateFormatter // 日付フォーマッタ
let calendar: NSCalendar // カレンダー
// イニシャライザ
init(birthday: String) {
self.dateFormatter = NSDateFormatter();
self.dateFormatter.dateFormat = "Y/M/d"
self.calendar = NSCalendar.currentCalendar()
self.birthday = self.dateFormatter.dateFromString(birthday)!
}
}
let p = Person(birthday: "1980/12/31")
print(p.age)
p.age = p.age - 2
print(p.birthday) // 1982/12/31
三角測量
話を戻します。現在、notationは"3♥"を静的に返しているだけなので、一般化が保証されてません。テストを修正して、別のケースも追加します。
class TDDPokerBySwiftTests: XCTestCase {
(省略)
func testCardNotation() {
let card1 = Card(suit: .heart, rank: .three)
XCTAssertEqual(card1.notation, "3♥")
let card2 = Card(suit: .spade, rank: .jack)
XCTAssertEqual(card2.notation, "J♠")
}
}
このように2つ以上のテストケースを用意することで、機能の一般化を導き出すテクニックを「三角測量」と言います。テストがグリーンになるように、Card.swiftを書き換えます。enumをStringで定義し直し、各caseに文字列を代入します。取り出す際には、rawValueで取り出せます。
struct Card {
enum Suit: String {
case spade = "♠"
case heart = "♥"
case club = "♣"
case diamond = "♦"
}
enum Rank: String {
case ace = "A"
case two = "2"
case three = "3"
case four = "4"
case five = "5"
case six = "6"
case seven = "7"
case eight = "8"
case nine = "9"
case ten = "10"
case jack = "J"
case queen = "Q"
case king = "K"
}
enum CardType {
case other
}
let suit: Suit
let rank: Rank
var notation: String {
return rank.rawValue + suit.rawValue
}
}
Command + U でテストを実行しましょう。テストが成功すれば、グリーンのステップを終えたことになるので、リファクタリングを行います。今回も、cardの変数をテストケースごとに初期化して、共通利用できるようにするだけです。
class TDDPokerBySwiftTests: XCTestCase {
(省略)
func testCardNotation() {
var card: Card
card = Card(suit: .heart, rank: .three)
XCTAssertEqual(card.notation, "3♥")
card = Card(suit: .spade, rank: .jack)
XCTAssertEqual(card.notation, "J♠")
}
}
まとめ
今回は、
- 仮実装
- 三角測量
について学びました。
次回は、「明白な実装」というものについて、学んで行きたいと思います。
参考
プロパティ
https://tea-leaves.jp/swift/content/%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3