(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.
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.
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)
}
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)
}
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.
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
}
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>
@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]
}
@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.
encoding/xml
is so good, it almost makes you think XML was
a good idea. It might even be dangerous.