通过重构实现模板方法模式(Template Method ) 模式篇

书接上文

上次使用了一些简单地重构方法,得到的最终代码如下:

.h 未做改动,文件省略
.m 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#import "GUIResetPasswordController.h"
#import "GUIChangePasswordController.h"
#import <SMS_SDK/SMS_SDK.h>
#import "GUIHeader.h"



@interface GUIResetPasswordController ()

@property (nonatomic, weak) UITextField *phonenumberTextfield;
@property (nonatomic, weak) UITextField *verifyCodeTextfield;
@property (nonatomic, weak) UIButton *getVerifyCodeButton;
@property (nonatomic, weak) UIButton *submitButton;

@end

static NSInteger secondsCoutDown;
static NSTimer *countDownTimer;

static CGFloat const kLeftViewHeight = 40;
static CGFloat const kLeftViewWidth = 40;


@implementation GUIResetPasswordController





#pragma mark -life cycle
- (void)viewDidLoad {
[super viewDidLoad];

self.view.backgroundColor = kcMainGrayBackgroud;

[self setupPhoneNumberTextField];

[self setupVerifiedCodeTextField];

[self setupGetVerifiyCodeButton];

[self setupSubmitButton];


}

- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];

WSE(ws);

static const CGFloat kTextFieldHeight = 50;
[self.phonenumberTextfield mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(ws.view);
make.leading.mas_equalTo(ws.view);
make.trailing.mas_equalTo(ws.view);
make.height.mas_equalTo(kTextFieldHeight);
}];

[self.verifyCodeTextfield mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(ws.phonenumberTextfield.mas_bottom).with.offset(1);
make.leading.mas_equalTo(ws.view);
make.trailing.mas_equalTo(ws.view).with.offset(-GUIScreenWidth/3);
make.height.mas_equalTo(kTextFieldHeight);
}];

[self.getVerifyCodeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(GUIScreenWidth/3, kTextFieldHeight));
make.leading.mas_equalTo(ws.verifyCodeTextfield.mas_trailing);
make.bottom.mas_equalTo(ws.verifyCodeTextfield);
}];

[self.submitButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(GUIScreenWidth/3, 30));
make.leading.mas_equalTo((GUIScreenWidth-(GUIScreenWidth/3))/2);
make.top.mas_equalTo(ws.verifyCodeTextfield.mas_bottom).with.offset(60);
}];


}

#pragma mark - delegate

#pragma mark -event response
- (void)getTheCodeAgain {

secondsCoutDown = 60;
self.getVerifyCodeButton.enabled = NO;
countDownTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeDown) userInfo:nil repeats:YES];

}

- (void)nextStep:(UIButton *)button {

[SMS_SDK commitVerifyCode:self.verifyCodeTextfield.text result:^(enum SMS_ResponseState state) {
if (state == SMS_ResponseStateSuccess) {
[MBProgressHUD showMessage:@"正在验证 ... "];
[self.navigationController pushViewController:[[GUIChangePasswordController alloc]init] animated:YES];
[MBProgressHUD hideHUD];
} else {
[MBProgressHUD showError:@"验证失败"];
}
}];
#warning temp code ,remember to delete
[self.navigationController pushViewController:[[GUIChangePasswordController alloc]init] animated:YES];

}

#pragma mark - share sdk sms
- (void)getVerifyCode:(UIButton *)button {

if (self.phonenumberTextfield.text == nil) {
[MBProgressHUD showError:@"请先填写手机号码"];
return;
}


NSString *phoneNumber = self.phonenumberTextfield.text;

#warning temp code ,remember to delete
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = [paths lastObject];
NSString *filePath = [documentPath stringByAppendingPathComponent:@"resetPhoneNumber.plist"];
[phoneNumber writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];

[SMS_SDK getVerificationCodeBySMSWithPhone:phoneNumber zone:@"86" result:^(SMS_SDKError *error) {
//save phone number to file
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = [paths lastObject];
NSString *filePath = [documentPath stringByAppendingPathComponent:@"resetPhoneNumber.plist"];
[phoneNumber writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}];

[self getTheCodeAgain];
}

#pragma mark -private methods

- (void)timeDown {
secondsCoutDown--;
if (secondsCoutDown == 0) {
[countDownTimer invalidate];
self.getVerifyCodeButton.enabled = YES;
}
[self.getVerifyCodeButton setTitle:[NSString stringWithFormat:@"请等待 %ld 秒", (long)secondsCoutDown] forState:UIControlStateDisabled];
}

- (UIImageView *)textLeftViewWithImage:(UIImage *)leftImage {
UIImageView *containerView = [[UIImageView alloc] init];
UIImageView *iconView = [[UIImageView alloc]init];
iconView.image = leftImage;
[containerView addSubview:iconView];

UIImageView *seperatorView = [[UIImageView alloc]init];
seperatorView.backgroundColor = [UIColor redColor];
[containerView addSubview:seperatorView];
//set frame
containerView.bounds = CGRectMake(0, 0, kLeftViewWidth+20, kLeftViewWidth);

CGFloat iconViewWH = 20;
CGFloat iconViewX = (kLeftViewWidth - iconViewWH)/2;
CGFloat iconViewY = (kLeftViewHeight - iconViewWH)/2;

iconView.frame = CGRectMake(iconViewX, iconViewY, iconViewWH, iconViewWH);

CGFloat seperatorW = 1;
CGFloat seperatorH = 20;
CGFloat seperatorX = kLeftViewWidth-seperatorW;
CGFloat seperatorY = (kLeftViewHeight-seperatorH)/2;

seperatorView.frame = CGRectMake(seperatorX, seperatorY, seperatorW, seperatorH);

return containerView;
}

- (void)setupPhoneNumberTextField {

UITextField *phonenumberTextfield = [[UITextField alloc]init];
phonenumberTextfield.leftView = [self textLeftViewWithImage:[UIImage imageNamed:@"zhuce_dianhua"]];
phonenumberTextfield.leftViewMode = UITextFieldViewModeAlways;
phonenumberTextfield.placeholder = @"您的手机号";
phonenumberTextfield.backgroundColor = [UIColor whiteColor];
phonenumberTextfield.clearButtonMode = UITextFieldViewModeWhileEditing;
[self.view addSubview:phonenumberTextfield];
self.phonenumberTextfield = phonenumberTextfield;

}

- (void)setupVerifiedCodeTextField {

UITextField *verifyCodeTextfield = [[UITextField alloc]init];
verifyCodeTextfield.leftView = [self textLeftViewWithImage:[UIImage imageNamed:@"zhuce_yanzhengma"]];
verifyCodeTextfield.leftViewMode = UITextFieldViewModeAlways;
verifyCodeTextfield.placeholder = @"输入验证码";
verifyCodeTextfield.backgroundColor = [UIColor whiteColor];
[self.view addSubview:verifyCodeTextfield];
self.verifyCodeTextfield = verifyCodeTextfield;


}

- (void)setupGetVerifiyCodeButton {

UIButton *getVerifyCodeButton = [[UIButton alloc]init];
[getVerifyCodeButton setTitle:@"获取验证码" forState:UIControlStateNormal];
[getVerifyCodeButton setBackgroundColor:kcMainRed];
[getVerifyCodeButton setTitleColor:kcMainGrayBackgroud forState:UIControlStateNormal];
[getVerifyCodeButton addTarget:self action:@selector(getVerifyCode:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:getVerifyCodeButton];
self.getVerifyCodeButton = getVerifyCodeButton;

}

- (void)setupSubmitButton {

UIButton *submitButton = [[UIButton alloc] init];
[submitButton setTitle:@"下一步" forState:UIControlStateNormal];
[submitButton setBackgroundColor:[UIColor whiteColor]];
submitButton.layer.cornerRadius = 3;
[submitButton setTitleColor:kcMainRed forState:UIControlStateNormal];
[submitButton addTarget:self action:@selector(nextStep:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:submitButton];
self.submitButton = submitButton;

}

#pragma mark -getters and setters



@end

模板方法

模板方法设计模式是最常用来避免直接复制代码的设计模式.

1. 隔离变化

所谓的隔离变化,概括起来说就是:

  • 对比两份或者多份代码,找出不同点
  • 抽取出方法 (要求子类必须重写)
  • 将公共代码作为模板类.
  • 子类实现差异方法

2.创建模板类

新建模板控制器 GUIVaiidationTemplateController

在编写模板类的时候,我的最佳实践步骤是:

  • 版本控制保存当前状态
  • 拷贝重构好的代码到模板类
  • 原控制器继承模板类
  • 将差异抽取,并在子类中分别实现

拷贝完毕的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228

#import "GUIVaiidationTemplateController.h"
#import <SMS_SDK/SMS_SDK.h>
#import "GUIHeader.h"


static NSInteger secondsCoutDown;
static NSTimer *countDownTimer;

static CGFloat const kLeftViewHeight = 40;
static CGFloat const kLeftViewWidth = 40;


@interface GUIVaiidationTemplateController ()

@property (nonatomic, weak) UITextField *phonenumberTextfield;
@property (nonatomic, weak) UITextField *verifyCodeTextfield;
@property (nonatomic, weak) UIButton *getVerifyCodeButton;
@property (nonatomic, weak) UIButton *submitButton;

@end


@implementation GUIVaiidationTemplateController


#pragma mark -life cycle
- (void)viewDidLoad {
[super viewDidLoad];

self.view.backgroundColor = kcMainGrayBackgroud;

[self setupPhoneNumberTextField];

[self setupVerifiedCodeTextField];

[self setupGetVerifiyCodeButton];

[self setupSubmitButton];


}

- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];

WSE(ws);

static const CGFloat kTextFieldHeight = 50;
[self.phonenumberTextfield mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(ws.view);
make.leading.mas_equalTo(ws.view);
make.trailing.mas_equalTo(ws.view);
make.height.mas_equalTo(kTextFieldHeight);
}];

[self.verifyCodeTextfield mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(ws.phonenumberTextfield.mas_bottom).with.offset(1);
make.leading.mas_equalTo(ws.view);
make.trailing.mas_equalTo(ws.view).with.offset(-GUIScreenWidth/3);
make.height.mas_equalTo(kTextFieldHeight);
}];

[self.getVerifyCodeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(GUIScreenWidth/3, kTextFieldHeight));
make.leading.mas_equalTo(ws.verifyCodeTextfield.mas_trailing);
make.bottom.mas_equalTo(ws.verifyCodeTextfield);
}];

[self.submitButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(GUIScreenWidth/3, 30));
make.leading.mas_equalTo((GUIScreenWidth-(GUIScreenWidth/3))/2);
make.top.mas_equalTo(ws.verifyCodeTextfield.mas_bottom).with.offset(60);
}];


}

#pragma mark - delegate

#pragma mark -event response
- (void)getTheCodeAgain {

secondsCoutDown = 60;
self.getVerifyCodeButton.enabled = NO;
countDownTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeDown) userInfo:nil repeats:YES];

}

- (void)nextStep:(UIButton *)button {

[SMS_SDK commitVerifyCode:self.verifyCodeTextfield.text result:^(enum SMS_ResponseState state) {
if (state == SMS_ResponseStateSuccess) {
[MBProgressHUD showMessage:@"正在验证 ... "];
[self.navigationController pushViewController:[[GUIChangePasswordController alloc]init] animated:YES];
[MBProgressHUD hideHUD];
} else {
[MBProgressHUD showError:@"验证失败"];
}
}];
#warning temp code ,remember to delete
[self.navigationController pushViewController:[[GUIChangePasswordController alloc]init] animated:YES];

}

#pragma mark - share sdk sms
- (void)getVerifyCode:(UIButton *)button {

if (self.phonenumberTextfield.text == nil) {
[MBProgressHUD showError:@"请先填写手机号码"];
return;
}


NSString *phoneNumber = self.phonenumberTextfield.text;

#warning temp code ,remember to delete
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = [paths lastObject];
NSString *filePath = [documentPath stringByAppendingPathComponent:@"resetPhoneNumber.plist"];
[phoneNumber writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];

[SMS_SDK getVerificationCodeBySMSWithPhone:phoneNumber zone:@"86" result:^(SMS_SDKError *error) {
//save phone number to file
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = [paths lastObject];
NSString *filePath = [documentPath stringByAppendingPathComponent:@"resetPhoneNumber.plist"];
[phoneNumber writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}];

[self getTheCodeAgain];
}

#pragma mark -private methods

- (void)timeDown {
secondsCoutDown--;
if (secondsCoutDown == 0) {
[countDownTimer invalidate];
self.getVerifyCodeButton.enabled = YES;
}
[self.getVerifyCodeButton setTitle:[NSString stringWithFormat:@"请等待 %ld 秒", (long)secondsCoutDown] forState:UIControlStateDisabled];
}

- (UIImageView *)textLeftViewWithImage:(UIImage *)leftImage {
UIImageView *containerView = [[UIImageView alloc] init];
UIImageView *iconView = [[UIImageView alloc]init];
iconView.image = leftImage;
[containerView addSubview:iconView];

UIImageView *seperatorView = [[UIImageView alloc]init];
seperatorView.backgroundColor = [UIColor redColor];
[containerView addSubview:seperatorView];
//set frame
containerView.bounds = CGRectMake(0, 0, kLeftViewWidth+20, kLeftViewWidth);

CGFloat iconViewWH = 20;
CGFloat iconViewX = (kLeftViewWidth - iconViewWH)/2;
CGFloat iconViewY = (kLeftViewHeight - iconViewWH)/2;

iconView.frame = CGRectMake(iconViewX, iconViewY, iconViewWH, iconViewWH);

CGFloat seperatorW = 1;
CGFloat seperatorH = 20;
CGFloat seperatorX = kLeftViewWidth-seperatorW;
CGFloat seperatorY = (kLeftViewHeight-seperatorH)/2;

seperatorView.frame = CGRectMake(seperatorX, seperatorY, seperatorW, seperatorH);

return containerView;
}

- (void)setupPhoneNumberTextField {

UITextField *phonenumberTextfield = [[UITextField alloc]init];
phonenumberTextfield.leftView = [self textLeftViewWithImage:[UIImage imageNamed:@"zhuce_dianhua"]];
phonenumberTextfield.leftViewMode = UITextFieldViewModeAlways;
phonenumberTextfield.placeholder = @"您的手机号";
phonenumberTextfield.backgroundColor = [UIColor whiteColor];
phonenumberTextfield.clearButtonMode = UITextFieldViewModeWhileEditing;
[self.view addSubview:phonenumberTextfield];
self.phonenumberTextfield = phonenumberTextfield;

}

- (void)setupVerifiedCodeTextField {

UITextField *verifyCodeTextfield = [[UITextField alloc]init];
verifyCodeTextfield.leftView = [self textLeftViewWithImage:[UIImage imageNamed:@"zhuce_yanzhengma"]];
verifyCodeTextfield.leftViewMode = UITextFieldViewModeAlways;
verifyCodeTextfield.placeholder = @"输入验证码";
verifyCodeTextfield.backgroundColor = [UIColor whiteColor];
[self.view addSubview:verifyCodeTextfield];
self.verifyCodeTextfield = verifyCodeTextfield;


}

- (void)setupGetVerifiyCodeButton {

UIButton *getVerifyCodeButton = [[UIButton alloc]init];
[getVerifyCodeButton setTitle:@"获取验证码" forState:UIControlStateNormal];
[getVerifyCodeButton setBackgroundColor:kcMainRed];
[getVerifyCodeButton setTitleColor:kcMainGrayBackgroud forState:UIControlStateNormal];
[getVerifyCodeButton addTarget:self action:@selector(getVerifyCode:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:getVerifyCodeButton];
self.getVerifyCodeButton = getVerifyCodeButton;

}

- (void)setupSubmitButton {

UIButton *submitButton = [[UIButton alloc] init];
[submitButton setTitle:@"下一步" forState:UIControlStateNormal];
[submitButton setBackgroundColor:[UIColor whiteColor]];
submitButton.layer.cornerRadius = 3;
[submitButton setTitleColor:kcMainRed forState:UIControlStateNormal];
[submitButton addTarget:self action:@selector(nextStep:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:submitButton];
self.submitButton = submitButton;

}

#pragma mark -getters and setters



@end

好的,现在开始对比两个子类. 两个子类的代码和上面的代码类似,就不上代码了.

以上面的代码为例,首先提取变化.

我粘贴完毕之后,发现在跳转控制器的地方,需要有这个控制器 GUIChangePasswordController ,而另一个注册的子类没有,所以这就是一个变化,应该将这个变化放到一个方法中.

在子类和模板类的方法有所不同, 模板类做的只是把方法提取出来,同时要求子类必须实现,自己不需要写具体的实现.

模板类中原代码:

1
2
3
4
5
6
7
8
9
[SMS_SDK commitVerifyCode:self.verifyCodeTextfield.text result:^(enum SMS_ResponseState state) {
if (state == SMS_ResponseStateSuccess) {
[MBProgressHUD showMessage:@"正在验证 ... "];
[self.navigationController pushViewController:[[GUIChangePasswordController alloc]init] animated:YES];
[MBProgressHUD hideHUD];
} else {
[MBProgressHUD showError:@"验证失败"];
}
}];

隔离变化后代码:

1
2
3
4
5
6
7
8
9
[SMS_SDK commitVerifyCode:self.verifyCodeTextfield.text result:^(enum SMS_ResponseState state) {
if (state == SMS_ResponseStateSuccess) {
[MBProgressHUD showMessage:@"正在验证 ... "];
[self.navigationController pushViewController:[self pushTargetController] animated:YES];
[MBProgressHUD hideHUD];
} else {
[MBProgressHUD showError:@"验证失败"];
}
}];

重头戏来了 ! OC中没有任何关键字能要求子类必须重写父类的方法.那么很多的设计模式是不是对OC就不适用了呢 ? 其实还是有方法的.

在OC 2.0之后 , 大家都用 @throw了,更符合规范,但是1.0的 NSAssert 更加简洁,谁优谁劣,自己选择.

对应的要求子类必须实现的代码:

1
2
3
4
5
-(id)pushTargetController{
//不写实现,子类必须重写此方法
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"方法: %@ 必须被子类实现",NSStringFromSelector(_cmd)] userInfo:nil];

}

可以看到,写了异常代码之后,返回值也不是必须的了,爽 !

到两个子类中实现跳转代码,为了方便,我称呼为 子类A 和 子类B

子类A

1
2
3
4
5
-(id)pushTargetController{

return [[GUIChangePasswordController alloc]init];

}

子类B

1
2
3
4
5
-(id)pushTargetController{

return [[GUISignupPasswordController alloc]init];

}

最后一步

接下来的工作就比较简单暴力了. 把两个子类中完全相同的代码删掉.
将差异的部分提取到父类,抽取方法. 重复进行.

收尾

运行,测试.

子类A完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import "GUIResetPasswordController.h"
#import "GUIChangePasswordController.h"


@implementation GUIResetPasswordController

#pragma mark -life cycle
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"重置密码";
}

#pragma mark - delegate

#pragma mark -event response

#pragma mark -private methods

-(id)pushTargetController{

return [[GUIChangePasswordController alloc]init];

}
#pragma mark -getters and setters

@end

子类B 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@implementation GUISignupPasswordController

-(void)viewDidLoad
{
[super viewDidLoad];
self.title = @"用户注册";

}


#pragma mark -life cycle

#pragma mark - delegate

#pragma mark -event response

#pragma mark -private methods
-(id)pushTargetController{

return [[GUISignupPasswordController alloc]init];

}

#pragma mark -getters and setters
@end

清爽吧 ?

结语

注意点

  • 要养成随手编译的好习惯,尤其是在重构代码的过程中,随时发现问题.
  • 善用git svn等版本控制工具,它们是你的后悔药.

不要再拷贝代码了

就如刚才的例子,如果有新需求,需要更改样式或者其他内容,那我需要分别修改两个类.这还只是两个类,如果是10个,20个呢 ? 程序员加班多得原因之一,就是为自己的不规范负责.

使用了模板方法,其实只要维护一份代码即可,以后增加新的类 ,只需要继承模板 ,维护起来非常方便.