Skip to content

Instantly share code, notes, and snippets.

@choonkeat
Last active March 6, 2020 16:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save choonkeat/3713d395f7d90687afb8dfae1db16cb0 to your computer and use it in GitHub Desktop.
Save choonkeat/3713d395f7d90687afb8dfae1db16cb0 to your computer and use it in GitHub Desktop.
"Explicit is better than implicit." Python said it first, but Python didn't say much

the init function in Go

If you're unfamiliar with it, here's a short paragraph

package mylib

func init() {
    defaultClient = new(42)
}
package main

import "mylib"

func main() {
    mylib.Doit()
}

Don't do that. Whatever you need that init function to do, make the calling package do it explicitly instead.

Scattering init in your packages means a developer will need a lot of implicit knowledge to know their way around your codebase: something is happening somewhere setting up some initial state and you have no idea unless you know where to look. That developer bumbling around the codebase could be yourself, after spending months in another project and coming back to this one, not remembering a thing.

Being explicit in what the code needs and not rely on magic happening automatically helps a lot. No surprises. You can follow code simply and track down problems. Having init somewhere.. that’ll be a fun day of debugging.

    package mylib
    
-   func init() {
+   func Initialize() {
        defaultClient = new(42)
    }
    package main
    
    import "mylib"
    
    func main() {
+       mylib.Initialize()
        mylib.Doit()
    }

👌 explicit better than init

Assuming I've convinced you to not use init, let me continue and say, don't just move an implicit init() into an explicit mylib.Initialize() too.

func main() {
    mylib.Initialize()
    mylib.Doit()
}

Whatever is going on inside mylib.Doit(), it's a safe guess to say your Doit function needs something to have been setup in some particular way or else it would fail. Correct?

func Doit()

But, looking at the function by itself, how does a developer know / remember to call mylib.Initialize() before calling mylib.Doit()?

Read every word in the function documentation? Step through the function source code? Hope they included mylib.Initialize() when they copy-pasted how other places were using mylib.Doit()? Wait for the test to fail and scratch their head? Wait for runtime errors and read about it on Sentry? Or worse?

Instead of blaming programmers for writing imperfect code, why not make such mistakes impossible?

Usability of our function

One way to improve the function's usability is to make the implicit dependencies of our code as explicit function parameters.

- function Doit()
+ function Doit(client Client)

Now in order to use Doit, the developer has to first find a Client value somewhere. Various bad things can happen based on other details, but "forgetting to do something before calling Doit" is no longer a possible mistake.

Certainly there could be other approaches that you can think of.

Apply it recursively with a fresh eye towards usability and safety, you'll be surprised how many landmines can be eliminated; how many landmines there were.

Object Oriented Programming

If you haven't noticed, I'd like to point out that methods in OOP relies on "implicit dependencies": the state of the instance (all its attributes) can be used in a method.

func (object Thing) DoIt()

You'll never know, which object attribute (attributes? all of them? none of them?) are being used in a method call unless you step through the source code.

func (object Thing) DoIt() {
    object.defaultClient.doIt(object.currentPage)
}

When you see some code calling the method x.DoIt(), do you remember to ask, "is x.defaultClient and x.currentPage set?". What lies underneath the defaultClient.doIt method?

How can we go about improving OOP methods to prevent the unnecessary mistakes we've discussed?

how do you know [some object state or implicit global variable] was set before this function call? ... you'll never know, which object attribute are being used in a method call unless you step through the source code.

This is actually quite nebulous and weak sauce argument. Why does it matter? Let's get concrete.

Say there's a new thing that we need to do

// new method for class `Thing` !
func (object Thing) DoSomething(s string) {
    object.clientX.Write(object.Name, s)
}

and we need it to be done in various places

func (object Thing) HandleHTTP(request, response) {
	object.Tags = request.Get('tags')
	someService(object)
	object.Name = request.Get('name')
	object.Process()
	// <-- object.DoSomething("foobar")
	backgroundJob(object)
	...

func backgroundJob(object Thing) {
	object.Validate()
	// <-- object.DoSomething("foobar")
	...
	
func someService(object Thing) {
	object.Confirm()
	// <-- object.DoSomething("foobar")
	...

So, how do we know if we can just slide in our object.DoSomething("foobar") into those lines and that it'll work?

  • Specifically, under which context does object come with a ready to use object.clientX?
  • In which context do we have to setup a fresh object.clientX for this new feature?
  • And object.Name?

in an alternate universe

Say we pass our dependencies in as function parameters instead. But same logic.

// new function!
func DoSomething(clientX, name, s string) {
    clientX.Write(name, s)
}

to call DoSomething, we need a clientX, name and s

func HandleHTTP(db, clientX, request, response) {
	someService(db, clientX, request.Get('tags'))
	name = request.Get('name')
	process(clientX, name)
	// <-- DoSomething(clientX, name, "foobar")
	backgroundJob(db, name)
	...

func backgroundJob(db, name) {
	validate(db, name)
	// <-- DoSomething(clientX, name, "foobar")
	...
	
func someService(db, clientX, tags) {
	confirm(db, clientX, tags...)
	// <-- DoSomething(clientX, name, "foobar")
	...

It isn't a mystery which places will need some changes to bring in a missing clientX or a missing name

-func backgroundJob(db, name) {
+func backgroundJob(db, name, clientX) {
	validate(db, name)
+	DoSomething(clientX, name, "foobar")
	...

-func someService(db, clientX, tags) {
+func someService(db, clientX, tags, name) {
	confirm(db, clientX, tags...)
+	DoSomething(clientX, name, "foobar")
	...

and finish off with the necessary changes to func HandleHTTP.

Note that we can do all this confidently without peeking into implementations of process, validate, or confirm -- unlike the OOP version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment