Whilst listening to one of my favourite podcasts, Mobile Couch (Episode #34: Tuples, Chuples, Twooples) there was a discussion about whether or not adding inheritance, methods and other previously-restricted-to-classes capabilities was a good idea or not. The key question being... is it now harder to know which one to use?
- enums are initialised by one of a finite number of cases, are completely defined by their case, and should always be valid instances of that case when instantiated
- structs should be used when there is not a finite number of valid instances (e.g. enum cases) and the struct also does not form a complete definition of an object, but rather an attribute of an object
- A class completely defines a primary actor in your object model, both its attributes and its interactions.
I thought it was worth looking at the question in a bit more detail (although I agree with the commentary in the episode itself, you are likely to hit a wall if you pick the wrong one). There are some key assumptions used throughout the article
- Performance is not a consideration I think most people don't need to worry about a couple of extra milliseconds here or there. Where you do care, you will probably be sticking to known technology stacks right now, or at least focusing on the Objective-C based Metal API!
- You believe compiler checking is better than run-time checking, and running time checking is better than a crash If I can use the compiler to force me to do the right thing, or make it very hard for me to do the wrong thing, I want to. What I can say is that run-time checking involves more typing for me.
- You care about future you The more code can have its semantics dripping off every line of code the better. The more inescapable the concepts are in each reference, condition and case the better. This is why we've decided to augment tuples in our returns
To really address the question posed, these should hopefully be clean simple rules. Note here none of these proposals are driven by language features, rather an attempt to capture the intention behind a choice which must be supported by language features.
Enums represent a finite number of "states of being". Unlike enums in many other languages, they are not attributes of a state of being (e.g. used as a surrogate to specify one or more flags that ultimately become integers or'd together). In my opinion there are two use-cases
- Standalone The enum describes a state, for example a return value as an enum describes the exit state of the function
- Assembled The enum describes a sub-set of the state of an object. It possible that you could use it to define much more than that, but I believe that as soon as you are adding functions that do things other than contribute to the description of that state (or utilisation of it), you are straying into class-space
The second is a grey area, so here's an example often used where an enum captures all of the attributes of a planet. You may have attributes for diameter and density, and functions that calculate mass from those attributes. They are all describing the state of a planet. However, unless you define that there are 9 planets called Mercury, Venus, etc (the states of planet in the solar system) you should be looking at a struct or class. So here's the checklist
- There is a finite number of identifiable instances of the thing the enum is representing
- All functions and attributes common to all states can be derived from the state (e.g. the suit of a card completely defines its colour and symbol)
Swift enums really support this idea rather wonderfully, and the provision of tuples to accompany a state allow you to decorate cases with anything that is unique solely to that specific state of being.
Perhaps this can all be summarised as enums are initialised by their case and should always be valid instances of that case when instantiated.
I tend to think of a struct as a base type. It cannot be extended, and whilst functions can be performed on it, they should only provide additional information about that type. In short it is type defined by an assembly of attributes (which may be constant or variable)
Here's the checklist
- All functions either return information about the instance (for example, the centre of a CGRect as a CGPoint) or provide convenience functionality for modifying the instance (e.g. scaling a rect).
- Functions should not manipulate the state of other objects (classes or structs)
- None of the attributes of the struct require explicit releasing (e.g. observers)
- Inheritance is not required
Whilst you can extend structs, the extensions should conform to the above rules (well really 1 and 2). Whilst you might consider Protocols do allow you to sneak in inheritance through the back door, they really don't. Any given struct either implements a protocol or it doesn't.
structs should be used when there is not a finite number of valid instances that also does not form a complete definition of an object.
Perhaps things get easier here. If none of the others can be used, use this one! However, I think there are some characteristics here that are important for building a good OO model that can exploit the benefits of all of these language supported types
- Classes collaborate as primary components of a network that forms the view or model of your application (or aspect of your application)
- Functions on classes could have behaviour that manipulates the view or model of your application outside of the scope of the class and its attributes (i.e. may manipulate other objects in the object model)
I try to minimise the number of classes if possible, encapsulating behaviours pertaining to their attributes inside structs, and descriptive attributes inside enums (or base types if suitable).
Classes completely define a primary actor in your object model, both its attributes and its interactions.
Conclusion... Is it Harder?
I don't think it's any harder to get it right than it used to be, but I do think there are at least more opportunities to get it wrong. However, I think if you trickle up from enum, to struct to class in your decision making process you won't go far wrong.
We'd love to hear your thoughts