Study with me for Code Refactoring ( Story 5— Primitive Obsession of Bloaters)
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