I have a general strategic question about when to create subclasses versus when to do something else. As a motivating example, let’s say I have three boxes that I’d like to model. Boxes have various properties like material and mass, height, length, and width. Additionally, the boxes that I have are moving in space, with this motion given by a parametric function.
There are three very unsatisfying ways I can think of to solve this problem. I’m requesting help on thinking about whether there is a better way. Please understand that this is a very silly illustrative example!
Method 1: AbstractBox
It would be easy to define something like
abstract type AbstractBox end
function position(b::AbstractBox, t)::Tuple(AbstractFloat, AbstractFloat) # returns the (x, y) position
    throw("not yet implemented")
end
To do this, I need to define
struct Box1 <: AbstractBox
  material::String
  mass::AbstractFloat
  height::AbstractFloat
  length::AbstractFloat
  width::AbstractFloat
end
position(b::Box1, t) = (t, t^2) # returns the (x, y) position
and similarly for Box2, Box3, etc even though the only thing that should differ is the position method. This is unsatisfying because I have to copy out the material, mass, height, length, width, etc fields
Option 2: BaseBox
An alternative approach is to define a BaseBox object that I can then embed in Box1, Box2, Box3, etc.
struct BaseBox
  material::String
  mass::AbstractFloat
  height::AbstractFloat
  length::AbstractFloat
  width::AbstractFloat
end
struct Box1
  b::BaseBox
end
position(b::Box1, t) = (t, t^2) # returns the (x, y) position
This works OK, but it’s pretty clunky and requires that accessing things like Box1.b.height.
Option 3: Box
The third option is just to create a single Box type, and to treat Box1, Box2, and Box3 as separate instances. To make this work, position needs to be a field
struct Box
  material::String
  mass::AbstractFloat
  height::AbstractFloat
  length::AbstractFloat
  width::AbstractFloat
  position_fn::Function
end
position(b::Box, t) = b.position_fn(t)
box1_pos_fn(t) = (t, t^2)
Box1 = Box(..., box1_pos_fn)
This works fine and is perhaps the cleanest, but adding functions as fields doesn’t feel very clean to me.
Any advice / suggestions appreciated! Performance is definitely of interest.