デコレーターをSwift5で実装する


※この記事は「全デザインパターンをSwift5で実装する」https://qiita.com/satoru_pripara/items/3aa80dab8e80052796c6 の一部です。

The Decorator(デコレータ)

0. デコレータの意義

デコレータは、あるクラスなどに元は無かった振る舞いを追加するデザインパターンである。元のクラスのコードをいじることなく振る舞いを柔軟に変えることができる。

また継承を行うのとは違って、プログラム実行時に動的にデコレータを使用し機能拡張することもできる。

注意点としては、元のクラスと全く関連性がないような機能をデコレータで付け加えるのはよろしくないという事である。単一のクラスは単一の機能・責務だけを持つべきという「単一責務の原則」に反し、分かりづらく保守しにくいコードになってしまう。

※単一責務の原則については下記などを参照
https://qiita.com/decoy0318/items/f201b725e91a4a1bb4cf

Swiftではextensionを用いるか、デコレータクラスを新たに作る方法がある。

1. extensionを使う場合

extensionを使うと元のクラスにそのまま新しい機能を追加できるため非常に直感的で使いやすくなっている。

UIColorExtension.swift
import UIKit

public extension UIColor {
    //HTMLコード(#1411A4など)からUIColorインスタンスを作るイニシャライザを新設する
    convenience init(hex: UInt32) {
        let divisor = CGFloat(255)
        let red = CGFloat((hex & 0xFF0000) >> 16) / divisor
        let green = CGFloat((hex & 0x00FF00) >> 8) / divisor
        let blue = CGFloat(hex & 0x0000FF) / divisor

        self.init(red: red, green: green, blue: blue, alpha: 1)
    }
}

ただしextensionには制約があり、ストアドプロパティを新設するor既存のストアドプロパティの振る舞いを変更することはできない。

※ストアドプロパティについては下記参照
https://qiita.com/akeome/items/2197a635ac616ab2f8e2

その場合には、下記のようにデコレータクラスを使うことになる。

2.デコレータクラスを使う場合

下記はUILabelにデコレータを使った例。

デコレータクラス自体が、目的のクラス(ここではUILabel)を継承する。

UILalbelDecorator.swift
import UIKit

public class BorderedLabelDecorator: UILabel {

プロパティとしてもUILabelを保持する。

UILalbelDecorator.swift
fileprivate let wrappedLabel: UILabel

UILabelの変数の振る舞いを変更していく。

UILalbelDecorator.swift
    public required init(label: UILabel, cornerRadius: CGFloat = 3.0, borderWidth: CGFloat = 1.0, borderColor: UIColor = .black) {
        self.wrappedLabel = label

        super.init(frame: label.frame)

        self.wrappedLabel.layer.cornerRadius = cornerRadius
        self.wrappedLabel.layer.borderWidth = borderWidth
        self.wrappedLabel.layer.borderColor = borderColor.cgColor
        self.wrappedLabel.clipsToBounds = true
    }

    public required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    //使用予定のあるUILabelの変数をオーバーライドし、wrappedLabelとBorderedLabelDecoratorの変数がリンクするようにする
    public override var textAlignment: NSTextAlignment {
        get {
            return self.wrappedLabel.textAlignment
        }

        set {
            self.wrappedLabel.textAlignment = newValue
        }
    }

    public override var backgroundColor: UIColor? {
        get {
            return self.wrappedLabel.backgroundColor
        }

        set {
            self.wrappedLabel.backgroundColor = newValue
        }
    }

    public override var textColor: UIColor? {
        get {
            return self.wrappedLabel.textColor
        }

        set {
            self.wrappedLabel.textColor = newValue
        }
    }


    public override var layer: CALayer {
        return self.wrappedLabel.layer
    }

    //元のストアドプロパティの振る舞いを変更する(文字をセットした時に顔文字を付け加える)
    public override var text: String? {
        get {
            return self.wrappedLabel.text
        }

        set {
            var str = "🤔"
            if let newVal = newValue {
                str += newVal + str
            }

            self.wrappedLabel.text = str
        }
    }
}

参考文献: https://www.amazon.com/Design-Patterns-Swift-implement-Improve-ebook/dp/B07MDD3FQJ