Minimum Viable Code
The previous log had a surprising strong answer, with two feedbacks:
- Nolwenn spotted a weakness in the tests
- Anton was more direct, arguing I had dodged the kata
As a reminder, we have reached the following code:
data Stats = Stats
{ diet :: Diet,
organic :: Organic,
fat :: Fat,
salt :: Salt,
calories :: Calories
}
deriving stock (Eq, Show, Generic)
deriving (Semigroup, Monoid) via (Generically Stats)
data Diet
= Pescetarian
| Vegetarian
| Vegan
deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
deriving (Semigroup, Monoid) via (Min Diet)
data Organic
= NonOrganic
| Organic
deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
deriving (Semigroup, Monoid) via (Min Organic)
newtype Fat
= Fat Natural
deriving newtype (Eq, Ord, Num, Show)
deriving (Semigroup, Monoid) via (Sum Fat)
newtype Salt
= Salt Natural
deriving newtype (Eq, Ord, Num, Show)
deriving (Semigroup, Monoid) via (Sum Salt)
newtype Calories
= Calories Natural
deriving newtype (Eq, Ord, Num, Show)
deriving (Semigroup, Monoid) via (Sum Calories)
If I had to follow the intent of the intent of the kata, I would have end up with the following snippet:
data Stats' = Stats'
{ diet :: Diet,
organic :: Organic,
fat :: Natural,
salt :: Natural,
calories :: Natural
}
deriving stock (Eq, Show, Generic)
instance Semigroup Stats' where
x <> y =
Stats'
{ diet = min x.diet y.diet,
organic = min x.organic y.organic,
fat = x.fat + y.fat,
salt = x.salt + y.salt,
calories = x.calories + y.calories
}
instance Monoid Stats' where
mempty =
Stats'
{ diet = Vegan,
organic = Organic,
fat = 0,
salt = 0,
calories = 0
}
Before highlighting the trade-offs, we should explore what I call minimum viable code.
When I review code, I focus on one thing: refactorability.
Nobody should expect a piece of code to stay unchanged, so every piece of should be evaluated to its ability to change.
So I look at:
- Architecture: coupling and cohesion, trade-offs, data management, layering
- Code structure: type-safety, composability of the interface, structure-based vs opaque body-based, composed vs complected
Let's analyze the first version:
- Architecture: each instance only know its type definition
- Code structure: strongly typed, each type can be composed and reused, structure-based, composed
And the second one:
- Architecture: only one instance of each
- Code structure: weakly typed, reused typed cannot be composed, opaque body-based, complected
Both are solving the problem, but the first one has a modular design with strong boundaries, enabling reckless refactorings.