Code bloat

Code bloat smells are cases where unwieldy slabs of code have been added to structs or functions so that they have become hard to understand, maintain, and test. Frequently found in older code, they are often the result of a gradual degradation and lack of maintenance rather than intentional choices.

They can be found with a visual scan of the source code or by employing a cyclomatic complexity checker (a software metric that indicates the complexity of a piece of code) such as gocyclo (https://github.com/fzipp/gocyclo).

These smells include the following:

  • Long methods: While the code is run on computers, it is written for humans. Any method of more than about 30 lines should be split into smaller chunks. While it makes no difference to the computer, it makes it easier for us humans to understand.
  • Long structs: Similar to long methods, the longer a struct, the harder it is to understand and therefore maintain. Long structs typically also indicate the struct is doing too much. Splitting one struct into several smaller ones is also a great way to increase the reusability potential of the code.
  • Long parameter lists: Long parameter lists also indicate that the method is likely doing more than it should. When adding new features, it is tempting to add a new parameter to an existing function to account for the new use case. This is a slippery slope. This new parameter is either optional/unnecessary for the existing use cases or is an indication of a significant increase in complexity in the method.
  • Long conditional blocks: Switch statements are amazing. The problem is they are very easy to abuse and tend to multiply like proverbial rabbits. Perhaps the most significant problem, however, is their effect on the readability of the code. Long conditional blocks take up a lot of space and interrupt the readability of the function. Consider the following code:
func AppendValue(buffer []byte, in interface{}) []byte{
var value []byte

// convert input to []byte
switch concrete := in.(type) {
case []byte:
value = concrete

case string:
value = []byte(concrete)

case int64:
value = []byte(strconv.FormatInt(concrete, 10))

case bool:
value = []byte(strconv.FormatBool(concrete))

case float64:
value = []byte(strconv.FormatFloat(concrete, 'e', 3, 64))
}

buffer = append(buffer, value...)
return buffer
}

By taking interface{} as input, anywhere we wish to use it, we are almost forced to have a switch like this one. We would be better off changing from interface{} to an interface and then adding the necessary operations to the interface. This approach is better illustrated by the json.Marshaller and driver.Valuer interfaces in the standard library.

Applying DI to these smells will typically reduce the complexity of individual pieces of code by breaking them into smaller, separate pieces, which in turn makes them easier to understand, maintain, and test.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.36.231