Study with me for Code Refactoring ( Story 5— Primitive Obsession of Bloaters)

Thaw Zin Toe
6 min readFeb 7, 2023

Primitive obsession is when a developer uses simple data types like integers, booleans, and strings too often in their code instead of creating custom classes to represent more complex information. This can make the code hard to read and understand and harder to maintain and test.

Symptoms

  • Overuse of simple data types (such as integers, strings, and booleans) to represent complex information.
# Overuse of integers to represent currency
val dollar: Int = 10
val cents: Int = dollars * 100 % 100

#Improvement
class Money(var dollars: Double) {
fun getCents(): Int {
return (dollars * 100 % 100).toInt()
}
}
  • Use primitives instead of small objects for simple tasks (such as currency, ranges, special strings for phone numbers, etc.)
# primitive obsession
val price: Double = 10.0

# Improvement
data class Currency(val amount: Double, val type: String)
val price: Currency = Currency(10.0, "USD")
  • Using constants to display information such as ( VIP = 0, Customer = 1 , Guest = 2)
# primitive obsession
val VIP= 0
val customer = 1
val Guest = 2

# Improvement
enum class CustomerType{
VIP,
CUSTOMER,
GUEST
}
-------------
sealed class CustomerType {
object VIP : CustomerType()
object CUSTOMER : CustomerType()
object GUEST : CustomerType()
}
  • Use of string constants as field names for use in data arrays.
const val NAME_FIELD = "name"
const val AGE_FIELD = "age"
const val ADDRESS_FIELD = "address"

val user = mapOf(NAME_FIELD to "John Doe", AGE_FIELD to 30, ADDRESS_FIELD to "123 Main St.")

Treatment

Replace Data Value with Object — If you have many different basic data fields, it may be possible to organize some of them into their separate class. It is even better if you can also move the actions related to this data into the class. For this task, consider using the technique

val colorValues = listOf("red", "green", "blue")

fun printColor(value: String) {
when (value) {
"red" -> println("The color is red")
"green" -> println("The color is green")
"blue" -> println("The color is blue")
else -> throw IllegalArgumentException("Invalid value")
}
}

colorValues.forEach { printColor(it) }

Solution

// Define a class for representing colors
sealed class Color
class Red : Color()
class Green : Color()
class Blue : Color()

// Define a function to replace values with Color objects
fun valueToColor(value: String): Color {
return when (value) {
"red" -> Red()
"green" -> Green()
"blue" -> Blue()
else -> throw IllegalArgumentException("Invalid value")
}
}

// Example usage
val colorValues = listOf("red", "green", "blue")
val colors = colorValues.map { valueToColor(it) }

Benefit

  • Improve readability & increase maintainability
  • Reduced code duplication
  • Type safety
  • More clear code and well-organized

Introduce Parameter Object or Preserve Whole Object — If the values of primary data fields are used as inputs to methods, consider using these techniques.

Introduce Parameter Object — refactoring technique involves replacing several individual parameters with a single object. The purpose of this refactoring is to simplify the method signature and make it easier to understand what data is being passed to the method.

Problem

fun calculateCost(quantity: Int, price: Double, taxRate: Double): Double {
return quantity * price * (1 + taxRate)
}

Solution

data class Order(val quantity: Int, val price: Double, val taxRate: Double)

fun calculateCost(order: Order): Double {
return order.quantity * order.price * (1 + order.taxRate)
}

Benefit

  • Improve readability & increase maintainability
  • Improve documentation
  • Reduce complexity
  • Increased Type safety

Preserve Whole Object — involves passing an entire object to a method, instead of just its parts. This can simplify the method signature and make it easier to understand what data is being passed to the method.

Problem

fun chargeCustomer(name: String, street: String, city: String, state: String, zipCode: String, amount: Double) {
if (zipCode.startsWith("9")) {
// add surcharge
amount *= 1.05
}
// charge the customer
println("Charging $amount to $name")
}

Solution

data class Address(val street: String, val city: String, val state: String, val zipCode: String)
data class Customer(val name: String, val address: Address)

fun chargeCustomer(customer: Customer, amount: Double) {
val shippingAddress = customer.address
if (shippingAddress.zipCode.startsWith("9")) {
// add surcharge
amount *= 1.05
}
// charge the customer
println("Charging $amount to ${customer.name}")
}

Benefit

  • the same benefit with Introduce Parameter Object
  • Better encapsulation

Replace Type Code Class/ Subclasses / State or Strategy — When complex data is stored in variables, recommended to use these techniques

Type code is a term used in software development to describe a primitive data type. For example, an integer type code might be used to represent different customer types (e.g., 0 for VIP, 1 for customer, 2 for guest)

Replace Type Code with Class

Problem

class Shape(val type: Int) {
companion object {
const val RECTANGLE = 0
const val CIRCLE = 1
}

fun getType(): String {
return when (type) {
RECTANGLE -> "Rectangle"
CIRCLE -> "Circle"
else -> throw IllegalArgumentException("Incorrect Shape")
}
}
}

Solution

sealed class ShapeType {
object RECTANGLE : ShapeType()
object CIRCLE : ShapeType()
}

class Shape(val type: ShapeType) {
fun getType(): String {
return when (type) {
ShapeType.RECTANGLE -> "Rectangle"
ShapeType.CIRCLE -> "Circle"
}
}
}

Benefit

  • Improved readability and maintainability
  • Type safety
  • Improve extensibility
  • Improve Performance

Replace Type Code with Subclasses

Problem

class Shape(val type: Int, val width: Double, val height: Double) {
companion object {
const val RECTANGLE = 0
const val CIRCLE = 1
}

fun area(): Double {
return when (type) {
RECTANGLE -> width * height
CIRCLE -> Math.PI * Math.pow(width / 2, 2)
else -> throw IllegalArgumentException("Incorrect Shape Type")
}
}
}

Solution

abstract class Shape(val width: Double, val height: Double) {
abstract fun area(): Double
}

class Rectangle(width: Double, height: Double) : Shape(width, height) {
override fun area(): Double {
return width * height
}
}

class Circle(width: Double, height: Double) : Shape(width, height) {
override fun area(): Double {
return Math.PI * Math.pow(width / 2, 2)
}
}

Benefit

  • Better encapsulation
  • More flexibility
  • More testability
  • More Readability

Replace Type Code with State/Strategy

Problem

class Product {
var type: Int = 0
var price: Double = 0.0
fun calculateTotalPrice(): Double {
var totalPrice = 0.0
if (type == 1) {
totalPrice = price * 0.9
} else if (type == 2) {
totalPrice = price * 0.8
} else if (type == 3) {
totalPrice = price * 0.7
}
return totalPrice
}
}

Solution

interface ProductType {
fun calculatePrice(price: Double): Double
}

class Clothing: ProductType {
override fun calculatePrice(price: Double): Double {
return price * 0.9
}
}

class Electronic: ProductType {
override fun calculatePrice(price: Double): Double {
return price * 0.8
}
}

class Food: ProductType {
override fun calculatePrice(price: Double): Double {
return price * 0.7
}
}

class Product {
var type: ProductType
var price: Double = 0.0
constructor(type: ProductType) {
this.type = type
}

fun calculateTotalPrice(): Double {
return type.calculatePrice(price)
}
}

val product1 = Product(Clothing())
val product2 = Product(Electronic())
val product3 = Product(Food())

println(product1.calculateTotalPrice())
println(product2.calculateTotalPrice())
println(product3.calculateTotalPrice())

Benefit

  • can add easily without changing the existing code
  • Improve readability and increase maintainability

Replace Array with Objects — If there are arrays among the variables, use it

Problem

fun processEmployeeData(employeeData: Array<Any>) {
val employeeId = employeeData[0] as Int
val employeeName = employeeData[1] as String
val employeeAge = employeeData[2] as Int
val employeeSalary = employeeData[3] as Double

println("Employee ID: $employeeId")
println("Employee Name: $employeeName")
println("Employee Age: $employeeAge")
println("Employee Salary: $employeeSalary")
}

Solution

data class Employee(val id: Int, val name: String, val age: Int, val salary: Double)

fun processEmployeeData(employee: Employee) {
println("Employee ID: ${employee.id}")
println("Employee Name: ${employee.name}")
println("Employee Age: ${employee.age}")
println("Employee Salary: ${employee.salary}")
}

This is the end of story 5.

See you next time, bye-bye 👋

Story 1 — https://thawzintoe.medium.com/study-with-me-for-code-refactoring-story1-introduction-50d0f9b95cee

Story 2 — https://thawzintoe.medium.com/study-with-me-for-code-refactoring-story2-code-smell-4a5dbac61a0b

Story 3 — https://thawzintoe.medium.com/study-with-me-for-code-refactoring-story3-long-method-of-bloaters-461473504a1b

Story4 — https://medium.com/@thawzintoe/study-with-me-for-code-refactoring-story4-large-class-of-bloaters-24e4aa472020

--

--