Tree: the Object Oriented Way — eureca
Tree DS, and how it saved us hours and lines of coding and debugging from scalability pov
We just made our Solar System enhancement live. As exhilarating as it was to build, it was interspersed with full-blown consternation at times, and delight at other. But mostly, it was the former to start off.
We already had made a minimum viable module in the form of Solar System earlier in eureca. You got the Sun, and the planets. You tap and they tell you about themselves. There was basic animation included as well. Well, that was all folks. Our users were polite enough to say that it looked good but deep in our guts we knew we are missing something big.
We use firebase for our data analytics, and as it had never failed before, it didn’t do this time around, when we took a deep dive into our aggregated metrics. The fact that most app users started off with Solar System module after download, and then moved on to something else, if they were interested, was right in our face. It was time to revamp it. If that doesn’t get the value out in terms of what kids are learning within, what’s the point after all!
We were to inch towards the Universe.
Problème eh!
The data structure before our enhancement, I am sure would make most coders squirm in their sleep.
enum Planet: String {case sun, mercury, earth, jupiter, saturn, mars, venus, uranus, neptune, pluto}let planets: [Planet] = [.sun, .mercury, .earth, .jupiter, .saturn, .mars, .venus, .uranus, .neptune, .pluto]
Ew, right!
The major issue was how to extend this now to include the natural satellites, such as Moon, Phobos, Titan and more. If we kept on adding onto the existing DS and then creating their relationship, it would be a mess — and for some, a coding blasphemy, wouldn’t it!
We needed our relationships in the form above — a parent-children relationship, where each object knows about its parent and children. Just imagine managing the previous data structure for Sun, planets and their several moons, let alone extending it to include, let’s say, the Milky Way.
The Solution — Daim!
Sun is a proud papa. He has 9 children out of which one (Pluto) lives really far away but is close to Sun’s heart as his other children are. Bigger (elder) of his children have already become proud papa themselves, such as Jupiter, Saturn, and more. Each of these have some common property, such as they all spin, have a tilt, and are either hot or cold. Sun’s father is the Milky Way. Sun didn’t take his last name, but nevertheless is deeply connected to him at a philosophical level.
In order to create this complicated a relationship which should be as scalable as the Sun’s family, we need a different Data Structure. We need Tree — which scratches the surface of Graph Theory.
Tree is an undirected graph in which any two vertices are connected by exactly one path — Wikipedia rules, doesn’t it!
So, here is what we did.
// Swift // The enum need not be declared this way. It could very easily be created automatically using FileIO.enum PlanetName {
case sun, mercury, earth, jupiter, saturn, mars, venus, uranus, neptune, pluto, moon, callisto, europa, ganymede, io, lysithea, deimos, phobos, nereid, proteus, triton, dione, enceladus, hyperion, iapetus, janus, mimas, rhea, tethys, titan, ariel, miranda, oberon, titania, umbriel
}class Astronode {
var children = [Astronode]()
var name: PlanetName
var distFromCenterForRevolving: Float
var scaleForRevolving: Float
var axialTilt: Float
var orbitalTilt: Float
var rotationTime: CFTimeInterval
var revolutionTime: CFTimeInterval
var astroDisplayInfo: AstroDisplayInfo
var root: Astronode?
var spinDirections: SpinDirections
var surfaceTemperatues: MeanSurfaceTemperature
var gValue: Float init(_name: PlanetName,_distFromCenterForRevolving: Float, _axialTilt: Float, _orbitalTilt: Float, _rotationTime: CFTimeInterval, _revolutionTime: CFTimeInterval, _scaleForRevolving: Float, _displayRotation: String, _displayRevolution: String, _displayDistFromCenter: String, _progradeRot: Bool, _progradeRev: Bool, _avgSurfaceTemp: Float, _gValue: Float) {
name = _name
distFromCenterForRevolving = _distFromCenterForRevolving
rotationTime = _rotationTime
axialTilt = _axialTilt
orbitalTilt = _orbitalTilt
revolutionTime = _revolutionTime
scaleForRevolving = _scaleForRevolving
gValue = _gValue
astroDisplayInfo = AstroDisplayInfo(rot: _displayRotation, rev: _displayRevolution, dist: _displayDistFromCenter)
spinDirections = SpinDirections(progradeRot: _progradeRot, progradeRev: _progradeRev)
surfaceTemperatues = MeanSurfaceTemperature(avgTemp: _avgSurfaceTemp)
} func addChildren(child: Astronode) {
children.append(child)
child.addParent(parent: self)
} func removeChildren(child: Astronode) {
children.removeAll { (obj) -> Bool in
obj.name == child.name
}
} func addParent(parent: Astronode) {
root = parent
}
}struct SpinDirections {
var progradeRot: Bool
var progradeRev: Bool init(progradeRot: Bool, progradeRev: Bool) {
self.progradeRot = progradeRot
self.progradeRev = progradeRev
} func getSpinToValues() -> (rotTo: Float, revTo: Float) {
let _rotTo: Float = progradeRot ? 360 : -360
let _revTo: Float = progradeRev ? 360 : -360
return (rotTo: _rotTo, revTo: _revTo)
}
}struct MeanSurfaceTemperature {
var avgTemperature: Float init(avgTemp: Float) {
avgTemperature = avgTemp
} func getSurfaceTemperaturesInCelcius() -> Float {
return avgTemperature
} func fetchTemperatureImage() -> UIImage {
return avgTemperature > 36 ? imageLiteral(resourceName: "hot_temp") : imageLiteral(resourceName: "cold_temp")
}
}struct AstroDisplayInfo {
var rotation: String
var revolution: String
var distanceFromCenter: String
init(rot: String, rev: String, dist: String) {
rotation = rot
revolution = rev
distanceFromCenter = dist
}
}
Each body in the Universe is an Astronode, with several properties, such as own axial tilt, orbital tilt, average surface temperature, etc. Some of these properties can be clubbed together into a different Data Structure — either Struct
or Class
, depending on the need to use composition and keeping internal mechanism of their methods isolated from Astronode. One such Astronode’s property is MeanSurfaceTemperature
, which and only which needs to fetch get relevant image to represent cold or hot temperature, obtain temperature in Celsius or Fahrenheit , and more. These properties are declared separately as Structs
.
Let’s work on creating the actual Tree now.
class Astrograph {
static var sharedInstance = Astrograph()
var collection = [PlanetName: Astronode]() let sun = Astronode(_name: .sun, _distFromCenterForRevolving: 0.0, _axialTilt: 7.25, _orbitalTilt: 0, _rotationTime: 1500.0, _revolutionTime: 0.0, _scaleForRevolving: 1.0, _displayRotation: "25d", _displayRevolution: "", _displayDistFromCenter: "", _progradeRot: true, _progradeRev: true, _avgSurfaceTemp: 5600, _gValue: 274)
...
init() {
collection[.sun] = sun; ..., collection[.umbriel] = umbriel
sun.addChildren(child: mercury)
...
...
earth.addChildren(child: moon)
}
}
For sure, rather than creating a Dictionary
to hold all bodies, we could have used a recursive method to get to any of the bodies within the Tree, howmuchever deep it is ensconced . But given the optimisation achievable through the use of hashmap
, especially at not a humongous size, we decided to go ahead with a dictionary holding direct reference to each body. We might need to choose the former though when we extend the Tree beyond three generations.
We didn’t need multipleAstrograph
objects — we needed just one, which could be referred from anywhere within the code, hence the need for singleton structure with sharedInstance
.
And voilà! We have it.
Hope you enjoy it as much as we have when we were creating it. Let’s know ideas to improve the DS further. Signing off for another time. Know a way to make it crisper, let us know!