前言
最近开始学习iOS设计模式了,越学习越发现自己代码功底的不足。要想成为一名优秀的程序员,任重而道远啊。不废话了,开始本文。
什么是策略模式?
在设计模式这本书中这样定义:
策略模式:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。本模式是的算法可独立于使用它的客户而变化。
设计模式其实就是给出一个公共的接口,继承了这个接口的类,实现自己相应的算法。当外部需要调用接口的时候,它们会根据情况进行判断从而进行合适的运算。它们的关系如以下类图所示:
其中,策略类中有一个计算的接口,但是计算可能有很多种,可能是计算长方形面积、圆形面积、三角形面积。但是我们所有的子类都继承自策略类,只需要重写策略类中的计算方法,就可以实现自己的计算规则。
iOS中的策略模式
假设有这么一种情况,我们需要做文本输入框的文本校验,输入全为数字或者全为字母。否则就会弹框提醒错误。按照一般思路,我们需要在- (void)textFieldDidEndEditing:(UITextField *)textField
中判断输入的字符类型。毫无疑问,这里会产生多个判断语句。如下:
- (void)textFieldDidEndEditing:(UITextField *)textField{
if (textField.text不是全为数字) {
NSLog(@"不是全为数字");
}
else if (textField.text不是全为字母) {
NSLog(@"不是全为字母");
}
else if.....
}
这样就会导致以后每有一种验证的改变,都需要来改变代码。有没有一种方式能够让以后的需求变更都不用做很大的程序变动呢?有,那就是策略模式!
我们可以抽象一个校验的策略基类,以后的字母校验、数字校验、邮箱校验等等各种校验都只需要继承自策略基类,然后各自实现自己的校验方法就行了。关系图如下:
各种校验类的实现
大致了解了策略模式的用处和做法,下面动手做一做。我们想要实现这么一个功能,声明三个一样的文本框,设置文本框校验器的类型,然后就能自动的进行校验。
校验基类
首先,我们先创建一个校验基类,在基类中给出接口,子类重写基类函数即可。
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
// 错误域名
static NSString *const InputValidationErrorDomain = @"InputValidationErrorDomain";
@interface InputValidator : NSObject
// 默认方法,子类重写此方法
- (BOOL)validateInput:(UITextField *)input error:(NSError **)error;
@end
#import "InputValidator.h"
@implementation InputValidator
- (BOOL)validateInput:(UITextField *)input error:(NSError *__autoreleasing *)error{
if (error) {
*error = nil;
}
return NO;
}
@end
其中的- (BOOL)validateInput:(UITextField *)input error:(NSError *__autoreleasing *)error
实例方法需要子类来重写以便实现自己的校验程序。使用统一的接口利于程序的解耦和应对不同的需求。同时,我们也可以隐藏与算法相关的数据结构。
字母校验类
有了基类之后,我们可以新建一个继承自InputValidator
的子类CharInputValidator
,实现自己的校验方法。
#import "CharInputValidator.h"
@implementation CharInputValidator
- (BOOL)validateInput:(UITextField *)input error:(NSError *__autoreleasing *)error
{
NSError *regError = nil;
// 使用正则表达式来验证输入
NSRegularExpression *regx = [NSRegularExpression regularExpressionWithPattern:@"^[a-zA-Z]*$" options:NSRegularExpressionAnchorsMatchLines error:®Error];
// - (NSUInteger)numberOfMatchesInString:(NSString *)string options:(NSMatchingOptions)options range:(NSRange)range;实例方法会返回匹配的字符串个数
NSUInteger numberOfMatches = [regx numberOfMatchesInString:[input text] options:NSMatchingAnchored range:NSMakeRange(0, [[input text] length])];
// 为0则说明没有匹配
if (numberOfMatches == 0) {
if (error!=nil) {
// 定义错误描述
NSString *description = NSLocalizedString(@"输入非法", @"");
NSString *reason = NSLocalizedString(@"字符只能包含字母", @"");
NSArray *objArrary = [NSArray arrayWithObjects:description,reason,nil];
NSArray *keyArray = [NSArray arrayWithObjects:NSLocalizedDescriptionKey,NSLocalizedFailureReasonErrorKey, nil];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjects:objArrary forKeys:keyArray];
*error = [NSError errorWithDomain:InputValidationErrorDomain code:1002 userInfo:userInfo];
}
return NO;
}
return YES;
}
@end
NSRegularExpression
顾名思义,是正则表达式的专用类,利用这个类我们可以进行一些正则表达式的验证。
数字校验类
数字校验类和字母校验类十分相似,只有其中的正则表达式和对错误的说明不同,其它完全相同。
#import "NumberInputValidator.h"
@implementation NumberInputValidator
- (BOOL)validateInput:(UITextField *)input error:(NSError *__autoreleasing *)error
{
NSError *regError = nil;
NSRegularExpression *regx = [NSRegularExpression regularExpressionWithPattern:@"^[0-9]*$" options:NSRegularExpressionAnchorsMatchLines error:®Error];
NSUInteger numberOfMatches = [regx numberOfMatchesInString:[input text] options:NSMatchingAnchored range:NSMakeRange(0, [[input text] length])];
if (numberOfMatches == 0) {
if (error!=nil) {
NSString *description = NSLocalizedString(@"输入非法", @"");
NSString *reason = NSLocalizedString(@"字符只能包含数字", @"");
NSArray *objArrary = [NSArray arrayWithObjects:description,reason,nil];
NSArray *keyArray = [NSArray arrayWithObjects:NSLocalizedDescriptionKey,NSLocalizedFailureReasonErrorKey, nil];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjects:objArrary forKeys:keyArray];
*error = [NSError errorWithDomain:InputValidationErrorDomain code:1002 userInfo:userInfo];
}
return NO;
}
return YES;
}
@end
重复很多的代码,我们可以抽取出来,这里涉及到了另外一种设计模式-——模板方法。此处讲的是策略模式,感兴趣的同学可以自己了解。
策略类的集成
想要UITextField
能够使用我们自己定义的验证器,就需要自定义一个TextField
。创建一个继承自:UITextField
的子类CustomTextField
。在类中声明一个属性inputValidator
和属性检查器validatorType
。前者用来设置校验器,后者用来设置校验器类型。
@class InputValidator;
typedef NS_OPTIONS(NSUInteger, LARValidatorState) {
LARNumberValidator = 1 << 0,
LARCharValidator = 1 << 1,
LARMailValidator = 1 << 2,
};
@interface CustomTextField : UITextField
@property (strong, nonatomic) InputValidator *inputValidator;
/** 检查器类型 */
@property (assign, nonatomic) NSUInteger validatorType;
- (BOOL)validateWithTarget:(UIViewController *)vc;
@end
在类中,我们声明了一个LARValidatorState的枚举,方便管理校验器的类型。
#import "CustomTextField.h"
#import "InputValidator.h"
static NSString * const LARCharTypeValidator = @"CharInputValidator";
static NSString * const LARNumberTypeValidator = @"NumberInputValidator";
static NSString * const LARMailTypeValidator = @"MailInputValidator";
@implementation CustomTextField
- (InputValidator *)inputValidator{
if (_inputValidator == nil) {
// 根据验证器类型名字建立一个验证器
Class clazz = [self makeClassWithName:[self LAR_getValidatorTye]];
_inputValidator = [[clazz alloc] init];
}
return _inputValidator;
}
// 在target上显示验证结果
- (BOOL)validateWithTarget:(UIViewController *)vc{
NSError *error = nil;
// 进行验证
BOOL validationResult = [self.inputValidator validateInput:self error:&error];
// 根据验证结果判断是否需要弹框,错误需要,反之则不需要
if (!validationResult) {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[error localizedDescription] message:[error localizedFailureReason] preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
[alertController addAction:okAction];
[vc presentViewController:alertController animated:YES completion:nil];
}
return validationResult;
}
// 获取验证器类型
- (NSString *)LAR_getValidatorTye{
switch (self.validatorType) {
case LARCharValidator:
return LARCharTypeValidator;
break;
case LARNumberValidator:
return LARNumberTypeValidator;
break;
default:
return nil;
break;
}
}
// 动态创建类
- (Class)makeClassWithName:(NSString *)name{
Class clazz = NSClassFromString(name);
return clazz;
}
@end
在我们自定义的CustomTextField
类中,我们可以根据外部设置的校验器类型,来动态的设置校验器的类,进行相关校验。如果以后需求变更需要改变校验器和其算法的时候,改动就会很小了。在下文,我们会进行实践。
验证校验器
实现了自己的文本框后,我们试试看能不能工作。在storyboard
中,拖入两个文本框,设置类为自定义的CustomTextField
类。
#import "ViewController.h"
#import "CustomTextField.h"
@interface ViewController ()<UITextFieldDelegate>
@property (weak, nonatomic) IBOutlet CustomTextField *numberTextField;
@property (weak, nonatomic) IBOutlet CustomTextField *charTextField;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 设置验证器类型
_numberTextField.validatorType = LARNumberValidator;
_charTextField.validatorType = LARCharValidator;
// 设置代理
_numberTextField.delegate = self;
_charTextField.delegate = self;
}
- (void)textFieldDidEndEditing:(UITextField *)textField{
if ([textField isKindOfClass:[CustomTextField class]]) {
[(CustomTextField *)textField validateWithTarget:self];
}
}
@end
我们设置了验证器类型和代理后,就可以进行相关验证。运行结果如下:
应对需求变更
如果我们需要添加一个邮箱校验类怎么办?
- 我们需要建立一个继承自
InputValidator
的子类MailInputValidator
,在其中将正则表达式改为^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$
就行了。 - 添加
CustomTextField
类中的枚举和获取验证器类型。
修改完成之后,我们再向storyboard
中拖入一个UITextField
,并让它继承自CustomTextField
。添加如下代码并运行
// 设置代理
_mailTextField.delegate = self;
// 设置验证器类型
_mailTextField.validatorType = LARMailValidator;
最后
在一下情景我们可以考虑使用这模式:
- 一个类在其操作中使用多个添加语句来定义许多行为。我们可以把相关的条件分支移动到他们自己的策略类中
- 需要算法的各种变体。
- 需要避免把复杂的、与算法相关的数据结构暴露给客户端。
参考
《Objective-C 编程之道 - iOS设计模式解析》
本文Demo:iOS策略模式Demo