Achieve More with a Variety of Value Objects in Swift

null

From the department of Domain-Driven Design code patterns, I today present to you: well-named value objects! You can go a lot farther than you have previously imagined with value objects in Swift. Now that Swift is more than a year old, most of us have seen the use of struct and heard how useful passing objects by value is.

A typical real-world example is the data-transfer object, which contained a painful amount of boilerplate in Objective-C but became super simple with Swift:

struct BananaData {
    let size: Int
    let color: UIColor
}

Suppose we also have a Banana entity for which BananaData is a simpler representation.

You see how versatile BananaData is?

Versatility is not always a good sign. It can mean that something is used in the wrong way, too, or that you have missed to model something else because the existing stuff is just enough.

Given how easy it is to create a value object in Swift, let me propose something better than XYZData:

struct NewBanana {
    let size: Int
    let color: UIColor
}

This is used to create a Banana entity only. It took me some getting used to because there’s no “Data” suffix to signify that it’s not in fact a new banana itself, but data for a new banana. Nowadays, though, its name screams “use me for banana creation” at me.

NewBanana should be the result of a user interaction.

“But it just looks like BananaData to me! Can’t we use it for the JSON parser as well?” – Yes we could, but then we’d mix banana creation from new data and banana reconstitution (or “hydration”, as some call it) from stored data.

To use a NewBanana as view model sounds just too plain weird. I can bend my mind and think “display a new banana in the view” means it’s novel for the view, as opposed to “update an existing banana view”, but that’s not working out for me.

You can think of NewBanana as a command. If you want to have verbs in your commands, call it CreateNewBanana. Why not?

Remember that your project terms may differ and that a “New” prefix doesn’t always have to be used for the same concepts.

struct ParsedBanana {
    let size: Int
    let color: UIColor
}

Use ParsedBanana to signify it’s the result of the JSON data. I’d probably stick with BananaData here, as “-Data” signifies its a representation of storage contents to me already. That’d make it harder for you to see the result of this change, so I better pick another name. Again, your use of the terms may differ, so consider to use something that’s meaningful to you, which may be ParsedBanana.

struct BananaViewModel {
    let size: String  // watch this: it's not an Int
    let color: UIColor
}

Use BananaViewModel to display bananas in your user interfaces. Notice that the view model sports properties tailored closely for the view: the size will be printed as text, so it’s a string. Think “medium” or “2 inches” – whatever it is, it’s prepared for immediate use.

Now you wouldn’t put all these banana-related value objects in one place. Instead, put them where they are used: The view model should live in the user interface layer.

The real Banana entity should know about none of these, though it’s okay for me to extend Banana with factories to create a new entity from ParsedBanana and NewBanana. Although extension affect the type, it seems to be an idiomatic Swift way to create factories and encapsulate knowledge about object creation.

So now our project contains:

The three value objects we end up with are strikingly similar. Isn’t that a stupid duplication? Well, it is duplication right now. But that’s because this is the result of a refactoring towards more meaningful objects.

These diverse types can’t be mixed freely, whereas the initial BananaData could have come from anywhere and be used everywhere. This enables us to attach specific properties to one of the types without affecting the other uses of banana representation.

For example, add a date property to NewBanana and store it along the other attributes on your server. Now you have the power to run statistics from the backend, which is another “bounded context” of your application as a whole. Users of your app will not need this statistical information, so we won’t include a similar property in ParsedBanana or BananaViewModel.

This way we end up with different read and write models. That’s great news, as we can architect our app differently with more ease to support CQRS. Or use an event store for writing and obtain data in a CRUD way from another store. The all-purpose BananaData hasn’t made that harder, but it now feels more natural.

Creating low-cost value objects which look similar but mean different things is the way to go to let your app grow. When everything looks the same, it’s harder to find leverage to improve code.


Photo credit: “Minion Phil with BANANA!” by Wilson Hui on flickr. License: CC-BY 2.0

Browse the blog archive