LoginSignup
5
3

More than 3 years have passed since last update.

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

Posted at

Swift歴3ヶ月の初心者の初投稿

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

今回作った課題

各辺の長さがInt型の直方体のクラスをリファクタリングしながら学びます。

目的

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

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

条件:どんな直方体?

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

初期コード

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

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. 一辺の最大6という条件が無視されている。
  2. 一辺の長さが0以下の場合にエラーになってくれない。
  3. 辺の長さを変えても体積やサイズは変わらない。
  4. 体積を外から変更できてしまう。
  5. サイズを外から変更できてしまう。

検証

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 一辺の最大6という条件が無視されている。

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

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

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 辺の長さを変えても体積やサイズは変わらない。

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

will did どちらのタイミングに何をするのか

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

  • willSet: バリデーション
  • didSet: 体積やサイズのセット
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 にデータを保持します。
private var _volume: Int
private var _size: Size

var volume: Int {
    get {
        return _volume
    }
}

var size: Size {
    get {
        return _size
    }
}

一旦完成!?

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

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()

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

#4 DRY原則適用

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

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

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()
}

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()を呼ぶかどうか評価します。

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原則違反の解決後コード

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を返すとか長さ変更時のエラーとは分けた方がいいのかもしれない。。。
一旦、今の自分の精一杯を記事にしてみました。
いつかこれが恥ずかしい記事と思えるほど成長しようと思います。
あとこの記事に2時間×3日かけてしまった。。。
コスパも考えて記事を書いていこうと思います。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3