-
Notifications
You must be signed in to change notification settings - Fork 0
Composition Subclassing
On this page you can read about how Juliagebra handles OOP through composition.
We need to have a type system where we can easily define object-oriented like classes that have:
- Inheritance
- Abstraction
- Dynamic dispatch
Or first step to achieve the points mentioned above starts by defining the abstract class setup of our program using the built-in abstract types of Julia
abstract type VehicleDNA end
abstract type CarDNA <: VehicleDNA end
abstract type SuperCarDNA <: CarDNA endHere we define three abstract classes:
- Vehicle
- Car
- Supercar
The "body" of theese classes will be created later. Here we just define them, so that in later files we can start using theese classes as function inputs, without theese classes being defined yet (so this is how we defend ourselves from circular imports).
Note here that their Julian abstract type name ends with "DNA" everywhere.
Now let's start defining Vehicle first:
# ! Vehicle is an "abstract class"
mutable struct Vehicle
_passengerCount::Int
end
_Vehicle_(self::VehicleDNA)::Vehicle = error("Missing func!")
getPassengerCount(self::VehicleDNA) = _Vehicle_(self)._passengerCountWe can see that Vehicle is defined as a struct.
Note here that when writing we use VehicleDNA as an input type. This is because Vehicle is an abstract class, and force that we can't use it as is, only children of Vehicle can actually exist.
The Vehicle function will play a crutial role, as theese functions will be defined in the children, and they will help Julia know where the parent's data is in the children and so theese function should be named like ParentName with two "_"-s.
Observe that Vehicle function is used in the getPassengerCount function, where if someone is a children of Vehicle, it should have a Vehicle compositionally, so we ask for it, and return _passengerCount, because Vehicle has that field.
Let's see how Car is defined:
# ! Car is an "abstract class"
mutable struct Car
_vehicle::Vehicle
_fuel::Float64
Car(passengerCount,fuel) = new(Vehicle(passengerCount),fuel)
end
_Car_(self::CarDNA)::Car = error("Missing func!")
_Vehicle_(self::CarDNA)::Vehicle = return _Car_(self)._vehicle
getFuel(self::CarDNA) = return _Car_(self)._fuelAs we can see Car extends Vehicle. To tell Julia that this is the case, we define the Vehicle function as seen above and make Car have Vehicle as a field. For setting up the class to have children, we define Car as well.
Concrete classes are classes which can't have children, but can be constructed and polymorphism works on them.
# ! Helicotper is a "Concrete class"
mutable struct Helicopter <: VehicleDNA
_vehicle::Vehicle
_altitude::Float64
Helicopter(passengerCount,altitude) = new(Vehicle(passengerCount),altitude)
end
_Vehicle_(self::Helicopter) = return self._vehicle
getAltitude(self::Helicopter) = return self._altitudeNote that Vehicle has to be defined and Helicopter has to have a Vehicle typed field for full subclassing.
Now let's finish up the remaining DNA, SuperCarDNA:
# ! SuperCar is a "Concrete class" and it can have children as well
mutable struct SuperCar <: SuperCarDNA
_car::Car
_color::String
SuperCar(passengerCount,fuel,color) = new(Car(passengerCount,fuel),color)
end
_SuperCar_(self::SuperCarDNA)::SuperCar = error("Missing func!")
_SuperCar_(self::SuperCar)::SuperCar = return self
_Car_(self::SuperCarDNA) = _SuperCar_(self)._car
getColor(self::SuperCarDNA) = return _SuperCar_(self)._colorThe only big difference is that SuperCar is defined twice, and SuperCar is Julian subtype of SuperCarDNA.
Note that children for SuperCar should be defined like this:
# ! Ferrari is a "Concrete class"
mutable struct Ferrari <: SuperCarDNA
_superCar::SuperCar
function Ferrari()
passengerCount = 2
fuel = 50.0
color = "Red"
new(SuperCar(passengerCount,fuel,color))
end
end
_SuperCar_(self::Ferrari) = return self._superCarExample usage of the above defined type system:
ferrari = Ferrari()
println("$(getPassengerCount(ferrari))")
println("$(getFuel(ferrari))")
println("$(getColor(ferrari))")
lamborghini = SuperCar(2,96.0,"Orange")
println("$(getPassengerCount(lamborghini))")
println("$(getFuel(lamborghini))")
println("$(getColor(lamborghini))")
helicopter1 = Helicopter(5,0.0)
println("$(getPassengerCount(helicopter1))")
println("$(getAltitude(helicopter1))")
helicopter2 = Helicopter(15,670.0)
println("$(getPassengerCount(helicopter2))")
println("$(getAltitude(helicopter2))")One very powerful advantage of this type system, is that whenever I write functions for parent classes, inside thoose functions I can acces the full reference to the children as well, not just the compositional part of the class.
The compositional functions ("ParentName") are not that performance heavy, because Julia can inline theese, but one could write @inline just to make sure.
For Generic classes (for example "GenericClass{T} - GenericClassDNA{T}") this pattern should be followed (Julia even has Generic abstract types).
This implemention of Object-orientedness can be made shorter with macros, but for debuggability it remains typed.