Implementing Value Objects in Objective C
Value Objects are objects that hold
simple data. This article is about creating such value objects. I use them a lot
in my code, because they are robust and keep the code simple. Note that it’s not
about NSValue
, but about simple objects with simple data.
Implementing value objects should be easy, but there are some slightly tricky bits. So let’s look at the requirements:
-
We want to create value objects quickly (i.e. an
initWith:
) -
The created objects should be immutable
-
The created objects should be equal when they have equal values
Suppose we want to create Person
objects with properties name
andbirthDate
, then our interface looks like this:
@interface Person : NSObject
- (id)initWithName:(NSString*)name birthDate:(NSDate*)birthDate;
@property (nonatomic, copy, readonly) NSString* name;
@property (nonatomic, strong, readonly) NSDate* birthDate;
@end
The important thing to notice here is that the properties are readonly
.
However, the modern runtime still generates instance variables for us, that are
prefixed by a _
. Our implementation looks like this:
@implementation Person
- (id)initWithName:(NSString*)name birthDate:(NSDate*)birthDate {
self = [super init];
if(self) {
_name = [name copy];
_birthDate = birthDate;
}
return self;
}
@end
In the modern runtime, you don’t have to use synthesize
. If you do, then your
instance variables get different names (without the underscore).
Now for the equality, we implement the method isEqual:
. There is an excellent
articleby Mike Ash, however, there is a mistake in there. Following his advice, our
first (incorrect) implementation looks like this:
- (BOOL)isEqual:(id)obj {
if(![obj isKindOfClass:[Person class]]) return NO;
Person* other = (Person*)obj;
BOOL nameIsEqual = [_name isEqual:other->_name];
BOOL dateIsEqual = [_date isEqual:other->_date];
return nameIsEqual && dateIsEqual;
}
There is a problem here: if one of the two properties is nil
, then isEqual:
will return NO
. This is because methods sent to nil
always return NO
, 0
or nil
. Even [nil isEqual:nil]
returns NO
.
Therefore, our second, correct implementation looks like this:
- (BOOL)isEqual:(id)obj {
if(![obj isKindOfClass:[Person class]]) return NO;
Person* other = (Person*)obj;
BOOL nameIsEqual = _name == _other->_name || [_name isEqual:other->_name];
BOOL dateIsEqual = _date == _other->_date || [_date isEqual:other->_date];
return nameIsEqual && dateIsEqual;
}
To implement the hashing function, I would like to recommend following Mike’s advice.
Bonus
Finally, as a bonus, let’s also implement NSCoding
, so we can serialize our
objects. First change the interface of Person
to this:
@interface Person : NSObject <NSCoding>
The implementation is now very simple:
- (id)initWithCoder:(NSCoder*)aDecoder {
self = [super init];
if(self) {
_name = [aDecoder decodeObjectForKey:kName];
_date = [aDecoder decodeObjectForKey:kDate];
}
return self;
}
- (void)encodeWithCoder:(NSCoder*)aCoder {
[aCoder encodeObject:_name forKey:kName];
[aCoder encodeObject:_date forKey:kDate];
}
The two constants kName
and kDate
are declared in the implementation file,
above the @implementation
directive:
static NSString* const kName = @"name";
static NSString* const kDate = @"date";
Voila, now we can create objects, read their properties, serialize them to disk and read them back in. Some catches: when you add a new property, you have to make sure to update the code in lots of places:
-
Add the property to the interface file
-
Add the parameter to
initWith:
, and also update the callers of that method -
Add a comparison to
isEqual:
-
Update the
hash
function -
Add the method to both
initWithCoder:
andencodeWithCoder:
It helps to have some tests in place that check this for you.
The full code of the examples (without the hash
function) is on
github.
Update
: changed the name
attribute to be copy
instead of strong
, thanks
to Christian
Kienle