MENU

iOS设计模式之策略模式

March 16, 2017 • Read: 365 • iOS

前言

最近开始学习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:&regError];
    // - (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:&regError];
    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

我们设置了验证器类型和代理后,就可以进行相关验证。运行结果如下:

运行结果

应对需求变更

如果我们需要添加一个邮箱校验类怎么办?

  1. 我们需要建立一个继承自InputValidator的子类MailInputValidator,在其中将正则表达式改为^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$就行了。
  2. 添加CustomTextField类中的枚举和获取验证器类型。

修改完成之后,我们再向storyboard中拖入一个UITextField,并让它继承自CustomTextField。添加如下代码并运行

// 设置代理
_mailTextField.delegate = self;
// 设置验证器类型
_mailTextField.validatorType   = LARMailValidator;

运行结果

最后

在一下情景我们可以考虑使用这模式:

  1. 一个类在其操作中使用多个添加语句来定义许多行为。我们可以把相关的条件分支移动到他们自己的策略类中
  2. 需要算法的各种变体。
  3. 需要避免把复杂的、与算法相关的数据结构暴露给客户端。

参考

《Objective-C 编程之道 - iOS设计模式解析》

本文Demo:iOS策略模式Demo

Tags: iOS开发
Archives QR Code
QR Code for this page
Tipping QR Code