SwiftのwillSet didSetの使い分けについて考えてみた。

# Swift歴3ヶ月の初心者の初投稿
独学でSwiftを学習する中、覚えた知識をアウトプットにより血肉にしていきます。
アウトプットする事自体始めてで、アウトプットの練習も兼ねております。

# 今回作った課題
各辺の長さがInt型の直方体のクラスをリファクタリングしながら学びます。


# 目的
覚えた知識を実践に近い形でコーディングすることで使いこなせるようになること。

- **willSet**や**didSet**の有用性について
- コンピューティッドプロパティを利用した**Read Only Property**の有用性
- DRY原則違反の解消

# 条件：どんな直方体？

- Int型の縦横高さのプロパティを持っている
- 一辺の最大は６
- Int型の体積のプロパティを持っている
- 体積によって変わるenum型のサイズ（S,M,L）プロパティを持つ
- サイズの条件はS(1...8) M(9...64) L(65...216)

# 初期コード

※検証用に printStatus()という関数も用意しておきます。

```swift
class IntBox{
    enum Size{
        case S, M, L
    }

    // 横縦高さ
    var width: Int
    var height: Int
    var depth: Int

    // 体積
    var volume: Int

    // サイズ
    var size:Size
    
    init(w: Int, h: Int, d: Int){
	// 横縦高さの初期化
        width = w
        height = h
        depth = d
	
	// 体積の初期化
        volume = w * h * d
	
	// サイズの初期化
        switch volume{
        case 1...8:
            size = .S
        case 9...64:
            size = .M
        default:
            size = .L
        }
    }

    // 検証用にステータス一覧を表示する関数を用意しています。
    func printStatus(){
        var status = """
        ---ステータス---
        width: \(width)
        height: \(height)
        depth: \(depth)
        volume: \(volume)
        size: \(size)
        """
        print(status)
    }
}
```

### 突っ込みどころ
1. 一辺の最大６という条件が無視されている。
2. 一辺の長さが０以下の場合にエラーになってくれない。
3. 辺の長さを変えても体積やサイズは変わらない。
4. 体積を外から変更できてしまう。
5. サイズを外から変更できてしまう。

### 検証

```swift
let cb = IntBox(w: 1, h: 1, d: 1)
cb.printStatus()

// 横幅を変えてみる
cb.width = 11
cb.printStatus()

// 体積を変えてみる
cb.volume = -20
cb.printStatus()

// サイズを変えてみる
cb.size = .L
cb.printStatus()
```

実行結果

```
---ステータス---
width: 1
height: 1
depth: 1
volume: 1
size: S

---ステータス---
width: 11
height: 1
depth: 1
volume: 1 # 変化すべし!!
size: S

---ステータス---
width: 11
height: 1
depth: 1
volume: -20 # 0以下はダメ　setできるのもおかしい(体積は各辺の長さに依存している)
size: S

---ステータス---
width: 11
height: 1
depth: 1
volume: -20
size: L # 体積と同じくsetできるのがおかしい。依存関係を考えるべし
```

ということで一個ずつ解決していきます。

# #1 バリデーション

[突っ込みどころ](#突っ込みどころ) 1, 2を解決します。

>1 一辺の最大６という条件が無視されている。

>2 一辺の長さが０以下の場合にエラーになってくれない。

イニシャライズの最初に下記を挿入します。

```swift
if w <= 0 || w > 6 {fatalError("w:\(w) not in 1...6")}
if h <= 0 || h > 6 {fatalError("h:\(h) not in 1...6")}
if d <= 0 || d > 6 {fatalError("d:\(d) not in 1...6")}
```

# #2 willSet と didSet
[突っ込みどころ](#突っ込みどころ) 3を解決します。

>3 辺の長さを変えても体積やサイズは変わらない。

縦横高さの値が変化時に体積やサイズを変更したいので
各プロパティに**willSet**と**didSet**を追加してそこで体積やサイズをセットします。

### will did どちらのタイミングに何をするのか
バリデーションは変更したい長さをセットしてもいいかどうかの検証を意味するのでセットする前に。
長さの変更が原因で体積やサイズが変更するのでセット後に。

- willSet: バリデーション
- didSet: 体積やサイズのセット


```swift
var width: Int{
    willSet{
        if newValue<= 0 || newValue> 6 {fatalError("<width: \(newValue)> not in 1...6")}
    }
    didSet{
        _volume = width * height * depth
        switch _volume{
        case 1...8:
            _size = .S
        case 9...64:
            _size = .M
        default:
            _size = .L
        }
    }
}

var height: Int{
    willSet{
        if newValue<= 0 || newValue> 6 {fatalError("<height: \(newValue)> not in 1...6")}
    }
    didSet{
        _volume = width * height * depth
        switch _volume{
        case 1...8:
            _size = .S
        case 9...64:
            _size = .M
        default:
            _size = .L
        }
    }
}

var depth: Int{
    willSet{
        if newValue<= 0 || newValue> 6 {fatalError("<depth: \(newValue)> not in 1...6")}
    }
    didSet{
        _volume = width * height * depth
        switch _volume{
        case 1...8:
            _size = .S
        case 9...64:
            _size = .M
        default:
            _size = .L
        }
    }
}
```

# #3 Read Only Property にする。

[突っ込みどころ](#突っ込みどころ) 4と5を解決します。

>4 体積を外から変更できてしまう。
>5 サイズを外から変更できてしまう。

Read Only Property にすることでsetができなくしちゃいます。

- volume と size に get{}を追加
- 外から見ることができないprivate var _volume と private var _size にデータを保持します。

```swift
private var _volume: Int
private var _size: Size

var volume: Int {
    get {
        return _volume
    }
}

var size: Size {
    get {
        return _size
    }
}
```

# 一旦完成！？

。。。想像通りの冗長なコードになってます。

```swift
class IntBox{
    enum Size{
        case S, M, L
    }
    private var _volume: Int
    private var _size: Size

    var width: Int{
        willSet{
            if newValue<= 0 || newValue> 6 {fatalError("<width: \(newValue)> not in 1...6")}
        }
        didSet{
            _volume = width * height * depth
        switch _volume{
            case 1...8:
                _size = .S
            case 9...64:
                _size = .M
            default:
                _size = .L
            }
        }
    }
    
    var height: Int{
        willSet{
            if newValue<= 0 || newValue> 6 {fatalError("<height: \(newValue)> not in 1...6")}
        }
        didSet{
            _volume = width * height * depth
        switch _volume{
            case 1...8:
                _size = .S
            case 9...64:
                _size = .M
            default:
                _size = .L
            }
        }
    }
    
    var depth: Int{
        willSet{
            if newValue<= 0 || newValue> 6 {fatalError("<depth: \(newValue)> not in 1...6")}
        }
        didSet{
            _volume = width * height * depth
        switch _volume{
            case 1...8:
                _size = .S
            case 9...64:
                _size = .M
            default:
                _size = .L
            }
        }
    }
    
    var volume: Int {
        get {
    	    return _volume
        }
    }
    
    var size: Size {
        get {
    	    return _size
        }
    }
    
    init(w: Int, h: Int, d: Int){
        if w <= 0 || w > 6 {fatalError("<w: \(w)> not in 1...6")}
        if h <= 0 || h > 6 {fatalError("<h: \(h)> not in 1...6")}
        if d <= 0 || d > 6 {fatalError("<d: \(d)> not in 1...6")}
        width = w
        height = h
        depth = d
        _volume = w * h * d
        switch _volume{
        case 1...8:
            _size = .S
        case 9..<64:
            _size = .M
        default:
            _size = .L
        }
    }
    
    func printStatus(){
        var status = """
        ---ステータス---
        width: \(width)
        height: \(height)
        depth: \(depth)
        volume: \(volume)
        size: \(size)
        """
        print(status)
    }
}

let cb = IntBox(w: 1, h: 1, d: 1)
cb.printStatus()

// 横幅を変えてみる
cb.width = 11
cb.printStatus()
```

ＤＲＹ原則違反が多いので共通の関数を作ってまとめていこうと思います。

# #4 DRY原則適用

※関数にまとめる上ですべてのストアドプロパティが初期化しないとself.func が呼び出せないので各プロパティに初期値を設定しておきます。
→本当はもっといい方法があるのかも

### バリデート部分を関数にまとめるべし

```swift
private func lengthValidate(_ length: Int, _ name: String = "length"){
    if length <= 0 || length > 6 { fatalError("<\(name): \(length)> not in 1...6") }
}
```

### 体積の計算やサイズの評価をまとめるべし

```swift
private func setStatus() {
    setVolume()
    setSize()
}

private func setVolume() {
    _volume = width * height * depth
}

private func setSize(){
    switch _volume{
        case 1...8:
            _size = .S
        case 9...64:
            _size = .M
        default:
            _size = .L
    }
}

```

### 縦横高さを変更する度に***setStatus()***が呼ばれるのも解消したい。

volumeやsizeが参照される時に**setStatus()**ができればいいと思います。
ただし変更がないのに
volumeやsizeが参照される度に**setStatus()**が呼ばれるのも回避したいので
縦横高さを変更後一度だけ**setStatus()**を呼びたいので
**private var shouldSetStatus: Bool**を作成して**setStatus()**を呼ぶかどうか評価します。

```swift
private var shouldSetStatus: Bool

var width: Int = 1 {
    willSet { lengthValidate(newValue, "width") }
    didSet { shouldSetStatus = true } // <- 変更
}
...

var volume: Int {
    get {
        if shouldSetStatus { setStatus() }
        return _volume
    }
}
...

private func setStatus() {
    setVolume()
    setSize()
    shouldSetStatus = false // <- 追加
}
```

# DRY原則違反の解決後コード

```swift
class IntBox{
    enum Size{
        case S, M, L
    }
    
    private var _volume: Int = 1
    private var _size: Size = .S
    private var shouldSetStatus: Bool = false
    
    var width: Int = 1 {
        willSet { lengthValidate(newValue, "width") }
        didSet { shouldSetStatus = true }
    }
    var height: Int = 1 {
        willSet { lengthValidate(newValue, "height") }
        didSet { shouldSetStatus = true }
    }
    var depth: Int = 1 {
        willSet { lengthValidate(newValue, "depth") }
        didSet { shouldSetStatus = true }
    }

    var volume: Int {
        get {
            if shouldSetStatus { setStatus() }
            return _volume
        }
    }
    var size: Size {
        get {
            if shouldSetStatus { setStatus() }
    	    return _size
        }
    }
    
    init(w: Int, h: Int, d: Int){
        lengthValidate(w, "w")
        lengthValidate(h, "h")
        lengthValidate(d, "d")
        width = w
        height = h
        depth = d
        setStatus()
    }
    
    private func lengthValidate(_ length: Int, _ name: String = "length"){
    	if length <= 0 || length > 6 { fatalError("<\(name): \(length)> not in 1...6") }
    }
    
    private func setStatus() {
        setVolume()
        setSize()
        shouldSetStatus = false
    }
    
    private func setVolume() {
        _volume = width * height * depth
    }
    
    private func setSize(){
        switch _volume{
            case 1...8:
                _size = .S
            case 9...64:
                _size = .M
            default:
                _size = .L
        }
    }
    
    // 検証用コード
    func printStatus(){
        let status = """
        ---ステータス---
        width: \(width)
        height: \(height)
        depth: \(depth)
        volume: \(volume)
        size: \(size)
        """
        print(status)
    }
}

// 検証
let cb = IntBox(w: 1, h: 2, d: 3)
cb.printStatus()

// 横幅を変えてみる
cb.width = 5
cb.printStatus()
```

# 最後に
本来は一辺の最大値やS,M,Lの評価を柔軟に変更できるよう抽象的なプロトコルなどから設計したり
エラーに関して**Optional**を使わずに**fatalError**のみになってますが、イニシャライズ時はエラーの際に**nil**を返すとか長さ変更時のエラーとは分けた方がいいのかもしれない。。。
一旦、今の自分の精一杯を記事にしてみました。
いつかこれが恥ずかしい記事と思えるほど成長しようと思います。
あとこの記事に２時間×3日かけてしまった。。。
コスパも考えて記事を書いていこうと思います。