This article is also available in Chinese.

I often get a better picture of a type’s purpose from knowing what its instance variables are. Once you know the underlying structure of that type, it’s more obvious how to use it. The converse is also true: it can sometimes be hard to figure out what exactly an object is for, without seeing the layout of its members. This is especially apparent with Apple’s closed-source classes.

A great example of this is NSDate. When I started programming, I had a tough time figuring out how to use the NSDate and all of its corresponding sibling objects, like NSDateComponents, NSDateFormatter NSCalendar. Why do you have to use NSCalendar to add 2 days to date? The boundaries between these classes seem arbitrarily drawn, and that makes it hard to know where to look to find any specific functionality you’re looking for.

The key revelation for me was understanding what NSDate really was under the hood, which made all the rest of the pieces fall together. NSDate is just a fancy wrapper around a double. That’s it. And a close reading of the docs reveals this fact:

NSDate objects encapsulate a single point in time, independent of any particular calendrical system or time zone.

All NSDate does is store a double representing the number of seconds since 00:00:00 UTC on January 1, 2001. This number of seconds has nothing to do with timezones, days of the week, months, daylight savings time, leap seconds, or leap years. If the calculation can be done with just the number of seconds since that reference date, it goes on NSDate. Otherwise, it goes somewhere else.

Examples of things that can be done with just this double are comparison (earlierDate, laterDate), equality, and time interval calculations (which also returns a number of seconds). distantFuture and distantPast are obvious now too, they’re just the largest expressible doubles, in positive and negative forms.

For other stuff, you have to go to other classes and objects. For example, adding one day to a moment in time, while it can done by just adding 24*60*60 seconds to an NSDate, is best handled via NSCalendar, to avoid running afoul of issues with Daylight Savings Time, leap seconds/days, and other non-standard issues that can crop up with time. This blog post lays out the case for using NSCalendar for these calculations.

Because NSDate doesn’t store any information about the expected month of a particular date, if you want to change that month, you have to use an object that knows about and can split apart the various “components” that make up a date. For this, we use a NSDateComponents object. I find that thinking about how I might write a similar class can help me understand what it’s for and how its layout works. Take a look at the public interface for NSDateComponents and see if you can figure out how its data might be stored internally.

I found another interesting example of using an object’s property storage layout to learn things about the nature of that object when I was working on my Promise library. If you look at the properties that each of my Promise objects stores, you’ll see three things:

public final class Promise<Value> {
    
    private var state: State<Value>
    private let lockQueue = DispatchQueue(label: "promise_lock_queue", qos: .userInitiated)
    private var callbacks: [Callback<Value>] = []

Each promise has its current state (such as .pending, fulfilled, or .rejected), a queue to ensure thread-safety, and an array of callbacks to call when the promise is either fulfilled or rejected.

After writing my library, I looked at some Signal/Observable implementations to see if I could understand them. I found JensRavens/Interstellar to be the most straightforward. I look at the instance properties of each of its Signal objects, and I found a very similar structure:

public final class Signal<T> {
    
    private var value: Result<T>?
    private var callbacks: [Result<T> -> Void] = []
    private let mutex = Mutex()

Something to store the current state, something to store the callbacks, and something to store the thread-safety primitive. The order is different, and they used a mutex instead of a queue, but it’s otherwise identical. The only difference between these two types is a semantic one: promises can clear their callbacks when they’re completed (releasing them and the variables they capture), and signals must keep their callbacks around.

I think this principle can also help in the design of your own types too. Take a look at the properties on an object that you’re working on. Does each property have a purpose? Does it contribute to the overall identity of the object? Are there properties that are used in some instances of that type, but not others? If so, those properties might belong somewhere else. Making sure that the instance variables available on a type are tightly controlled and used fully ensures that our object has a well-defined purpose in your application.