DEV Community

JoeStrout
JoeStrout

Posted on

Living Without Constructors

The MiniScript language does not have constructors; when you create a new object with new, like

okBtn = new PushButton
Enter fullscreen mode Exit fullscreen mode

...what you get back is exactly a map with __isa set, like:

{"__isa": PushButton}
Enter fullscreen mode Exit fullscreen mode

So, hooray for simplicity! But boo for wanting to do any more sophisticated initialization when your object is created. In other languages, you'd likely do such initialization in a constructor (a method that automatically runs when you create an instance of a class). But MiniScript doesn't have those. So what do you do?

Yuki The Chinchilla, by Maxunbanned (CC BY-SA 4.0)

Option 1: Separate init method

The first and most obvious remedy is to have a separate init method that you call after creating each object. You can find an example of this in the Asteroids demo at /sys/demo/asteroids. Line 37 defines GameSprite as a subclass of Sprite (GameSprite = new Sprite), and then lines 55-59 give it an init method:

GameSprite.init = function
    self.v = {"x":0, "y":0}
    self.destroyed = false
    display(4).sprites.push self
end function
Enter fullscreen mode Exit fullscreen mode

Then later, it makes a subclass of GameSprite called TimedSprite, to represent a game sprite that despawns after a little while:

TimedSprite = new GameSprite
TimedSprite.despawnTime = 0
TimedSprite.init = function(duration = 0.5)
    super.init
    self.despawnTime = time + duration
end function
Enter fullscreen mode Exit fullscreen mode

(TimedSprite also has a custom update method, but that's not important here.) Note how TimedSprite.init calls super.init, ensuring that the base class gets a chance to do its usual initialization. Similarly, when we make a subclass of TimedSprite called Bullet, it does the same thing:

Bullet = new TimedSprite
Bullet.instances = []
Bullet.init = function(lifetime = 0.6)
    super.init lifetime
    Bullet.instances.push self
end function
Enter fullscreen mode Exit fullscreen mode

Then when the player presses the fire button, we create a new bullet with the create, then initialize pattern (line 116-117):

        b = new Bullet
        b.init
Enter fullscreen mode Exit fullscreen mode

You can find other examples of this pattern in some other demos, like fatbits and flappyBat. This pattern works. But it's no longer my favorite solution.

Option 2: Static factory method

The other basic option is to never have callers use new directly, but instead to call a factory method on the class. A factory method is nothing more or less than a method whose job it is to create and return an object. And a "static" method is one that you call on the class, rather than on some specific object. I've lately settled into the habit of naming static methods and properties with a capital first letter, to distinguish them from ordinary instance methods and properties, and most typically, I call my factory method Make.

So this solution is just: stick a Make method on your class that creates, initializes, and returns a new instance.

You can find examples of this pattern in /sys/demo/soundLab, which first makes a RectControl class, and then makes several subclasses of that, including PushButton:

PushButton = new RectControl
PushButton.Instances = []
PushButton.image = desktopPic.getImage(248,106, 20,20)
PushButton.icon = null

PushButton.Make = function(left, bottom, image=null, icon=null)
    noob = new self
    noob.left = left
    noob.bottom = bottom
    noob.icon = icon
    if image then noob.image = image
    noob.width = noob.image.width
    noob.height = noob.image.height
    PushButton.Instances.push noob
    return noob
end function
Enter fullscreen mode Exit fullscreen mode

Be sure to notice the neat trick here: to actually create the new instance, this Make method says noob = new self. Here self is whatever class the Make method was called on; so if the user said PushButton.Make, then new self is equivalent to new PushButton. But if they call Make on some subclass of PushButton, then this new will instantiate that subclass instead.

Of course we're only using new here at all because the base class (RectControl) did not define a Make method. What if it does? Well, let's look at the WaveButton class, which derives from PushButton:

WaveButton = new PushButton

WaveButton.Make = function(left, bottom, imageIdx=1, iconIdx=0)
    return super.Make(left, bottom, WaveButton.Images[imageIdx], WaveButton.Icons[iconIdx])
end function
Enter fullscreen mode Exit fullscreen mode

The WaveButton.Make method just calls super.Make to create the new instance. In this case, we only overrode Make in order to pass the appropriate button image and icon. But we could have also done something like:

WaveButton.Make = function(left, bottom, imageIdx=1, iconIdx=0)
    noob = super.Make(left, bottom, WaveButton.Images[imageIdx], WaveButton.Icons[iconIdx])
    // ...do other initialization with noob here...
    return noob
end function
Enter fullscreen mode Exit fullscreen mode

Why Factory Methods Rock

A strong case can be made that factory methods are actually superior to constructors. Here are some reasons why:

  1. You can have multiple factory methods on the same class, with unique names making it clear what each is for: MakeForStorage or MakeFromFile or whatever.
  2. A factory method doesn't have to actually create a new object; it could pull an old object out of a recycling pool, or some sharable instance, and the caller doesn't need to know or care.
  3. A factory method could return a different type based on the parameter values: maybe you need a different class for high-speed sprites (like bullets) vs stationary sprites (like trees). That logic can be hidden inside the factory method, so callers don't have to remember it.
  4. A factory method can fail gracefully, returning null or (in MiniScript 2) and error in place of a new object. new can never do that.
  5. Factories are easier to evolve. You can change the return type, caching policy, initialization steps, etc., without having to update all the callers.

Some of these benefits would also apply to the init method, but most of them would not.

Conclusion

New MiniScript programmers are sometimes surprised by the lack of a constructor feature. But now that you know how to live with it, it's not a lack at all! Just a different way of doing things.

What's your favorite approach to creating & initializing objects in MiniScript? Share your thoughts below!

Top comments (1)

Collapse
 
treytomes profile image
Trey Tomes

Picking on your PushButton example:

PushButton.make = function(left, bottom, image=null, icon=null)
    self = new self
    self.left = left
    self.bottom = bottom
    self.icon = icon
    if image then self.image = image
    self.width = self.image.width
    self.height = self.image.height
    return self
end function
Enter fullscreen mode Exit fullscreen mode

MiniScript doesn't care if I reassign self, and reusing it like this makes my life easier. Remembering to place that return self at the end is usually where the errors occur for me.