Protocol
Swift
DesignPattern
SwiftDay 21

User登録画面で活用できそうなProtocolを活用したDesignPattern3選

More than 1 year has passed since last update.

2016 Swift Advent Calenderの21日目です。
今回はアプリ開発をする時にはほとんどの場面で作るであろうユーザー登録機能をどのように実装していくのかという観点から役に立ちそうなProtocolを活用したDesign Patternをまとめていきます。(基礎的な内容であろう事項をまとめるので応用的・実践的なPattern等はコメントにて教えていただけると嬉しいです。)

今回考えるのは以下のようなUI達です。

スクリーンショット 2016-12-21 22.41.46.png

アプリの初回立ち上げ時に新規ユーザー登録を行う際はUserのモデルクラスを作って、そこにデータを管理することになると思います。

そのためにアプリ内で一つしか存在しえないインスタンスを生成してデータ管理をすることになるでしょう。その際には言わずもがなSingleton Patternを採用することになります。

1. Singleton Pattern

シングルトンを作る際にはSwiftではstaticを活用すると良さそうです。

struct UserSettingClass {

    static var sharedInstance = UserSettingClass()

    static var email: String = ""
    static var password: String = ""
    static var userName: String = ""
    static var gender: Gender = .unknown
    static var birthday: Date? = nil
    static var profileImage: Data? = nil

    enum Gender {
        case unknown
        case man
        case woman
    }

    static func saveData(){
        //処理
    }
}

そうすると以下の様にシングルトンを生成できます。

var userSetting = UserSettingClass.sharedInstance

しかし、classstructは空の状態でもインスタンスの初期化が出来てしまいますが、空のenumでは出来ません。そのためにenumを活用するという手もありそうです。

enum UserSettingEnum {

    static var email: String = ""
    static var password: String = ""
    static var userName: String = ""
    static var gender: Gender = .unknown
    static var birthday: Date? = nil
    static var profileImage: Data? = nil

    enum Gender {
        case unknown
        case man
        case woman
    }

    static func saveData(){
        //処理
    }
}

これで以下の様に呼び出しが出来ます。

var textField: UITextField!
UserSettingEnum.email = textField.text!

2. Builder Pattern

今回のUserSettingのモデルはプロパティ数が少なく初期化特に苦労はしなさそうです。

しかし、場合によってはUserモデルが大きくなりプロパティ数が多くなりすぎることがありえます。

class ComplexityUser {

    var name: String?
    var age: Int?
    var email: String?
    var password: String?
    var prefecture: String?
    var job: String?
    var weight: Double?
    var height: Double?
    var message: String?

    init(name: String, age: Int, email: String, password: String, prefecture: String, weight: Double, height: Double, message: String) {
        self.name = name
        self.age = age
        self.email = email
        self.password = password
        self.prefecture = prefecture
        self.weight = weight
        self.height = height
        self.message = message
    }
}

この場合、初期化のコードは以下の様になります。

let complexityUser = ComplexityUser(name: "aritaku", age: 25, email: "hoge@hoge", password: "password", prefecture: "shiga", weight: 62.0, height: 180.0, message: "hello")

辛いですね。そういった場合ではBuilder Patternが使えそうです。

protocol UserBuilder {
    var name: String { get }
    var age: Int { get }
    var email: String { get }
    var password: String { get }
    var prefecture: String { get }
    var job: String { get }
    var weight: Double { get }
    var height: Double { get }
    var message: String { get }
}

struct AppUserBuilder: UserBuilder {

    var name = "Burger"
    var age = 25
    var email = "hoge@hoge"
    var password = "hogehogehoge"
    var prefecture = "Kagoshima"
    var job = "Swift Programmer"
    var weight = 62.2
    var height = 180.0
    var message = "はじめまして。"

}

struct User {

    var name: String
    var age: Int
    var email: String
    var password: String
    var prefecture: String
    var job: String
    var weight: Double
    var height: Double
    var message: String

    init(builder: UserBuilder) {
        self.name = builder.name
        self.age = builder.age
        self.email = builder.email
        self.password = builder.password
        self.prefecture = builder.prefecture
        self.job = builder.job
        self.weight = builder.weight
        self.height = builder.height
        self.message = builder.message
    }
}

最初にUserBuilderというprotocolを宣言してあります。
このUserBuilder Protocolを活用してUserモデルの初期化をしてあげるというアイデアです。

すると初期化は以下の様に楽に済ませることができます。

var newUser = User(builder: AppUserBuilder())

3. Factory Pattern

次に以下の画面のようにパスワードやEmailなどが入力されたときにバリデーションをかける場合を考えます。

スクリーンショット 2016-12-21 23.13.08.png

めんどくさいのは、入力された値の種類によってどのようなバリデーションをかけるのかが変わってくる時です。

その場合にはFactory Patternが有効です。

以下の様にValidationに関するProtocolを宣言します。

protocol TextValidationProtocol {
    var regExFindMatchString: String { get }
    var validationMessage: String { get }
}

extension TextValidationProtocol {

    var regExMatchingString: String {
        get {
            return regExFindMatchString + "$"
        }
    }

    func validationString(str: String) -> Bool {
        if let _ = str.range(of: regExMatchingString, options: .regularExpression) {
            return true
        } else {
            return false
        }
    }

    func getMatchingString(str: String) -> String? {
        if let newMatch = str.range(of: regExFindMatchString, options: .regularExpression) {
            return str.substring(with: newMatch)
        } else {
            return nil
        }
    }

}

次に想定されるバリデーションの種類をTextValidationProtocolにもとづいて宣言していきます。

今回のシングルトンはenumではstored propertyが持てないためにclassを使用しました。

class AlphaValidation: TextValidationProtocol {

    static let sharedInstance = AlphaValidation()
    private init(){}
    let regExFindMatchString = "^[a-zA-Z]{0,10}"
    let validationMessage = "Can only contain Alpha characters"
}

class AlphaNumericValidation: TextValidationProtocol {

    static let sharedInstance = AlphaNumericValidation()
    private init(){}
    let regExFindMatchString = "^[a-zA-Z0-9]{0,10}"
    let validationMessage = "Can only contain Alpha Numeric characters"
}

class NumericValidation: TextValidationProtocol {

    static let sharedInstance = NumericValidation()
    private init(){}
    let regExFindMatchString = "^[0-9]{0,10}"
    let validationMessage = "Can only contain only Alpha and Numeric characters."
}

また以上の種類のバリデーション操作をまとめたメソッドを作成します。
引数にalphaCharactersnumericCharactersのBool値を持たせることによってかけるべきバリデーション種類を決めます。

func getValidator(alphaCharacters: Bool, numericCharacters: Bool) -> TextValidationProtocol? {

    if alphaCharacters && numericCharacters {
        return AlphaNumericValidation.sharedInstance
    } else if alphaCharacters && !numericCharacters {
        return AlphaValidation.sharedInstance
    } else if !alphaCharacters && numericCharacters {
        return NumericValidation.sharedInstance
    } else {
        return nil
    }
}

そうすることで以下の様にバリデーションをかけることができます。

var str = "abc123"
var validator1 = getValidator(alphaCharacters: true, numericCharacters: true)
print("String Validated: \(validator1?.validationString(str: str))") //true

var validator2 = getValidator(alphaCharacters: true, numericCharacters: false)
print("String Validated: \(validator2?.validationString(str: str))") //false

簡単ではありましたが、以上でユーザー登録機能の実装の際に役に立ちそうなProtocolを活用したDesign Pattern3選の紹介を終わります。シングルトンのところはProtocol使ってないじゃんっていうツッコミは心の中にとどめておいてください。笑

気になるところが色々とご指摘いただければ嬉しいです。

参考

「Protocol Oriented Programming」