13brane's holidazed cavalcade of code shenanigans: the flavors of FizzBuzz - Days 17-24

 

(Full source code for the entire series will be progressively published here, and the other days of the cavalcade are here: day 0, 1-8, 9-16)

Welcome back to the last instalment of the cavalcade. This week we are going to do something evil. Something forbidden. Something so egregiously deranged it might cause uproar with religious leaders.

Lo and behold, object oriented fizzbuzzing.

Day 17

New tech lead is hired. Asks junior dev to convert the previous implementation of Fizzbuzz to OOP. Junior dev, puzzled by this request, asks as politely as possible, why the hell would someone do that. Tech lead answers with some badly strung together argument about DI and logging. Junior dev does not understand why FizzBuzz needs DI (or logging, for that matter) and continues to ask questions. Conversation abruptly ends with “Just put it in an object.”

package day17

import "fmt"

// default implementation, for acceptance test
var FizzBuzz func(int) string = NewFizzBuzzer().FizzBuzz

type FizzBuzzer struct{}

func NewFizzBuzzer() *FizzBuzzer {
	return &FizzBuzzer{}
}

func (fb *FizzBuzzer) FizzBuzz(n int) string {
	if n%3 == 0 && n%5 == 0 {
		return "fizzbuzz"
	} else if n%3 == 0 {
		return "fizz"
	} else if n%5 == 0 {
		return "buzz"
	} else {
		return fmt.Sprint(n)
	}
}

Junior dev obliges. Goes home that night, starts reading Lambda The Ultimate.

This is how we create anti-OOP fundamentalists. Tech leads, take note.

Day 18

A hip new web startup decides to create an entirely new segment of the market: FBaaS, FizzBuzz as a Service.

An MVP is launched. Seed and Series A rounds are a massive success due to the disruptive potential to the FizzBuzzing industry. We are not entirely sure how much disruption can occur to a market segment that doesn’t exist, or if it’s actually good or necessary, but it’s in the business plan, so it must be true.

In order to future proof the solution and prepare for the “exponential” growth that is expected, the core of the service is implemented as a sort of routing engine library, but for integers.

A new Silicon Valley unicorn is clearly in the making here. They have issued only 3 API keys so far, and one of those is for the CI server, but hey, who am I to give investment advice.

Hustle to your heart’s content.

package day18

import "fmt"

var FizzBuzz func(int) string = InitFizzBuzz()

func InitFizzBuzz() func(int) string {
	c := NewNumberConverter()

	c.AddRule(15, "fizzbuzz")
	c.AddRule(3, "fizz")
	c.AddRule(5, "buzz")

	c.SetDefaultRule(func(n int) string {
		return fmt.Sprint(n)
	})

	return c.ApplyRules
}

// Implementation of NumberConverter Engine

type rule struct {
	i int
	s string
}

type NumberConverter struct {
	rules       []rule
	defaultRule func(int) string
}

func NewNumberConverter() *NumberConverter {
	return &NumberConverter{}
}

func (c *NumberConverter) AddRule(i int, s string) {
	c.rules = append(c.rules, rule{i, s})
}

func (c *NumberConverter) SetDefaultRule(f func(int) string) {
	c.defaultRule = f
}

func (c *NumberConverter) ApplyRules(n int) string {
	for _, r := range c.rules {
		if n%r.i == 0 {
			return r.s
		}
	}

	return c.defaultRule(n)
}

Day 19

Established FBaaS solution. As the company grew, the scarcity of Go programmers led them to hire expats from other environments. Slowly but surely, the codebase begins to OOssify with Javaisms, and *er classes, factories, interfaces and implementations start to appear. All the things, encapsulate.

Thank you Griesemer, Pike and Thompson, for leaving inheritance out. At least that bullet was dodged.

// day19/buzzer.go
package day19

type BuzzerImpl struct{}

func (f *BuzzerImpl) Buzz(fb FBState) {
	if fb.Number()%5 == 0 {
		fb.SetState(fb.Number(), fb.String()+"buzz")
	}
}

// ----------------------------------------------
// day19/factories.go
package day19

func NewFBState(n int) *FBStateImpl {
	return &FBStateImpl{n, ""}
}

func NewFormater() *FormaterImpl {
	return &FormaterImpl{}
}

func NewFizzer() *FizzerImpl {
	return &FizzerImpl{}
}

func NewBuzzer() *BuzzerImpl {
	return &BuzzerImpl{}
}

func NewFizzBuzzManager() *FizzBuzzManagerImpl {
	return &FizzBuzzManagerImpl{
		NewFizzer(),
		NewBuzzer(),
		NewFormater(),
	}
}

// ----------------------------------------------
// day19/fbstate.go
package day19

type FBStateImpl struct {
	i int
	s string
}

func (f *FBStateImpl) SetState(i int, s string) {
	f.i = i
	f.s = s
}

func (f *FBStateImpl) Number() int {
	return f.i
}

func (f *FBStateImpl) String() string {
	return f.s
}

// ----------------------------------------------
// day19/fizzer.go
package day19

type FizzerImpl struct{}

func (f *FizzerImpl) Fizz(fb FBState) {
	if fb.Number()%3 == 0 {
		fb.SetState(fb.Number(), fb.String()+"fizz")
	}
}

// ----------------------------------------------
// day19/formatter.go
package day19

import "fmt"

type FormaterImpl struct{}

func (_ *FormaterImpl) Format(f FBState) string {
	if len(f.String()) == 0 {
		return fmt.Sprint(f.Number())
	} else {
		return f.String()
	}
}

// ----------------------------------------------
// day19/interfaces.go
package day19

// adapter for test harness
var FizzBuzz func(int) string = NewFizzBuzzManager().Manage

type FBState interface {
	SetState(int, string)
	Number() int
	String() string
}

type Fizzer interface {
	Fizz(FBState)
}

type Buzzer interface {
	Buzz(FBState)
}

type Formater interface {
	Format(FBState) string
}

// ----------------------------------------------
// day19/manager.go
package day19

type FizzBuzzManagerImpl struct {
	f Fizzer
	b Buzzer
	m Formater
}

func (i *FizzBuzzManagerImpl) Manage(n int) string {
	s := NewFBState(n)
	i.f.Fizz(s)
	i.b.Buzz(s)
	return i.m.Format(s)
}

Day 20

This one is special. Code will not be pasted inline: I’d need a new rant just for it. See it here instead.

After 12 years of operation in the FBaaS market, many developers that came and went and the realization that “exponential” means different things for mathematicians than it does for the rest of the world, the company’s codebase is fully OOssified.

There are interfaces for everything, implementations for everything, factories for some things, a manager object that does pretty much all the business logic, a DI library refactoring that someone started but clearly couldn’t finish, and even stale code that is no longer used but people are afraid to delete because of Justin Case and their selective ignorance about source control. Glorious.

This codebase has clearly reached it’s maturity and enlightenment, the peak of it’s existence. From this point on maintenance efforts will continue to increase, adding new features will become next to impossible, and there will be louder and louder calls for a rewrite. Its light will slowly dim over time, waiting for the inevitable. The much sought rewrite, or the scrapping of the entire project.

The cycle of software life. Unstoppable, unending.

Day 21

Unless you do it correctly.

Here it is, the generalized ([a-y][a-y]zz){n} algorithm, as implemented by someone extremely competent in the arts and sciences of computer bending. The type of person that refactors relentlessly until that last unneeded line of code can be removed. The type of person that reads SICP for relaxation, Clean Code for fun, and 13brane.net for a laugh.

This started out as OO code. There was a Rules(...).Apply there. Seriously. There was even a type Rules and another type Rule, of all things. But the continuous massaging of the code eventually made it shed all the extra fat, culminating in the following masterpiece.

package day21

import "fmt"

var FizzBuzz func(n int) string = Apply(
	Rule{3, "fizz"},
	Rule{5, "buzz"},
)

// DSL implementation ---

func Apply(rules ...Rule) func(int) string {
	return func(n int) string {
		s := ""

		for _, r := range rules {
			if n%r.divisor == 0 {
				s = s + r.part
			}
		}

		if s == "" {
			return fmt.Sprint(n)
		}

		return s
	}
}

type Rule struct {
	divisor int
	part    string
}

Day 22

The company referred to in Day 20 was eventually acquired by a larger and older competitor. Mostly for the IP (which turned out to be next to worthless) and domain knowledge. It is decided that the FBaaS solution will finally be retired in favor of a more traditional software delivery method and a more familiar business model.

Since a rewrite is due, the OSS code from Day 21 is pilfered with no attribution whatsoever, to build the next product with optimal time-to-market.

So, without further ado, we present, SmarTazz: all-in-one, XML configurable, abstract enterprise rules engine for all your on-prem FizzBuzzBazzing needs.

Licensing starts at $5000/yr, per node. Optional round-the-clock support contract and mass discounts available (contact sales for a quote). And of course, it crashes with a cryptic error if you feed it bad configuration, like any respectable enterprise grade software.

The rules engine:

package day22

import (
	"encoding/xml"
	"fmt"
	"io/ioutil"
	"os"
)

const CONFIG_PATH_ENV_VAR = "DAY22_CONFIG_PATH"

// FizzBuzz needs to be lazily initialized due to the use of `os.Env`.
// See test harness.
// var FizzBuzz func(n int) string = MakeXMLRulesApplicator()

func MakeXMLRulesApplicator() func(int) string {

	path := os.Getenv(CONFIG_PATH_ENV_VAR)
	config := LoadConfig(LoadConfigFileBytes(path))
	rules := config.Rules

	return func(n int) string {
		s := ""

		for _, r := range rules {
			if n%r.Divisor == 0 {
				s = s + r.Part
			}
		}

		if s == "" {
			return fmt.Sprint(n)
		}

		return s
	}
}

func LoadConfig(configBytes []byte) Config {
	config := Config{}

	err := xml.Unmarshal(configBytes, &config)
	if err != nil {
		panic(err)
	}

	return config
}

func LoadConfigFileBytes(path string) []byte {
	configBytes, err := ioutil.ReadFile(path)
	if err != nil {
		panic(err)
	}

	return configBytes
}

type Config struct {
	XMLName xml.Name `xml:"config"`
	Rules   []Rule   `xml:"rules>rule"`
}

type Rule struct {
	Divisor int    `xml:"divisor,attr"`
	Part    string `xml:"part,attr"`
}

Standard FizzBuzz configuration:

<?xml version="1.0" encoding="UTF-8"?>
<config>
	<rules>
		<rule divisor="3" part="fizz" />
		<rule divisor="5" part="buzz" />
	</rules>
</config>

Day 23

@carlosdavidepto, trying to showoff Go prowess by one-upping the l33ts, using a purely declarative approach which exploits maps instead of slices. No helper functions or if statements needed.

package day23

import "fmt"

func FizzBuzz(n int) string {
	return map[bool]map[bool]string{
		true:  {true: "fizzbuzz", false: "fizz"},
		false: {true: "buzz", false: fmt.Sprint(n)},
	}[n%3 == 0][n%5 == 0]
}

Day 24

@carlosdavidepto, solving FizzBuzz in job interviews with recruiters who clearly did not read the resume, while trying to wrap up quickly to go home for the holidays.

package day24

import "fmt"

func FizzBuzz(n int) string {
	if n%3 == 0 {
		if n%5 == 0 {
			return "fizzbuzz"
		} else {
			return "fizz"
		}
	} else {
		if n%5 == 0 {
			return "buzz"
		} else {
			return fmt.Sprint(n)
		}
	}
}

This solution is probably the simplest and dumbest (and therefore maybe the easiest to understand), but it has the interesting properties of being purely functional (no internal state) and executing exactly two branches per function call for the core logic. Except maybe on Intel CPUs. We aren’t sure what executes on those anymore.

Final Reflections
  • I had way too much fun doing this series.
  • Sometimes, doing (close to) nothing is the best course of action. The simplest change is more often than not the correct change.
  • Things can ALWAYS be made more complicated.
  • Simplicity on the other hand, has at least one limit. Unlike rational numbers, however, code may have more than one irreducible form.
  • Unrefactoring can be as difficult as refactoring.
  • Making good use of Functional Programming doesn’t require a Math PhD.
  • FP has a vast array of tools in it’s belt, but there’s no need to use every single one of them every time. Belt grinders are not meant for tightening screws. Use a screwdriver instead.
  • You can build DSLs without access to macros, hygienic or not. An XML configuration file is, in a way, a DSL.
  • Not everything needs a DSL, though. And there is nothing that really needs XML.
  • encoding/xml is so good, it almost makes you think XML was a good idea. It might even be dangerous.
  • Automated acceptance tests cut development time in half. Use them.