Expressive Domain Model, Core Data, and You
I’ve written a whole book on the topic of architecting a Mac app using Core Data. Today I found an approach which doesn’t add lots of overhead and which also doesn’t clutter everything everywhere.
It’s pretty simple and involves Swift.
Here’s the steps you need to take:
- Create a
NSManagedObject
subclass for your entity. I like to prefix mine with “CoreData”. Make all@NSManaged
properties private, at least the setter. That’s important to avoid clients changing every possible attribute of your model. - If the stored property is a
String
orNSData
representation of something else, for example, mark the whole property private and provide a public getter which wraps the representation in the real thing. - In the same file, create an internal or public domain object with the appropriate name as a subclass of your managed object.
Since I like to put some static convenience methods on my managed objects, I am currently considering to use double-dispatch
Here’s a very simple example.
First, the value objects which are part of the domain:
// MARK: Value Objects - may live in a separate file
enum EggSize: Int {
case Small = 1
case Medium
case Large
case XLarge
}
struct EggID {
let identifier: String
init() {
self.identifier = NSUUID().UUIDString
}
init(identifier: String) {
self.identifier = NSUUID(UUIDString: identifier)!.UUIDString
}
}
/// Contains all data necessary to create a new Egg
struct NewEgg {
let eggID: EggID
let size: EggSize
}
You see where this is going, don’t you?
// MARK: Domain Object
@objc(Egg)
class Egg: CoreDataEgg {
var eggID: EggID {
return EggID(identifier: eggIdentifier)
}
var size: EggSize {
return EggSize(rawValue: eggSize)
}
// Now this doesn't make sense. But I needed to prove a point quickly :)
func changeSize(newSize: EggSize) {
let oldSize = size
eggSize = newSize.rawValue
DomainEventPublisher.sharedInstance
.publish(EggSizeChanged(eggID: eggID, from: oldSize, to: newSize))
}
}
// Living in the same file to make use of per-file private properties
class CoreDataEgg: NSManagedObject {
static let entityName = "Egg"
static func fetchRequest() -> CoreDataFetchRequest<Group> {
return CoreDataFetchRequest(entityType: Group.self)
}
static func insert(newEgg: NewEgg, context: NSManagedObjectContext) {
let egg = NSEntityDescription.insertNewObjectForEntityForName(entityName, inManagedObjectContext: context) as! Egg
egg.eggSize = newEgg.size.rawValue
egg.eggIdentifier = newEgg.eggID
}
@NSManaged private var eggIdentifier: String
@NSManaged private var eggSize: Int
}
Now Egg
, which is exposed to clients, has a proper public API. It still inherits a lot of the NSManagedObject
stuff, of course, but when you look at its interface, you can see what you should be doing with it.
That’s a lot more than you’d know if you used CoreDataEgg
directly with public or internal setters for the properties.
An expressive domain starts where you don’t have to guess. It sports meaningful methods which reveal your intent. In that sense, changeSize(_:)
reveals that eggs are intentionally resizable. It’s arguably a better place than overriding the setter to publish events, too.
Don’t fight the framework, they say. Core Data is awesome. But I still don’t want to have its internals linger everywhere.
Caveats (will add to the list as I encounter any):
- Using Core Data managed objects as domain objects makes testing harder: you can’t create instances easily, like
Egg()
, but have to useNSEntityDescription
instead. - Initializer inheritance. You can’t get rid of
init(entity:,insertIntoManagedObjectContext:)
. (2015-10-08)
I’m trying this approach for now and keep you updated.
Update 3 hours later: The Egg
domain object has to subclass CoreDataEgg
, of course, which makes the public interface of Egg
“dirty.” so far, it’s the best trade-off between functionality and conceptual purity I’ve encountered. Simple CRUD models without much behavior don’t need all of this – you can better decouple the domain objects from the Core Data managed objects.