I am learning Go by working through programming puzzles.There is shared boilerplate between puzzles that I want to factor out into into some kind of helper structure:
- Read the input (always the same, may change later, but in the same manner for all puzzles, eg. moving from reading files to HTTP)
- Prepare the input (may differ, but often identical)
- Process the input to arrive at a solution (always different)
I have come up with the solution shown below.
My specific questions are:
- Is there an idiomatic way to provide a "default method" for
PuzzleSolver.Prepare()that can be overwritten, if required? I suspect the "correct" thing to do may be to write a function with the default implementation and wire each method to call that instead, but I'm not sure. - I am unsure about
Solve(Puzzle)vsPuzzle.Solve(). Are there common reasons to prefer one over the other?
package mainimport "fmt"type PuzzleSolver interface { Prepare(*PuzzleInput) Do() string}type Puzzle struct { input *PuzzleInput solver PuzzleSolver}func (p *Puzzle) Solve() string { fmt.Println("==== Solving", p.input.ID, "====") p.solver.Prepare(p.input) ans := p.solver.Do() return ans}type PuzzleInput struct { ID int input string}func (pi *PuzzleInput) Read() { fmt.Println("Reading Puzzle with ID", pi.ID, "-- Reading is always the same.") pi.input = fmt.Sprintf("Input for Puzle %d", pi.ID)}func NewPuzzle(ID int, solver PuzzleSolver) *Puzzle { input := &PuzzleInput{ID: ID} return &Puzzle{input: input, solver: solver}}type SolverOne struct{}func (p *SolverOne) Prepare(pi *PuzzleInput) { pi.Read() fmt.Println("Preparing input for the first puzzle")}func (p *SolverOne) Do() string { ans := fmt.Sprintf("The answer to the first puzzle!") return ans}type SolverTwo struct{}func (p *SolverTwo) Prepare(pi *PuzzleInput) { pi.Read() fmt.Println("Preparing input for the second puzzle. This behaviour is actually shared with SolverOne. Can I avoid implementing SolverTwo.Prepare() explicitly?")}func (p *SolverTwo) Do() string { ans := fmt.Sprintf("The answer to the second puzzle!") return ans}type SolverThree struct { a, b int}func (p *SolverThree) Prepare(pi *PuzzleInput) { pi.Read() // process input ... p.a = 10 p.b = 20 fmt.Println("Preparing input for the third puzzle. This performs an action distinct from SolverOne and SolverTwo.")}func (p *SolverThree) Do() string { ans := fmt.Sprintf("The answer to the third puzzle is %d!", p.a+p.b) return ans}func Solve(ps *Puzzle) string { return ps.Solve()}func main() { p1 := NewPuzzle(1, &SolverOne{}) fmt.Println(Solve(p1)) p2 := NewPuzzle(2, &SolverTwo{}) fmt.Println(Solve(p2)) p3 := NewPuzzle(3, &SolverThree{}) fmt.Println(Solve(p3))}1 Answer1
Alternate design approach: minimal subset of language as needed for the problem. This immensely helps readability.
My own insight is that mostly puzzles involve input/output in other types thanstring (e.g. graph puzzles, tree puzzles, ...).
func Act[Input, Output any](prepareFn func() Input, solveFn func(input Input) Output) Output { input := prepareFn() return solveFn(input)}func Read() string { return "x"}func funkyPrepare() string { return Read() + "but more funky"}func solve1(input string) string { ...}func main() { Act(Read, solve1) Act(Read, solve2) Act(funkyPrepare, solve3)}You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.