一、前言
近期公司有一个APP重构登录页面,我们知道在页面绘制大量UITextField时会产生一个问题:需要在UITextFieldDelegate或者监听.editingChanged
状态时添加大量判断逻辑。控制器中充斥着大量if else/switch case等逻辑判断。这种情况下去解耦控制器,使用策略模式再好不过。
二、什么是策略模式
策略模式的概念这里不多赘述,可以去参考一下我之前写的一篇:iOS设计模式:策略模式和工厂模式区别
这里把策略模式的结构图贴过来,一会根据实际项目在讲解。
三、项目案例
这里仿写一个<沪江网校>的登录界面,把关键代码贴出来给大家讲解。
上面图中所示的两种登录方式,分别有4个TextField,一般的做法是在点击登录按钮时,添加每个TextField的判断:
/// 伪代码
func loginButtonClick() {
1 用户名不能为空 -> 用户名长度在4-20位 -> 不要有符号等等
2 密码不能为空 -> 密码长度在8-20位 -> 必须包含大小写等等
3 手机号正则判断
4 动态码长度判断 -> 特殊字符判断等等
}
可想而知这个方法会有大量的if else判断,下面我们通过策略模式对其进行解耦。
四、策略模式应用
4.1 实现Strategy类
对应之前的策略模式结构图,每一个TextField的逻辑判断对应一个Strategy子类,所以先声明一个Strategy父类,所有子类继承它:
class TextFieldStrategy {
var message: String = "" // 策略判断的结果说明
var code: LoginSuccessCode = .Error // 策略判断的结果
// 抽象方法
func textFieldValidate(_ textField: UITextField) -> LoginSuccessCode {
return code
}
}
说明:这里抽象方法的返回值,可以根据不同的项目需求自行定义,我这里是声明了一个枚举,默认值的枚举rawValue为400,如果返回结果rawValue>400,则说明策略判断失败,反之成功。
enum LoginSuccessCode: Int {
case Username = 1
case Password = 2
case Phonenum = 3
case Verifycode = 4
case Error_Username = 401
case Error_Password = 402
case Error_Phonenum = 403
case Error_VerifyCode = 404
case Error = 400
}
4.2 实现Context类
在策略模式结构图中,Context要持有Strategy。这个案例中,Context类毫无疑问就是TextField的父类了,让TextField基类持有刚刚创建的TextFieldStrategy策略,并指定一个目标方法,在目标方法中执行TextFieldStrategy策略的抽象方法。
class BaseTextField: UITextField {
/// init()等其他代码省略
/// Context
var inputStrategy: TextFieldStrategy?
func validate() -> LoginSuccessCode {
if let strategy = inputStrategy {
let result = strategy.textFieldValidate(self)
/// 判断 textFieldValidate 返回的 code
if result.rawValue > LoginSuccessCode.Error.rawValue {
/// 失败
print(strategy.message)
return strategy.code /// 返回判断结果
}else {
/// 成功
return strategy.code /// 返回判断结果
}
}
return .Error
}
}
到这里,一个策略模式的雏形已经基本完成,Context就是BaseTextField基类,这个基类持有一个Strategy基类的属性:inputStrategy,并声明一个目标方法:validate()
接下去的工作就是根据不同的TextField子类去创建自己的策略子类,并在控制器中调用Context的目标方法即可。
4.3 创建Strategy子类
以输入用户名为例,创建一个用户名判断的策略
/// 用户名策略
class UserNameTextFieldStrategy: TextFieldStrategy {
let UserNameHud = "用户名必须是4~20位(仅支持字母、数字)"
override func textFieldValidate(_ textField: UITextField) -> LoginSuccessCode {
if let name = textField.text?.replacingOccurrences(of: " ", with: "") {
if name.length == 0 {
message = "请输入用户名"
}else if 正则判断(4~20位,仅支持字母、数字) {
code = .Username
return .Username
}else {
message = UserNameHud
}
}
code = .Error_Username
return .Error_Username
}
}
4.4 Controller中写法
以用户名为例,这里有在创建textField时,讲之前在基类中声明的属性绑定策略UserNameTextFieldStrategy,下面关于$0
的写法如果有疑惑的,可以参考一下:Swift - 属性 相关
/// 用户名
lazy var userNameTextField: BaseTextField = {
$0.delegate = self // UITextFieldDelegate
$0.inputStrategy = UserNameTextFieldStrategy() // 指定策略
return $0
}(BaseTextField())
上面的工作完成之后,只需要在合适的地方调用Context类(也就是BaseTextField类)中的目标方法:validate()就完成了。
func textFieldDidEndEditing(_ textField: UITextField) {
if textField is BaseTextField {
let code = (textField as! BaseTextField).validate()
// 处理 code
}
}
到此,回到文章开头的登录按钮点击方法,其中复杂的逻辑判断已经被封装到不同的textField对应的策略中,在点击登录时,你只需要对textFieldDidEndEditing中最后的code
进行判断就可以了。
五、坑与总结
如果你真的在项目中使用了策略模式,那么你可能会问它到底有什么好处?本身很容易的一件事,处理的这么复杂?
答:策略模式可以对控制器进行解耦,大大提升代码的扩展能力,假如你现在实现的是一个注册页面,需要填写姓名,性别,昵称等等各种信息,难道你写一个if else的金字塔吗?
当然,在我使用的过程中也遇到了问题,因为textFieldDidEndEditing在输入完成后返回结果,而且这个结果会在多次调用时不断变化,你需要做的是记录这些结果,方法很多:
我设计的是2个数组分别接收成功和失败的结果,成功时只保留状态,失败时保留状态和错误信息,方便弹出提醒。两个数组相互关联(即输入正确后,就把之前保存的错误状态移除,反之亦然)
听上去有些复杂,其实上面的所有逻辑大概只有10行左右的代码,而且只需写一次即可。之后无论你再添加多少个TextField都是一样的。此外,假如你的设计思路确认无误,你还可以把这部分代码抽离出来,一来二去,你的水平自然就提高啦^^
六、最后的最后
如何你遇到任何问题,欢迎留言一起讨论,我会尽力提供帮助。进步来源于不断尝试嘛