Simple Value Objects for Objective-C.
Value Objects are great for well-designed programs!
They're great for documenting data from an API or your internal application data, but are terrible to maintain.
When you add (or change) a property, you usually have to do:
- Add the
@property
- Add property to the constructor (optional)
- Add it to
-[mutableCopyWithZone:]
if it supports NSMutableCopying - Add it to
-[copyWithZone:]
if it supports NSCopying - Add it to
-[isEqual:]
to support equality for that modified property - Add it to
-[hash]
to support hashing - Add it to
-[initWithCoder:]
to support deserialization - Add it to
-[encodeWithCoder:]
to support serialization (NSSecureCoding) - Add it to
-[description]
for nice logging output - Add it to
-[debugDescription]
for nice object printouts in a debugger.
And forget about doing the right thing, and having both mutable and immutable versions of value objects like Apple's Foundation data structures... until now!
JKVValue simplifies your work to only setting the @property and constructor!
If you like pod'n it up:
pod "JKVValue", "~> 1.3.0"
Add to your Cartfile
:
github "jeffh/JKVValue" ~> 1.3
If you like git submodules add this project and adding it to your project:
git submodule add https://github.com/jeffh/JKVValue <Externals/JKVValue>
# checking out the stable version
cd <Externals/JKVValue>
git co tag v1.3.2
And then add the JKVValue static library and public headers for your dependencies.
There are two classes you can subclass, JKVValue and JKVMutableValue. Any properties you declare will automatically be detected and have their corresponding methods in NSCopying, NSMutableCopying, NSCoding, NSObject protocols supported automatically:
#import "JKVValue.h"
@interface MyPerson : JKVValue
@property (strong, nonatomic, readonly) NSString *firstName;
@property (strong, nonatomic, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end
@interface MyPerson ()
@property (strong, nonatomic, readwrite) NSString *firstName;
@property (strong, nonatomic, readwrite) NSString *lastName;
@end
@implementation MyPerson
- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName
{
if (self = [super init]) {
self.firstName = firstName;
self.lastName = lastName;
}
return self;
}
@end
That's it! All the cool methods are supported now:
MyPerson *person = [[MyPerson alloc] initWithFirstName:@"John" lastName:"Doe"];
// Copy returns same instance here, since it assumes immutability.
MyPerson *cloned = [person copy];
// this creates a new MyPerson instance, but still read only. We'll see how to change that later.
MyPerson *mutableClone = [person mutableCopy];
[person isEqual:mutableClone]; // => true
[NSSet setWithArray:@[person, mutableClone]]; // => set of 1 person
// get a nice description for free
[person description]; // => <MyPerson 0xdeadbeef firstName=John lastName=Doe>
// even in LLDB:
// > po person => <MyPerson 0xdeadbeef firstName=John lastName=Doe>
Want -[mutableCopy]
to use a different, actual, mutable class? Not a problem!
@class MyMutablePerson;
@implementation MyPerson
- (Class)JKV_mutableClass
{
return [MyMutablePerson class];
}
@end
@interface MyMutablePerson : MyPerson
@property (strong, nonatomic, readwrite) NSString *firstName;
@property (strong, nonatomic, readwrite) NSString *lastName;
@end
@implementation MyMutablePerson
@synthesize firstName;
@synthesize lastName;
- (BOOL)JKV_isMutable
{
return YES; // to hint to JKVValue that this concrete class is mutable.
}
- (Class)JKV_immutableClass
{
return [MyPerson class]; // tells JKVValue which class to create for the immutable variant
}
@end
Now you can switch between mutable and immutable variants like NSArray or NSDictionary:
// assuming MyPerson *person from above
MyMutablePerson *mutablePerson = [person mutableCopy];
MyPerson *immutablePerson = [mutablePerson copy];
If you prefer to use use only mutable objects, JKVMutableValue
is provided as
a convinence, it simply overrides JKVValue's -[JVK_isMutable]
to be YES
instead of its default of NO
.
It's worth noting that copy/mutableCopy is called on all properties if they support NSCopying or NSMutableCopying correspondingly.
You have a lot of fields for two value objects and you want to know why
-[isEqual:]
is failing? Use -[differenceToObject:]
:
MyPerson *person1 = [MyPerson new];
person1.firstName = @"John";
MyPerson *person2 = [MyPerson new];
person2.firstName = @"James";
[person1 differenceToObject:person2]; // => @{@"firstName": @[@"John", @"James"};
[person1 differenceToObject:@1]; // => @{@"class": @[[MyPerson class], NSClassFromString(@"__CFNSNumber")}
This library comes with a factory class, JKVFactory
, to produce pre-built
value objects easily. It's not explicitly tied to JKVValue
or
JKVMutableValue
, but is useful pattern for drying up the boilerplate of
generating pre-populated value objects.
For the simpliest case of having a value object where non of its properties are zero:
JKVFactory *personFactory = [JKVFactory factoryForClass:[MyPerson class]]
MyPerson *person = [personFactory object];
If you want more customization, it's recommended to inherit from JKVFactory
with a custom -[init]
method:
@interface MyPersonFactory : JKVFactory
@end
@implementation MyPersonFactory
- (id)init
{
return [super initWithClass:[MyPerson class] properties:@{@"firstName": @"John"}];
}
@end
// shortcut to [[MyPersonFactory new] object]
MyPerson *person = [MyPersonFactory buildObject];
Want a special object with custom properties?
[MyPersonFactory buildObjectWithProperties:@{@"firstName": @"James"}];
Need to nil out a property? Use [NSNull null]
:
[MyPersonFactory buildObjectWithProperties:@{@"lastName": [NSNull null]}];
JKVValue provides nice descriptions to NSArrays
, NSDictionaries
, and
NSSets
properties. It doesn't override the default implementations on those
classes by default. You can tell JKVValue to override them:
[JKVObjectPrinter swizzleContainers];
// you can undo the swizzling using [JKVObjectPrinter unswizzleContainers].
Potential strangeness due to implementation details:
- Properties that are not backed by an instance variable are ignored.
- Property assignment is done using KVC, which allows mutation of properties despite being marked as readonly. A private
-[init]
constructor is used by JKVValue to create the initial object. - weak properties are not used for equality (or hashing) since their life can be lost to a value object at any time.
- weak properties are assigned through copying, and are not copied (a copied weak would just be released immediately).
- weak properties are correctly encoded and decoded as conditional objects.
- Due to the Objective-C Runtime, weak readonly properties behave like strong properties for JKVValues.
- If you use the immutable-mutable pattern, you cannot have your constructor use the ivars directly, since the mutable version is overwriting them with its own.