Sunday 1 September 2024

Design patterns: singleton

G'day:

(Long time no see, etc)

The other day on the CFML Slack channel, whilst being in a pedantic mood, I pointed out to my mate John Whish (OG CFMLer) that he was using the term "singleton" when all he really meant was "an object that is reused". A brief chat in the thread and in DM ensued, and was all good. We both revisited the docs on the Singleton Pattern, and refreshed & improved our underatanding of it, and were better for it. Cool. The end. Short article.

...

..

Then the "well ackshually" crowd showed up, and engaged in a decreasingly meritorious diatribe in how there's no difference between a class that implements the Singleton Pattern, and an object that one happens to decide to reuse: they're both just singletons.

OK so the reasoning wasn't quite that daft (well from one quarter, anyhow), but the positioning was missing a degree of nuance, and there was too much doubling down in the "I'm right" dept that I just gave up: fine fellas, you do you. In the meantime I was still talking to John in DM, and I mentioned I was now keen to see how an actual singleton might come together in CFML, so there was likely a blog article ensuing. I predicted CFML would throw up some barriers to doing this smoothly, which is interesting to me; and hopefully other readers - if I have any still? - can improve their understanding of the design pattern.

I'll put to bed the "debate" first.

The notion of a singleton comes from the Singleton Pattern, which is one of the perennial GoF Design Patterns book. It's a specific technique to achieve an end goal.

What's the end goal? Basically one of code organisation around the notion that some objects are intended to be re-used, and possibly even more strongly: must be re-used in the context of the application they are running in. One should not have more than one of these objects in play. An obvious, oft-cited, and as it turns out: unhelpful, example might be a Logger. Or a DatabaseConnection. One doesn't need to create and initialise a new DatabaseConnection object every time one wants to talk to the DB: one just wants to get on with it. If one needed to instantiate the DatabaseConnection every time it was used, the code gets unwieldy, breaks the Single Responsibility Principle, and is prone to error. Here's a naïve example:

numbers = new DatabaseConnection({
    host = "localhost",
    port = "3306",
    user = "root",
    password = "123", // eeeeek
    database = "myDb"
}.execute("SELECT * FROM numbers"))

One does not wanna be doing all that every time one wants to make a call to the DB. It means way too much code is gonna need to know minutiae of how to connect to the DB. One will quickly point out the deets don't need to be inline like that (esp the password!), and can be in variables. But then you still need to pass the DB credentials about the place.

There's plenty of ways to solve this, but the strategy behind the Singleton Pattern is to create a class that controls the usage of itself, such that when an instance is called for, the calling code always gets the same instance. The key bit here is a class that controls the usage of itself[…] always[…] the same instance. That is what defines a singleton.

My derision in the Slack conversation was that the other participants were like "yeah but one can do that with a normal object just by only creating one of them and reusing it (muttermutterDIcontainer)". Yes. Absolutely. One can def do that, and often it's exactly what is needed. And DI containers are dead useful! But that's "a normal object […] and reusing it". Not a singleton. Words have frickin meanings(*). I really dunno why this is hard to grasp.

It's like someone pointing to white vase with blue art work on it, and going "this is my Ming vase". And when one then points out it says "IKEA" on the bottom, they go "doesn't matter. White with blue detail, and can put flowers in it. That's what a Ming vase is, for all intensive purposes (sic)". "You know a Ming vase is a specific sort of vase, right?". "DOESN'T MATTER STILL HOLDS FLOWERS". OK mate.

Digression, around that (*) I put above. I'm a firm believer in words are defined by their usage, not by some definition written down somewhere. A dictionary, for example, catalogues usage, it doesn't dictate usage. This is fine, but it's also less than ideal that words like "irregardless" end up in the dictionary because people are too…erm… "lacking a sense of nuance"… to get easy things right. This is pretty much where I am coming from here: "Singleton" means something; it'd be grand if the usage of it didn't get diluted due to people "lacking a sense of nuance" to get easy things right. And then to double-down on it is just intellectually-stunted, and not something I think we should be revelling in. I do also feel rather like Cnut vs the surf in this regard though.

Ah well.


Anyhoo, can I come up with a singleton implementation in CFML?

The first step of this didn't go well:

// Highlander.cfc
component {

    private Highlander function init() {
        throw "should not be runnable"
    }
}
// test.cfm    
connor = new Highlander()

writeDump(connor)

I'd expect an exception here, but both ColdFusion and Lucee just ignore the init method, and I get an object. Sigh.

This is easily worked-around, but already my code is gonna need to be less idiomatic than I'd like it to be:

public Highlander function init() {
    throw "Cannot be instantiated directly. Use getInstance"
}

Now I get the exception.

Next I start working on the getInstance method:

public static Highlander function getInstance() {
    return createObject("Highlander")
}
// test.cfm
connor = Highlander::getInstance()

writeDump(connor)

This still returns a new instance of the object every time, but it's simply a first step. To easily show whether instances are the same or different, I'm gonna given them an ID:

// Highlander.cfc
component {

    variables.id = createUuid()

    public Highlander function init() {
        throw "Cannot be instantiated directly. Use getInstance"
    }

    public static Highlander function getInstance() {
        return createObject("Highlander")
    }

    public string function getId() {
        return variables.id
    }
}
// test.cfm
connor = Highlander::getInstance()

writeDump(connor)

goner = Highlander::getInstance()

writeDump([
    connor = connor.getId(),
    goner = goner.getId()
])

 

See how the IDs are different: they're different objects.

We solve this by making getInstance only create the object instance once for the life of the class (not the object: the class).

public static Highlander function getInstance() {
    static.instance = isNull(static.instance)
        ? createObject("Highlander")
        : static.instance

        return static.instance
}

It checks if there's already an instance of itself that it's created before. If so: return it. If not, create and store the instance, and then return it.

Now we get better results from the test code:

 

Now it's the same ID. Note that this is not isolated to that request: it sticks for every request for the life of the class (which is usually the life of the JVM, or until the class needs to be recompiled). I'm altering my writeDump call slightly:

writeDump(
    label = "Executed @ #now().timeFormat('HH:mm:ss')#",
    var = [
        connor = connor.getId(),
        goner = goner.getId()
    ]
)

 

 

The ID sticks across requests. It's not until I restarts my ColdFusion container that the static class object is recreated, and I get a new ID:

 

One flaw in this implementation is that there's nothing to stop the calling code using createObject rather than using new to try to create an instance of the object. EG: this "works":

// test2.cfm
connor = createObject("Highlander")
goner = createObject("Highlander")

writeDump(
    label = "Executed @ #now().timeFormat('HH:mm:ss')#",
    var = [
        connor = connor.getId(),
        goner = goner.getId()
    ]
)

 

When I say this "works" I am setting the bar very low, in that "it doesn't error": it's not how the Highlander class is intended to be used though.

Oh: in case it's not clear why there's no exception here: it's cos when one uses createObject, the init method is not automatically called.

Can I guard against this?

Sigh.

OK, on Lucee I can do this with minimal fuss:

// Highlander.cfc
component {

    if (static?.getInstanceUsed !== true) {
        throw "Cannot be instantiated directly. Use getInstance"
    }

    variables.id = createUuid()

    public Highlander function init() {
        throw "Cannot be instantiated directly. Use getInstance"
    }

    public static Highlander function getInstance() {
        static.getInstanceUsed = true

        static.instance = isNull(static.instance)
            ? createObject("Highlander")
            : static.instance

        static.getInstanceUsed = false

        return static.instance
    }

    public string function getId() {
        return variables.id
    }
}

What's going on here? The conceit is that the class's pseudo-constructor code is only executed during object creation, and when we are creating an object via getInstance we disable the "safety" in the pseudo-constructor, but then re-enable it once we're done creating the instance. if we don't use getInstance, then the safety has either never been set - exception - or it's been set to false by an erstwhile call to getInstance - also exception.

Looking at that code, I can see that there's a race-condition potential with getInstance's setting/unsetting of the safety, so in the real world that code should be locked.

As I alluded to above: this code does not work in ColdFusion, because ColdFusion has a bug in that the pseudo-constructor is run even when running static methods, so the call to getInstance incorrectly calls the pseudo-constructor code, and triggers the safety check. Sigh. I can't be arsed coming up with a different way to work around this just for ColdFusion. I will raise a bug with them though (TBC). ColdFusion also has another bug in that static?.getInstanceUsed !== true errors if static.getInstance is null, as the === doesn't like it. I guess I'll raise that too (also TBC).

So. There we go. A singleton implementation.


PHP's OOP is more mature than CFML's, so a PHP implementation of this is a bit more succinct/elegant:

class Highlander {
    
    private ?string $id;
    
    private static self $instance;
    
    private function __construct() {
        $this->id = uniqid();
    }
    
    public function getId() : string {
        return $this->id;
    }
    
    public static function getInstance() : static {
        self::$instance = self::$instance ?? new static();
        return static::$instance;
    }
}

$connor = Highlander::getInstance();
$goner = Highlander::getInstance();

var_dump([
    'connor' => $connor->getId(),
    'goner' => $goner->getId()
]);

$transient = new Highlander();
array(2) {
  ["connor"]=>
  string(13) "66d457f9d4cd1"
  ["goner"]=>
  string(13) "66d457f9d4cd1"
}

Fatal error: Uncaught Error: Call to private Highlander::__construct() from global scope in /home/user/scripts/code.php:30
Stack trace:
#0 {main}
  thrown in /home/user/scripts/code.php on line 30

And that's that.

Righto.

--
Adam

Friday 29 March 2024

OT: Starfield

G'day:

I finally relented on the Steam nagging screen, and am writing a review of Starfield, which I have been playing pretty much daily since it was released. I just noticed I'm writing an essay rather than a review, so figured I'll "own" it and post it here as well as give it to Steam/Bethesda.


Would you recommend this game other players? Yes.

There's some good gameplay in this, but around the edges. I wish the rating system on this had at least a "maybe" option as well as "yes" and "no". this is a 6/10 game (I'd rank Skyrim and Fallout 4 as 10/10 games, to compare).

The main quest is sterile and repetitive, and the main NPCs are on the whole obnoxious and unengaging. Companion AI doesn't seem improved over FO4: they are always just in the way, rather than being helpful. Too much dialogue is too repetitive. Note to the Bethesda scripting team: each canned script line should only be used once (maybe once per humanspace week or something, just not every time I come within 1m of the person). Silent NPCs would be better than irritating (and vaccuous) repetition. it detracts from immersion. Can't wait for Bethesda to integrate chatgpt-ish AI into NPC comms; it would mix it up a lot, and - I'm afraid - probably better than the current script-writing which is mediocre.

The faction quests are too short and linear, but more interesting than the main quest.

Inventory imbalance needs a look at. One needs tonnes of resources to play the game effectively as an outpost builder, but it's too hard to ferry about the place without mods to adjust / negate weight. It's not fun to be a cargo carrier (especially as the cargo capacity of ownable spacecraft are so low - at leat to start with, but they never get great).

Intersystem cargo transport is unusable on my rig, because as soon as I have more than one of them, the game blocks every few seconds (I guess it's doing cargo placement calculations? This only really needs to be done when arriving at an outpost, and could be rolled in to the load-screen-time, surely?), and it gets worse and worse with the more links added. I have read a lot of ppl have had this. The rest of the gameplay in the game is fine on this PC, so it's not just down to a badly spec'ed machine, I think: it's badly implemented code in the game. If cargo transport ran transparently in the background (like it does in FO4), then this would solve a lot of the inventory management issues. I just have a mod that zeros-out the weight of resources, so I carry them all around with me in my rucksack ;-)

Combat is fun, I'm not getting tired of that. And I like stealing the baddies' ships. That said, the need to use the scanner in combat to trigger some of the "social manipulations" is clunky. I know a lot of people have already told you that.

I also like planetary surveying (if a bit mindnumbing ;-), and I like building outposts, but they're a bit buggy. Still. After six months. But it's the part of the game I enjoy the most: finding the right spot to build, and then building. Object snapping could do with some work though.

The space part of the game is disappointing. There's no real travel, and no space exploration. It's just "fast travel exploration". Other games - even decades old - do this better.

I think the radiant quests in Starfield are fun though. It could do with a few more bases when doing the "kill the baddy" quests, but I'm not tiring of them yet. They are very "no-brainer", but it's telling they're more engaging than than the scripted quests. The scripted quests are a bit brain-dead and no real variation on "push the blue button so you can get to the red button… then to the yellow button…" sorta thing we had in Doom, 30 years ago. I know all RPG quests are basically like that, but it seems more apparent in SF than in FO4 or Skyrim. I think the level designers phoned-it-in a bit in Starfield.

I've played Starfield a lot (really a lot; less than I work and sleep, but more than anything else since it was released), but most of this is just as something I do with the sound off whilst I watch telly or YT, or listen to a book or use my brain and 50% of my attention for something else. This is a poor comparison to when I replay Skyrim or FO4, which I still actually enjoy.

I sure hope this is down to some vagary of the Starfield team composition or leadership, and not something that is going to bleed back in to the other franchises Bethesda own.

Right. I have some Ecliptic mercs to spacejack.

--
Adam

Sunday 17 March 2024

CFML: solving a CFML problem with Java. Kinda.

G'day:

The other day on the CMFL Slack channel, Nick Petrie asked:

Anyone know of a native or plugin-based solution to pretty-formatting XML for display on the page? I'd like to output the XML nested this:
I'll use a simplified example> we wanna render this:
<aaa><bbb ccc="ddd"><eee/></bbb></aaa>
Like this:
<aaa>
    <bbb ccc="ddd">
        <eee/>
    </bbb>
</aaa

There's no native CFML way of doing this, but I figured "this is a solved problem: there'll be an easy way to do it in Java". I googled java prettier xml, and the first match was on the Baeldung website (Pretty-Print XML in Java) which is a site I trust to have good answers. But I checked that and a few others, and they all seem to be solving the problem the same way. So I decided to run with that.

The task at hand is to convert this to CFML (still using the Java libs to do the work, I mean, just runnable in CFML):

public static String prettyPrintByTransformer(String xmlString, int indent, boolean ignoreDeclaration) {

    try {
        InputSource src = new InputSource(new StringReader(xmlString));
        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(src);

        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        transformerFactory.setAttribute("indent-number", indent);
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, ignoreDeclaration ? "yes" : "no");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");

        Writer out = new StringWriter();
        transformer.transform(new DOMSource(document), new StreamResult(out));
        return out.toString();
    } catch (Exception e) {
        throw new RuntimeException("Error occurs when pretty-printing xml:\n" + xmlString, e);
    }
}

To be very clear, this is not my code, it's from Pretty-Print XML in Java.

And also to be clear: I'm not gonna be doing anything unique or difficult or insightful or anything like that in this article. All I'm gonna do is to show how easy it is to convert native Java code to native CFML code, and this is a topical use case. It's gonna be a function with some object creation and some variable assigments. I'm gonna go line-by-line through that function above, and CFMLerise it. I just made that word up.

I'm gonna be using trycf.com to write and run this code, and the aim is to have a version that runs in both CF and Lucee.

First, let's run with the same function signature:

unformattedXml = '<aaa><bbb ccc="ddd"><eee/></bbb></aaa>' 

public string function prettyPrintByTransformer(required string xmlString, numeric indent=4, boolean ignoreDeclaration=true) {

}

prettyPrintByTransformer(unformattedXml)

In each step I'll be using that same unformattedXml input value, and just running the function to ensure it's got no compilation errors and each new statement I add "works" (in that it doesn't have runtime errors). The function won't do anything useful until it's done.

In this first step note that I've given the latter two parameters sensible defaults. This is a change from the Java version.

First things first: I don't like how they've put that largely pointless try/catch in the Java code. To me that sort of error-handling should be in the calling code if needed, not embedded in the implementation. If the implementation errors-out: let it. The actual exception will be more useful than swallowing the real exception and throwing a contrived one.

I'll include each statement I'm converting as a comment above the CFML version. This is only so I can draw your focus to it. I'd never include comments of this nature in my actual code.

public string function prettyPrintByTransformer(required string xmlString, numeric indent=4, boolean ignoreDeclaration=true) {
    // InputSource src = new InputSource(new StringReader(xmlString));
    var xmlAsStringReader = createObject("java", "java.io.StringReader").init(xmlString) // new StringReader(xmlString)
    var src = createObject("java", "org.xml.sax.InputSource").init(xmlAsStringReader)
}

I could have done this without the intermediary variable, but CFML is quite cumbersome making Java object proxies, so I think the code is clearer spread over two statements.

    // Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(src);
    var document = createObject("java", "javax.xml.parsers.DocumentBuilderFactory").newInstance().newDocumentBuilder().parse(src)
    // TransformerFactory transformerFactory = TransformerFactory.newInstance();
    var transformerFactory = createObject("java", "javax.xml.transform.TransformerFactory").newInstance()

Now: this statement did not work for me:

transformerFactory.setAttribute("indent-number", indent);

The CFML equivalent is exactly the same as it happens, but I was just getting "Not supported: indent-number" (CF) or "TransformerFactory does not recognise attribute 'indent-number'." (Lucee). I presume it's a library version difference, but I was fairly limited in my troubleshooting as I was running this on trycf.com. Although I did verify I was getting the same thing on my local CF2023 container too. I googled a bit and found a work around, but I'll come back to this a bit further down.

    // Transformer transformer = transformerFactory.newTransformer();
    var transformer = transformerFactory.newTransformer()
    /*
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, ignoreDeclaration ? "yes" : "no");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    */
    var outputKeys = createObject("java", "javax.xml.transform.OutputKeys")
    transformer.setOutputProperty(outputKeys.ENCODING, "UTF-8")
    transformer.setOutputProperty(outputKeys.OMIT_XML_DECLARATION, ignoreDeclaration ? "yes" : "no")
    transformer.setOutputProperty(outputKeys.INDENT, "yes")
    transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", indent)

Once again, I need an intermediary variable here for the outputKeys class proxy. Just to save repetition in this case.

Also note the last line there is extra: it's the solution for the bit that wasn't working earlier. Easy.

    // Writer out = new StringWriter();
    var out = createObject("java", "java.io.StringWriter").init()
    // transformer.transform(new DOMSource(document), new StreamResult(out));
    var documentAsDomSource = createObject("java", "javax.xml.transform.dom.DOMSource").init(document)
    var outAsStreamResult = createObject("java", "javax.xml.transform.stream.StreamResult").init(out)
    transformer.transform(documentAsDomSource, outAsStreamResult)

More intermediary variables here.

    // return out.toString();
    return out.toString()

And that's it. I've not got much commentary in all that lot, because it's all so straight forward [shrug].

The end result in one piece is thus:

public string function prettyPrintByTransformer(required string xmlString, numeric indent=4, boolean ignoreDeclaration=true) {
    var xmlAsStringReader = createObject("java", "java.io.StringReader").init(xmlString)
    var src = createObject("java", "org.xml.sax.InputSource").init(xmlAsStringReader)
    
    var document = createObject("java", "javax.xml.parsers.DocumentBuilderFactory").newInstance().newDocumentBuilder().parse(src)
    
    var transformerFactory = createObject("java", "javax.xml.transform.TransformerFactory").newInstance()
    var transformer = transformerFactory.newTransformer()
    
    var outputKeys = createObject("java", "javax.xml.transform.OutputKeys")
    transformer.setOutputProperty(outputKeys.ENCODING, "UTF-8")
    transformer.setOutputProperty(outputKeys.OMIT_XML_DECLARATION, ignoreDeclaration ? "yes" : "no");
    transformer.setOutputProperty(outputKeys.INDENT, "yes")
    transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", indent)
    
    var out = createObject("java", "java.io.StringWriter").init()
    
    var documentAsDomSource = createObject("java", "javax.xml.transform.dom.DOMSource").init(document)
    var outAsStreamResult = createObject("java", "javax.xml.transform.stream.StreamResult").init(out)
    transformer.transform(documentAsDomSource, outAsStreamResult)

    return out.toString()
}

And a coupla test runs:

formattedXml = prettyPrintByTransformer(unformattedXml)
writeOutput("<pre>#encodeForHtml(formattedXml)#</pre>")
<aaa>
    <bbb ccc="ddd">
        <eee/>
    </bbb>
</aaa>
formattedXml = prettyPrintByTransformer(unformattedXml, 8, false)
writeOutput("<pre>#encodeForHtml(formattedXml)#</pre>")
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<aaa>
        <bbb ccc="ddd">
                <eee/>
        </bbb>
</aaa>

The output above is from Lucee. On CF we get this:

<?xml version="1.0" encoding="UTF-8"?><aaa>
        <bbb ccc="ddd">
                <eee/>
        </bbb>
</aaa>

Note this is nothing to do with the CFML code, it'll be some Java library variation on the Lucee and CF set-ups on trycf.com.

Right, so all this lot shows is that there's no reason to shy away from implementing a CFML version of some Java code you might find that solves a problem. So in turn, if you have some "algorithmic" issue that you wanna solve in CFML, don't shy away from googling for Java solutions, and checking how easy/hard it is to convert.

A runnable version of this code is @ trycf.com.

Righto.

--
Adam