前置き
この記事は松村 Advent Calendar 2016の18日の記事になります。
今どきObjective-Cかよと言うツッコミが聞こえてきますが、まだ自分が携わっている開発ではバリバリ利用しています。(Swift使いたい)
概要
Objective-Cでもimmutableなモデルを作りたい!!
なぜ?
画面に何かを表示するときは、View(ViewController)にデータを渡して表示するかと思います。
上記のようにViewModelの値を変更するのはロジッククラスだけであり、ViewControllerやViewは変更する必要がないし変更して欲しくないです。
つまりViewModelは表示するためだけに使うデータなのでimmutableであってほしいと考えています。
@implementation MotorcycleSpecView
/**
* Viewを更新するときはこのメソッドを呼べば良いようにしたい
* 何か表示したい情報が増えたとしてもこのメソッドのIFは変わらない、
* 変わるのはViewModel(MotorcycleSpecViewModel)の中身と`UILabel`などのUI要素のみ
*/
- (void)updateWithMotorcycleSpecViewModel:(MotorcycleSpecViewModel*)viewModel {
// この辺のviewModelの値は参照のみであって欲しい、書き換えたくない
self.machineNameLbl.text = viewModel.name;
self.displacementLbl.text = viewModel.formatedDisplacementStr;
self.manufacturerLbl.text = viewModel.manufacturer;
}
@end
immutableでないモデルだと何が起きるのか?
immutableでないモデルの例です。
#import <Foundation/Foundation.h>
/**
* バイク画面のViewModel
*/
@interface MotorcycleSpecViewModel : NSObject
/** バイク名 */
@property (nonatomic) NSString * _Nonnull name;
/**
* CCの文字列を付加した排気量の文字列
* 例) "1000CC"
*/
@property (nonatomic) NSString * _Nonnull formatedDisplacementStr;
/** 製造元 */
@property (nonatomic) NSString * _Nonnull manufacturer;
@end
@implementation MotorcycleSpecViewModel
@end
このViewModel自身は簡潔で見通しも良く見えます。
ただしimmutableではないので各property
はいつでも誰でも書き換えることが可能です。
こんな感じに。
@implementation MotorcycleSpecView
- (void)updateWithMotorcycleSpecViewModel:(MotorcycleSpecViewModel*)viewModel {
if([name isEqualToString:@"Kawasaki"]){
// こんな風にView側で書き換えられちゃう!!
viewModel.name = [NSString stringWithFormat:@"%@(´L_` )", viewModel.name];
}
self.machineNameLbl.text = viewModel.name;
self.displacementLbl.text = viewModel.formatedDisplacementStr;
self.manufacturerLbl.text = viewModel.manufacturer;
}
@end
このViewModelを作った人はViewで値を書き換えることはしないかもしれません。
しかし作った人がプロジェクトを去り、後を引き継いだ人が見るといつでも誰でも書き換えていいように見えてしまいます。(引き継いだ人のスキルが低ければ低いほど)
これがimmutableであったなら書き換えが出来ないので、作った人がいなくてViewでは値を書き換えてはいけないのだと言う思想を感じることができます。
immutableなモデルの作り方
長くなりましたが実際のimmutableなモデルの作り方です。
- パターン1
- パターン2(パターン1の改良版)
パターン1
パターン1は初期化メソッドのみで値をセットし、外向けのproperty
をreadonly
にすることでimmutableを実現しています。
#import <Foundation/Foundation.h>
/**
* バイク画面のViewModel
*/
@interface MotorcycleSpecViewModel : NSObject <NSCopying>
/**
* 初期化
* @param name バイク名
* @param displacement 排気量
* @param manufacturer 製造元
*/
- (_Nullable instancetype)initWithName:( NSString* _Nonnull )name displacement:(NSUInteger)displacement manufacturer:( NSString* _Nonnull )manufacturer;
/** バイク名 */
@property (nonatomic, readonly) NSString * _Nonnull name;
/**
* CCの文字列を付加した排気量の文字列
* 例) "1000CC"
*/
@property (nonatomic, readonly) NSString * _Nonnull formatedDisplacementStr;
/** 製造元 */
@property (nonatomic, readonly) NSString * _Nonnull manufacturer;
@end
#import "MotorcycleSpecViewModel.h"
@interface MotorcycleSpecViewModel ()
/** バイク名(保持データ) */
@property (nonatomic, readwrite) NSString * _Nonnull inner_name;
/** 排気量(保持データ) */
@property (nonatomic, readwrite) NSUInteger inner_displacement;
/** 製造元(保持データ) */
@property (nonatomic, readwrite) NSString * _Nonnull inner_manufacturer;
@end
@implementation MotorcycleSpecViewModel
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone {
MotorcycleSpecViewModel *clone = [[[self class] allocWithZone:zone] init];
if(clone){
clone.inner_name = self.inner_name;
clone.inner_displacement = self.inner_displacement;
clone.inner_manufacturer = self.inner_manufacturer;
}
return clone;
}
#pragma mark - Public
- (instancetype)initWithName:(NSString*)name displacement:(NSUInteger)displacement manufacturer:(NSString*)manufacturer {
self = [super init];
if(self){
_inner_name = name;
_inner_displacement = displacement;
_inner_manufacturer = manufacturer;
}
return self;
}
#pragma mark - Accessor
- (NSString*)name {
return _inner_name;
}
- (NSString*)formatedDisplacementStr {
// CCを追加
return [NSString stringWithFormat:@"%@CC",[@(_inner_displacement) stringValue]];
}
- (NSString*)manufacturer {
return _inner_manufacturer;
}
@end
使い方
// 初期化
MotorcycleSpecViewModel *vm = [[MotorcycleSpecViewModel alloc] initWithName:@"トリッカー" displacement:250 manufacturer:@"YAMAHA"];
// 参照
NSLog(@"バイク名:%@",vm.name);
// 書き換えはできない
// コンパイルエラーになるはず
vm.name = @"Z1000";
// ViewModelの更新
// 一部のみ更新したい(排気量のみ変更)
MotorcycleSpecViewModel *newVm = [[MotorcycleSpecViewModel alloc] initWithName:vm.name displacement:249 manufacturer:vm.manufacturer:vm];
メリット
- 公開している
property
はreadonly
に出来るのでimmutable(完璧でないけど)
デメリット
- 一部のデータのみのViewModelの値を書き換えがめんどくさい
- データが増えるたびにinitメソッドを書き換えないといけない
パターン2
パターン1の改良版です。
パターン1との違いは初期化用のクラスを作成してそれを用いて初期化している点です。
#import <Foundation/Foundation.h>
/**
* MotorcycleSpecViewModelの初期化で利用するクラス
*/
@interface MotorcycleSpecViewModelParam : NSObject <NSCopying>
/** バイク名 */
@property (nonatomic, strong) NSString * _Nonnull param_name;
/** 排気量 */
@property (nonatomic, assign) NSUInteger param_displacement;
/** 製造元 */
@property (nonatomic, strong) NSString * _Nonnull param_manufacturer;
@end
/**
* バイク画面のViewModel
*/
@interface MotorcycleSpecViewModel : NSObject <NSCopying>
/** コピーされた内部データ */
@property (nonatomic, readonly) MotorcycleSpecViewModelParam * _Nonnull copiedInnerData;
/**
* 初期化
* @param specParam 初期化で利用する引数クラス
*/
- (_Nullable instancetype)initWithSpecParam:(MotorcycleSpecViewModelParam * _Nullable)specParam;
/** バイク名 */
@property (nonatomic, readonly) NSString * _Nonnull name;
/**
* CCの文字列を付加した排気量の文字列
* 例) "1000CC"
*/
@property (nonatomic, readonly) NSString * _Nonnull formatedDisplacementStr;
/** 製造元 */
@property (nonatomic, readonly) NSString * _Nonnull manufacturer;
@end
#import "MotorcycleSpecViewModel.h"
@implementation MotorcycleSpecViewModelParam
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone {
MotorcycleSpecViewModelParam *clone = [[[self class] allocWithZone:zone] init];
if(clone){
clone.param_name = self.param_name;
clone.param_displacement = self.param_displacement;
clone.param_manufacturer = self.param_manufacturer;
}
return clone;
}
@end
@interface MotorcycleSpecViewModel ()
/** 保持データ */
@property (nonatomic, strong) MotorcycleSpecViewModelParam *innerData;
@end
@implementation MotorcycleSpecViewModel
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone {
MotorcycleSpec2 *clone = [[[self class] allocWithZone:zone] init];
if(clone){
clone.innerData = [self.innerData copy];
}
return clone;
}
#pragma mark - Public
- (instancetype)initWithSpecParam:(MotorcycleSpecViewModelParam *)specParam {
self = [super init];
if(self){
_innerData = [specParam copy];
}
return self;
}
#pragma mark - Accessor
- (NSString*)name {
return _innerData.param_name;
}
- (NSString*)formatedDisplacementStr {
// CCを追加
return [NSString stringWithFormat:@"%@CC",[@(_innerData.param_displacement) stringValue]];
}
- (NSString*)manufacturer {
return _innerData.param_manufacturer;
}
- (MotorcycleSpecParam*)copiedInnerData{
return [_innerData copy];
}
@end
使い方
// 初期化
MotorcycleSpecViewModelParam *initParam = [[MotorcycleSpecViewModelParam alloc] init];
{
initParam.param_name = @"トリッカー";
initParam.param_displacement = 250;
initParam.param_manufacturer = @"YAMAHA";
}
MotorcycleSpecViewModel *vm = [[MotorcycleSpecViewModel alloc] initWithSpecParam:initParam];
// 参照
NSLog(@"バイク名:%@",vm.name);
// 書き換えはできない
// コンパイルエラーになるはず
vm.name = @"Z1000";
// ViewModelの更新
// 一部のみ更新したい
MotorcycleSpecViewModelParam *newInitParam = vm.copiedInnerData;
{
// 排気量のみ変更
newInitParam.param_displacement = 249;
}
MotorcycleSpecViewModel *newVm = [[MotorcycleSpecViewModel alloc] newInitParam];
メリット
- 公開している
property
はreadonly
に出来るのでimmutable(完璧でないけど) - データを増やしたい場合は
MotorcycleSpecParam
に引数を足せばよく、initWithSpecParam
メソッドを変更しなくて良い - 値を書き換える場合は
cloneInnerData
でMotorcycleSpecParam
を取り出し、変えたい部分だけ変えてinitWithSpecParam:
にて再生成すれば良い
デメリット
- 値を書き換えるのがめんどくさい(パターン1より簡単だけど)
- コード量が多い
まとめ
上記のいずれかのパターンを利用することでimmutableっぽい感じになりました。
これがベストプラクティスかと言うとそんな感じもしないのであくまで参考までにと言う感じです。
楽しいiOS開発をやっていきましょう( ・ㅂ・)و ̑̑