はじめに
サービスで使用するEmailアドレスやPasswordなどをアプリから入力する際に、アプリ側でValidationをかけることがあります。
そのような際に行う対応として直感的には以下のようにするかと思います。
extension ViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
if textField === textField {
let text = textField.text
if text?.isEmpty == true {
errorLabel.text = "文字を入力してください。"
} else if text?.contains("@") == false {
errorLabel.text = "正しいメールアドレスの形式ではありません。"
}
}
}
}
ただ、この方法では以下のような問題が発生します。
- 同様なバリデーション処理が様々な箇所に分散してしまう。結果として修正漏れなどによる不具合が発生する
- コードが冗長となり可読性が下がる
本記事では、Compositeパターンを適用することでValidation処理を構造的に作成し、
上記であげた問題に対処する方法を記載します。
環境設定
以下の環境を使用しています。
方法
Validator Protocolの作成
Validation用のエラーとValidation結果およびValidatorが準拠するべきProtocolを定義します。
ValidationResultはSwift.Resultを使用してもよかったのですが、Resultに指定した型Tの処理の部分が 冗長になるかと考えこの方法をとりました。
ValidationのErrorはLocalizedErrorに準拠することで errorDescription: String?
を実装すると、
error.localizedDescription
でエラーメッセージを表示させることができます。
Validatorはvalidate対象を引数とし、結果を返却するメソッドを持つProtocolです。
enum ValidationResult {
case valid
case invalid(ValidationError)
}
protocol ValidationErrorProtocol: LocalizedError { }
protocol Validator {
func validate(_ value: String) -> ValidationResult
}
Validatorを複数持つCompositeValidator Protocolを作成します。
Composite Validatorはvalidators
としてValidatorの配列を持ち、validateメソッド実行時に
それぞれのValidatorのvalidateメソッドを順次実行、その返却値を元にcomposite validatorの
validate実行結果を返却します。
UITextFieldのtextに対して適応するValidatorは基本的にComposite Validatorを使用します。
protocol CompositeValidator: Validator {
var validators: [Validator] { get }
func validate(_ value: String) -> ValidationResult
}
extension CompositeValidator {
func validate(_ value: String) -> [ValidationResult] {
return validators.map { $0.validate(value) }
}
func validate(_ value: String) -> ValidationResult {
let results: [ValidationResult] = validate(value)
let errors = results.filter { result -> Bool in
switch result {
case .valid: return false
case .invalid: return true
}
}
return errors.first ?? .valid
}
}
Validatorの実装の具体例
上記で記載したValidatorプロトコル、 Composite Validatorプロトコルを元に実際の実装をみていきます。
今回は試しに名前の入力値に対するValidatorを検討します。以下を仕様とします。
- 文字入力必須
- 1文字以上、8文字以下
- 英語大文字小文字のみ入力可
それぞれのケースのエラーをまずは作成します。
enum ValidationError: ValidationErrorProtocol {
case empty
case length(min: Int, max: Int)
case nameFormat
var errorDescription: String? {
switch self {
case .empty: return "文字を入力してください"
case .length(let min, let max): return "\(min)文字以上\(max)文字以下で入力してください。"
case .nameFormat: return "英語大文字小文字のみで入力してください。"
}
}
}
次にそれぞれのケースのValidatorを作成します。
文字入力必須
struct EmptyValidator: Validator {
func validate(_ value: String) -> ValidationResult {
if value.isEmpty == true {
return .invalid(.empty)
} else {
return .valid
}
}
}
1文字以上、8文字以下
struct LengthValidator: Validator {
let min: Int
let max: Int
func validate(_ value: String) -> ValidationResult {
if value.count >= min && value.count <= max {
return .valid
} else {
return .invalid(.length(min: min, max: max))
}
}
}
英語大文字小文字のみ入力可
struct NameFormatValidator: Validator {
let regExpression = "^[a-zA-Z]+$"
func validate(_ value: String) -> ValidationResult {
let predicate = NSPredicate(format: "SELF MATCHES %@", regExpression)
let result = predicate.evaluate(with: value)
switch result {
case true: return .valid
case false: return .invalid(.nameFormat)
}
}
}
最後に今まで作成したValidatorを組み合わせたCompositeValidatorを作成します。
これで名前のTextFieldの入力値を検査するValidatorが作成できました。
struct NameValidator: CompositeValidator {
var validators: [Validator] = [
EmptyValidator(),
LengthValidator(min: 1, max: 8),
NameFormatValidator()
]
}
実際の使い方を見てみます。以下のようになります。
Validationの具体的な実装はNameValidator内部に閉じ込められ、同様の処理が拡散することを防いでいます。
Validationの内容を変えたい場合はNameValidatorを変更するか、より下層のここのValidatorを変更すれば良いため修正漏れも防げます。
また、今後新しいComposite Validatorを作成したい場合は今まで作成したValidatorを有効に利用することもできます。
if else などが連続してかかれることもないためコードの冗長性もずいぶん改善され可読性が上がりました。
extension ViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
if textField === textField {
let nameValidator = NameValidator()
let result: ValidationResult = nameValidator.validate(textField.text ?? "")
switch result {
case .valid: break
case .invalid(let error): print(error.localizedDescription)
}
}
}
}
UITextFieldのExtensionとして定義する
先ほどまでのやり方でかなり改善されましたが、さらに簡略化する方法を考えてみます。
一つの方法としてUITextFieldのExtensionメソッドとしてvalidateを作成する方法があります。
Validateの種別(名前、メールアドレス、パスワード)を定義して、その種別に応じたvalidateを実行するメソッドを作成することで実行時の実装はかなり簡略化できます。
extension UITextField {
enum ValidateType {
case name
}
@discardableResult
func validate(type: ValidateType, errorMessageOn label: UILabel? = nil) -> ValidationResult {
let result: ValidationResult
switch type {
case .name:
result = NameValidator().validate(text ?? "")
}
switch result {
case .valid: label?.text = nil
case .invalid(let error): label?.text = error.localizedDescription
}
return result
}
}
上記のExtensionメソッドを使用することで、使用側の実装は下記のようにかなりシンプルになりました。
extension ViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
if textField === textField {
textField.validate(type: .name, errorMessageOn: errorLabel)
}
}
}
まとめ
ユーザーの文字入力をアプリ側でValidateする際に、それらの仕組みをCompositeパターンを使用して構造的に作成する方法を記載しました。本記事の方法を使用することで、修正漏れが少なく可読性が高い保守しやすいValidationの仕組みを作成することができるかと思います。
参考