I needed to set the default value if a user did not put the property. There were several ways to set a default value, but reaching the simplest way took me a while.
The possible solutions are as follows.
- switch-case or if-else
- map
- strategy pattern
- function
- struct
Using struct would be the simplest. Let’s look at the code one by one.
I ran into the issue. I first implemented it in a strategy pattern but changed it to a simple solution.
The struct that we want to set default values
Let’s think about a hotel booking. We have some plans and each plan has its default value.
Let’s define the default values in the table below.
Items | A | B | C |
---|---|---|---|
bedType | singleBed | doubleBed | queenBed |
persons | 1 | 2 | 1 |
numberOfBeds | 1 | 2 | 1 |
numberOfNights | 1 | 1 | 3 |
Then, we define the struct and the necessary info.
type planType string
type bedType string
const (
planTypeA planType = "A"
planTypeB planType = "B"
planTypeC planType = "C"
)
const (
singleBed bedType = "single"
doubleBed bedType = "double"
queenBed bedType = "queen"
kingBed bedType = "king"
)
type planInfo struct {
plan planType
bedType *bedType
persons *int
numberOfBeds *int
numberOfNights *int
}
func (m planInfo) print() {
fmt.Printf("plan: %s, ", m.plan)
if m.bedType == nil {
fmt.Printf("bedType: nil")
} else {
fmt.Printf("bedType: %s", *m.bedType)
}
if m.persons == nil {
fmt.Printf(", persons: nil")
} else {
fmt.Printf(", persons: %d", *m.persons)
}
if m.numberOfBeds == nil {
fmt.Printf(", numberOfBeds: nil")
} else {
fmt.Printf(", numberOfBeds: %d", *m.numberOfBeds)
}
if m.numberOfNights == nil {
fmt.Printf(", numberOfNights: nil\n")
} else {
fmt.Printf(", numberOfNights: %d\n", *m.numberOfNights)
}
}
Assume that the values are set by someone else. However, we write the following code to make it simple and easy to read. funcName
will be replaced with one of the actual function name. The first 3 function calls should add the default value but the last one shouldn’t because it already specifies all properties.
func SetDefaultValue() {
fmt.Println("---funcName---")
funcName(planInfo{plan: planTypeA})
funcName(planInfo{plan: planTypeB})
funcName(planInfo{plan: planTypeC})
chosenBedType := kingBed
numberOfPersons := 2
funcName(planInfo{plan: planTypeC, bedType: &chosenBedType, persons: &numberOfPersons})
}
Let’s look at the actual implementation based on this code.
Switch-case or if-else
The default value must be set only when the target property is not set. In each property check, we add the default value there.
func useSwitchCase1(planInfo planInfo) {
if planInfo.persons == nil {
var defaultValue int
switch planInfo.plan {
case planTypeA:
defaultValue = 1
case planTypeB:
defaultValue = 2
case planTypeC:
defaultValue = 1
}
planInfo.persons = &defaultValue
}
if planInfo.bedType == nil {
var defaultValue bedType
switch planInfo.plan {
case planTypeA:
defaultValue = singleBed
case planTypeB:
defaultValue = doubleBed
case planTypeC:
defaultValue = queenBed
}
planInfo.bedType = &defaultValue
}
if planInfo.numberOfBeds == nil {
var defaultValue int
switch planInfo.plan {
case planTypeA:
defaultValue = 1
case planTypeB:
defaultValue = 2
case planTypeC:
defaultValue = 1
}
planInfo.numberOfBeds = &defaultValue
}
if planInfo.numberOfNights == nil {
var defaultValue int
switch planInfo.plan {
case planTypeA:
defaultValue = 1
case planTypeB:
defaultValue = 1
case planTypeC:
defaultValue = 3
}
planInfo.numberOfNights = &defaultValue
}
planInfo.print()
}
But this looks horrible. switch-case
is written in each if clause. We should use the switch-case
only once.
func useSwitchCase2(planInfo planInfo) {
var defaultPersons int
var defaultBedType bedType
var defaultNumberOfBeds int
var defaultNumberOfNights int
switch planInfo.plan {
case planTypeA:
defaultPersons = 1
defaultBedType = singleBed
defaultNumberOfBeds = 1
defaultNumberOfNights = 1
case planTypeB:
defaultPersons = 2
defaultBedType = doubleBed
defaultNumberOfBeds = 2
defaultNumberOfNights = 1
case planTypeC:
defaultPersons = 1
defaultBedType = queenBed
defaultNumberOfBeds = 1
defaultNumberOfNights = 3
}
if planInfo.persons == nil {
planInfo.persons = &defaultPersons
}
if planInfo.bedType == nil {
planInfo.bedType = &defaultBedType
}
if planInfo.numberOfBeds == nil {
planInfo.numberOfBeds = &defaultNumberOfBeds
}
if planInfo.numberOfNights == nil {
planInfo.numberOfNights = &defaultNumberOfNights
}
planInfo.print()
}
It looks easier to read. The code looks similar if we use if-else instead.
Set default values in map
We can use map instead of switch-case or if-else. We need to define the default values for each plan. Therefore, we can define the map in the following way.
var defaultValueMap = map[planType]map[string]any{
planTypeA: {
"bedType": singleBed,
"persons": 1,
"numberOfBeds": 1,
"numberOfNights": 1,
},
planTypeB: {
"bedType": doubleBed,
"persons": 2,
"numberOfBeds": 2,
"numberOfNights": 1,
},
planTypeC: {
"bedType": queenBed,
"persons": 1,
"numberOfBeds": 1,
"numberOfNights": 3,
},
}
But we should check whether the key exists when we extract a value from a map. So additional check is required for each value. If all properties have the same data type, the code becomes easier but if it has a different type, the code gets more complicated because of conversion.
func useMap(planInfo planInfo) {
defaultMap, ok := defaultValueMap[planInfo.plan]
if !ok {
fmt.Printf("no default for plan %s", planInfo.plan)
return
}
if planInfo.persons == nil {
if val, ok := defaultMap["persons"]; ok {
if intValue, err := convertAnyToInt(val); err == nil {
planInfo.persons = intValue
}
}
}
if planInfo.bedType == nil {
if val, ok := defaultMap["bedType"]; ok {
if bedTypeValue, err := convertAnyToBedType(val); err == nil {
planInfo.bedType = bedTypeValue
}
}
}
if planInfo.numberOfBeds == nil {
if val, ok := defaultMap["numberOfBeds"]; ok {
if intValue, err := convertAnyToInt(val); err == nil {
planInfo.numberOfBeds = intValue
}
}
}
if planInfo.numberOfNights == nil {
if val, ok := defaultMap["numberOfNights"]; ok {
if intValue, err := convertAnyToInt(val); err == nil {
planInfo.numberOfNights = intValue
}
}
}
planInfo.print()
}
func convertAnyToInt(val any) (*int, error) {
ret, ok := val.(int)
if !ok {
return nil, errors.New("conversion error")
}
return &ret, nil
}
func convertAnyToBedType(val any) (*bedType, error) {
ret, ok := val.(bedType)
if !ok {
return nil, errors.New("conversion error")
}
return &ret, nil
}
This is even harder to read than switch-case…
Apply strategy pattern for default values
Strategy pattern is also one of the possibilities. Golang can’t define properties in interface, so we have to define functions to return a default value.
type getDefaults interface {
getPersons() int
getBedType() bedType
getNumberOfBeds() int
getNumberOfNights() int
}
type planADefaults struct{}
func (planADefaults) getPersons() int { return 1 }
func (planADefaults) getBedType() bedType { return singleBed }
func (planADefaults) getNumberOfBeds() int { return 1 }
func (planADefaults) getNumberOfNights() int { return 1 }
type planBDefaults struct{}
func (planBDefaults) getPersons() int { return 2 }
func (planBDefaults) getBedType() bedType { return singleBed }
func (planBDefaults) getNumberOfBeds() int { return 2 }
func (planBDefaults) getNumberOfNights() int { return 1 }
type planCDefaults struct{}
func (planCDefaults) getPersons() int { return 1 }
func (planCDefaults) getBedType() bedType { return queenBed }
func (planCDefaults) getNumberOfBeds() int { return 1 }
func (planCDefaults) getNumberOfNights() int { return 3 }
We need to assign the actual struct in switch-case but the function has fewer lines than using only switch-case that I showed above.
func useStrategy(planInfo planInfo) {
var defaultsGetter getDefaults
switch planInfo.plan {
case planTypeA:
defaultsGetter = planADefaults{}
case planTypeB:
defaultsGetter = planBDefaults{}
case planTypeC:
defaultsGetter = planCDefaults{}
}
if planInfo.persons == nil {
val := defaultsGetter.getPersons()
planInfo.persons = &val
}
if planInfo.bedType == nil {
val := defaultsGetter.getBedType()
planInfo.bedType = &val
}
if planInfo.numberOfBeds == nil {
val := defaultsGetter.getNumberOfBeds()
planInfo.numberOfBeds = &val
}
if planInfo.numberOfNights == nil {
val := defaultsGetter.getNumberOfNights()
planInfo.numberOfNights = &val
}
planInfo.print()
}
But I think applying strategy pattern is too much for this purpose because it doesn’t have any logic there. Each struct has just a value in each function.
Define a function
If strategy pattern is too much, function is also another solution.
type getDefaultValues func() (int, bedType, int, int)
func getDefaultValuesForPlanA() (int, bedType, int, int) {
person := 1
bedType := singleBed
numberOfBeds := 1
numberOfNights := 1
return person, bedType, numberOfBeds, numberOfNights
}
func getDefaultValuesForPlanB() (int, bedType, int, int) {
person := 2
bedType := doubleBed
numberOfBeds := 2
numberOfNights := 1
return person, bedType, numberOfBeds, numberOfNights
}
func getDefaultValuesForPlanC() (int, bedType, int, int) {
person := 1
bedType := queenBed
numberOfBeds := 1
numberOfNights := 3
return person, bedType, numberOfBeds, numberOfNights
}
getDefaultValues
type is not used here because the function is called in the following function.
func useFunction(planInfo planInfo) {
var defaultPersons int
var defaultBedType bedType
var defaultNumberOfBeds int
var defaultNumberOfNights int
switch planInfo.plan {
case planTypeA:
defaultPersons, defaultBedType, defaultNumberOfBeds, defaultNumberOfNights = getDefaultValuesForPlanA()
case planTypeB:
defaultPersons, defaultBedType, defaultNumberOfBeds, defaultNumberOfNights = getDefaultValuesForPlanB()
case planTypeC:
defaultPersons, defaultBedType, defaultNumberOfBeds, defaultNumberOfNights = getDefaultValuesForPlanC()
default:
}
if planInfo.persons == nil {
planInfo.persons = &defaultPersons
}
if planInfo.bedType == nil {
planInfo.bedType = &defaultBedType
}
if planInfo.numberOfBeds == nil {
planInfo.numberOfBeds = &defaultNumberOfBeds
}
if planInfo.numberOfNights == nil {
planInfo.numberOfNights = &defaultNumberOfNights
}
planInfo.print()
}
But if we want, we can extract the switch-case to outside and make the function easier to read.
func useFunction(planInfo planInfo, cb getDefaultValues) {
defaultPersons, defaultBedType, defaultNumberOfBeds, defaultNumberOfNights := cb()
if planInfo.persons == nil {
planInfo.persons = &defaultPersons
}
if planInfo.bedType == nil {
planInfo.bedType = &defaultBedType
}
if planInfo.numberOfBeds == nil {
planInfo.numberOfBeds = &defaultNumberOfBeds
}
if planInfo.numberOfNights == nil {
planInfo.numberOfNights = &defaultNumberOfNights
}
planInfo.print()
}
It looks better than before but this way has a problem. The function could return those default values in a different order by mistake. This can’t be checked by a tool.
The simplest and best solution is using struct
The simplest and best solution would be using struct. It doesn’t have any order of the properties. When we assign a default value, we have to write the property name as well.
type planDefaults struct {
bedType bedType
persons int
numberOfBeds int
numberOfNights int
}
func useStruct(planInfo planInfo) {
var defaultsGetter planDefaults
switch planInfo.plan {
case planTypeA:
defaultsGetter = planDefaults{
bedType: singleBed,
persons: 1,
numberOfBeds: 1,
numberOfNights: 1,
}
case planTypeB:
defaultsGetter = planDefaults{
bedType: doubleBed,
persons: 2,
numberOfBeds: 2,
numberOfNights: 1,
}
case planTypeC:
defaultsGetter = planDefaults{
bedType: queenBed,
persons: 1,
numberOfBeds: 1,
numberOfNights: 3,
}
}
if planInfo.persons == nil {
planInfo.persons = &defaultsGetter.persons
}
if planInfo.bedType == nil {
planInfo.bedType = &defaultsGetter.bedType
}
if planInfo.numberOfBeds == nil {
planInfo.numberOfBeds = &defaultsGetter.numberOfBeds
}
if planInfo.numberOfNights == nil {
planInfo.numberOfNights = &defaultsGetter.numberOfNights
}
planInfo.print()
}
We can pass the struct as a parameter. Then, the function looks much easier to read.
func useStruct(planInfo planInfo, defaultsGetter planDefaults) {
if planInfo.persons == nil {
planInfo.persons = &defaultsGetter.persons
}
if planInfo.bedType == nil {
planInfo.bedType = &defaultsGetter.bedType
}
if planInfo.numberOfBeds == nil {
planInfo.numberOfBeds = &defaultsGetter.numberOfBeds
}
if planInfo.numberOfNights == nil {
planInfo.numberOfNights = &defaultsGetter.numberOfNights
}
planInfo.print()
}
I didn’t come up with this idea until my team member pointed it out in my PR. Think simple, easy. But it’s not always the first idea to catch the simplest, easiest way.
I will take this technique into my tool pocket.
By the way, it could also be a good idea to implement functions to set the default value in the struct and call it in the business logic (useStruct
func in this case). Then, there will be no if-else in the function. We can write the test cases one by one for the corresponding function.
Comments