6
2

More than 1 year has passed since last update.

共通パーツはどのように共通化すべきなのか?(UIView)

Last updated at Posted at 2022-02-17

Xcode-13.1 Swift-5.5.1 iOS-15.0

はじめに

アプリを作っているときに複数画面で同じような表示をみかけると共通のカスタム View として切り出すことがよくあります。

しかし、安易に共通化すると後々の仕様変更時に痛い目にあう可能性があります。痛い目にあわないように共通のカスタム View はどのように作るべきなのか考えたいと思います。
(明確な答えがあるわけじゃないのでポエムのようなものです:innocent:

事象

添付のような画面があった場合、下部の金額表示部分は同じなので共通化したくなるはずです。

画面A 画面B
screen_a screen_b

こんな感じです。

view_1
// LoadXIBViewはxibからViewを生成するためのクラスです
final class HogePriceView: LoadXIBView {

    struct Config {
        let fugaPrice: String
        let piyoPrice: String
        let totalPrice: String
    }

    @IBOutlet private weak var fugaPriceLabel: UILabel!
    @IBOutlet private weak var piyoPriceLabel: UILabel!
    @IBOutlet private weak var totalPriceLabel: UILabel!

    func configure(_ config: Config) {
        fugaPriceLabel.text = config.fugaPrice
        piyoPriceLabel.text = config.piyoPrice
        totalPriceLabel.text = config.totalPrice
    }
}

いい感じです(DRY!DRY!):sunglasses:

仕様変更1

開発が進んでいくと HogePriceView は色々な画面で使用されるようになりました。ある日、「画面 F では項目 Foo も表示してほしい」という仕様変更がありました。

下記のように HogePriceView を修正します:muscle:

view_2
final class HogePriceView: LoadXIBView {

    struct Config {
        let fugaPrice: String
        let piyoPrice: String
        let totalPrice: String
        let fooPrice: String?
        let isFooShown: Bool

        init(fugaPrice: String,
             piyoPrice: String,
             totalPrice: String,
             fooPrice: String? = nil,
             isFooShown: Bool = false) {
            self.fugaPrice = fugaPrice
            self.piyoPrice = piyoPrice
            self.totalPrice = totalPrice
            self.fooPrice = fooPrice
            self.isFooShown = isFooShown
        }
    }

    @IBOutlet private weak var fugaPriceLabel: UILabel!
    @IBOutlet private weak var piyoPriceLabel: UILabel!
    @IBOutlet private weak var totalPriceLabel: UILabel!
    @IBOutlet private weak var fooPriceLabel: UILabel!
    @IBOutlet private weak var fooContainer: UIView!

    func configure(_ config: Config) {
        fugaPriceLabel.text = config.fugaPrice
        piyoPriceLabel.text = config.piyoPrice
        totalPriceLabel.text = config.totalPrice
        fooPriceLabel.text = config.fooPrice
        fooContainer.isHidden = !config.isFooShown
    }
}

画面 F でのみ下記のように呼び出せば画面 F でのみ項目 Foo が表示されます。

hogeView.configure(.init(
    fugaPrice: "100円", piyoPrice: "250円",
    totalPrice: "750円", fooPrice: "400円",
    isFooShown: true
))

完璧です:smirk:

仕様変更2

その後も仕様変更は続き、「画面 G では赤文字で警告文を表示してほしい」「画面 F では合計に注釈を表示してほしい」など様々な修正が入ります。

完成した HogePriceView がこちらです。

view_3
final class HogePriceView: LoadXIBView {

    struct Config {
        let fugaPrice: String
        let piyoPrice: String
        let totalPrice: String
        let fooPrice: String?
        let isFooShown: Bool
        let warning: String?
        let isWarningShown: Bool
        let isAnnotationShown: Bool

        init(fugaPrice: String,
             piyoPrice: String,
             totalPrice: String,
             fooPrice: String? = nil,
             isFooShown: Bool = false,
             warning: String? = nil,
             isWarningShown: Bool = false,
             isAnnotationShown: Bool = false) {
            self.fugaPrice = fugaPrice
            self.piyoPrice = piyoPrice
            self.totalPrice = totalPrice
            self.fooPrice = fooPrice
            self.isFooShown = isFooShown
            self.warning = warning
            self.isWarningShown = isWarningShown
            self.isAnnotationShown = isAnnotationShown
        }
    }

    @IBOutlet private weak var fugaPriceLabel: UILabel!
    @IBOutlet private weak var piyoPriceLabel: UILabel!
    @IBOutlet private weak var totalPriceLabel: UILabel!
    @IBOutlet private weak var fooPriceLabel: UILabel!
    @IBOutlet private weak var fooContainer: UIView!
    @IBOutlet private weak var warningLabel: UILabel!
    @IBOutlet private weak var warningContainer: UIView!
    @IBOutlet private weak var annotationLabel: UILabel!

    func configure(_ config: Config) {
        fugaPriceLabel.text = config.fugaPrice
        piyoPriceLabel.text = config.piyoPrice
        totalPriceLabel.text = config.totalPrice
        fooPriceLabel.text = config.fooPrice
        fooContainer.isHidden = !config.isFooShown
        warningLabel.text = config.warning
        warningContainer.isHidden = !config.isWarningShown
    }
}

HogePriceView はあらゆる画面で使用されているのでもう迂闊にはさわれない View になってしまいました。。。

どうすべきだったのか

共通化する場合、「変更頻度・変更理由が同じ」ものを共通化すべきです。ただ処理が似ているからという理由で安易に共通化してはいけません。

仕様変更1のときに画面 F でのみ必要な機能を HogePriceView に追加しています。HogePriceView に追加してしまうと今後も画面 F だけの修正のために共通 View である HogePriceView を修正する必要がでてきます。思い切って画面 F 専用の View を作るのが無難かなと思います。

まとめ

共通化するときは下記2つの原則を意識するといいはず(たぶん対象が違うだけでどっちも同じようなこと)。

  • 閉鎖性共通の原則(CCP)

    同じ理由、同じタイミングで変更されるクラスをコンポーネントにまとめること。変更の理由やタイミングが異なるクラスは、別のコンポーネントにわけること。

    Clean Architecture 達人に学ぶソフトウェアの構造と設計 p.119

  • 単一責任の原則(SRP)

    アクターの異なるコードは分割するべき

    Clean Architecture 達人に学ぶソフトウェアの構造と設計 p.83

最初から分離できていればいいのですがなかなか最初に判断するのは難しいです。異なるアクターというのがわりと判断が難しいです。最初から分離するのは困難なのでわからなければとりあえず共通化しといて仕様変更や機能追加のたびに共通化すべきか分離すべきかを考えるべきです。

おわりに

実装の初期段階から共通化すべきか分離すべきか判断するのは難しいのでおや?と思ったときに改めて考えるのがいいんじゃないかなというふわっとした結論です。

何回か修正しているとこの画面だけ他と違うんじゃないか?と思うことがあります。最初はとりあえず共通化しといて疑問に思ったときに分離するのもありじゃないかなと思います。

経験を積めば最初からいい感じに分離できるのかも:thinking:

6
2
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
6
2