Problem Link to heading
You’ve written an object or class. It’s somewhat complex and you want to iterate ways to better abstract your code but are unsure how to start. There exists documentation about what a good abstraction looks like, but no concrete iterative steps.
Being deep inside your own code can make you blind to obvious alternatives.
Solution Link to heading
Create a bipartite graph of instance methods and member variables. Draw edges between methods and the variables they use. Then move isolated components into separate classes.
Concrete, Simple Example Link to heading
A spaceship Link to heading
Assume you’re designing a video game with a spaceship. spaceships are circles that have some amount of health they can also heal (if they aren’t dead already). spaceships also have some location and you need to know if two spaceships intersect.
type Ship struct {
Health int
LocX float64
LocY float64
Size float64
}
func (s *Ship) Heal(amnt int) {
if s.Health >= 0 {
s.Health += amnt
}
}
func (s *Ship) Harm(amnt int) {
if s.Health >= 0 {
s.Health -= amnt
}
}
func (s *Ship) Intersects(other Ship) bool {
dist := math.Sqrt(math.Pow(s.LocX-other.LocX, 2) + math.Pow(s.LocY-other.LocY, 2))
return dist < s.Size+other.Size
}
Let’s created a bipartite graph of instance methods and member variables to see if we can simplify this abstraction.
Notice how we have two disconnected subgraphs. One connecting health with heal or harm, and another connecting intersects with location and size. This is a sign we are probably dealing with two separate game concepts: one about killing actors of the game and another about objects with physical locations. We can use composition to break our ship object into two concepts.
type Ship2 struct {
GameKillable
PhysicalObject
}
type PhysicalObject struct {
LocX float64
LocY float64
Size float64
}
func (s *PhysicalObject) Intersects(other PhysicalObject) bool {
dist := math.Sqrt(math.Pow(s.LocX-other.LocX, 2) + math.Pow(s.LocY-other.LocY, 2))
return dist < s.Size+other.Size
}
type GameKillable struct {
Health int
}
func (s *GameKillable) Heal(amnt int) {
if s.Health >= 0 {
s.Health += amnt
}
}
func (s *GameKillable) Harm(amnt int) {
if s.Health >= 0 {
s.Health -= amnt
}
}
Notice how Ship2
still does the same amount of things, but we can now reason about those things as smaller units.
As a bonus, we have discovered the concepts of physical objects and killable objects. You could imagine making smaller
interfaces or packages around these atomic units, spreading this abstraction to other places of your game.
Conclusion Link to heading
It’s not necessary to literally draw out a bipartite graph to simplify software, but knowing the process is something you can often do in your head to create smaller abstractions.
When hunting for simplified abstractions, consider the interactions between member variables and member functions