[Reading memo] System design principles that are useful in the field- "Small and easy to understand"

(Why) Why do you need to make it easier to understand?

The cost of delivering a product (*) depends on the comprehensibility of the program, which results in design.

(How) How do you do it?

The points are the following five.

  1. The basics of code organization are names and paragraphs
  2. Organize your code with short methods and small classes
  3. Make it easy to understand and secure with value objects
  4. Collect and organize complex logic with collection objects
  5. The more the class name or method name matches the business term, the easier it is to understand the intent of the program, and the easier and safer it is to change.

(What) Specifically?

1. The basics of code organization are names and paragraphs

A confusing example.kt


var a = 2000
val b = if (c < 2014) 1.05 else 1.08
a = a * b
a = if (a < 2500) a * 0.8 else a
println("total${a}It is a circle.");

Easy-to-understand example.kt


val basePrice = 2000

val taxRate = if (year < 2014) 1.05 else 1.08
val afterTax = basePrice * taxRate

val result = if (afterTax < 2500) afterTax * 0.8 else afterTax

println("total${result}It is a circle.");

point --Give a variable name so that "name represents the body" --Variables have only one role --If you repeat the assignment to the variable, the range of influence of the correction will be widened. It may cause side effects. --If you repeat the assignment to the same variable even after dividing the program for each paragraph, the degree of coupling for each paragraph becomes stronger. --Separate paragraphs for each group of meanings (do not write loosely)

2. Organize your code with short methods and small classes

Thanks to the above changes, it's easier to cut out the method. First, in the same class, it is an example of extracting the process to the method.

Easy-to-understand example (repost).kt


val basePrice = 2000

val taxRate = if (year < 2014) 1.05 else 1.08
val afterTax = basePrice * taxRate

val result = if (afterTax < 2500) afterTax * 0.8 else afterTax

println("total${result}It is a circle.");

Example of extracting a method.kt


fun main(args: Array<String>) {
    val basePrice = 2000

    val afterTax = getAfterTaxAmount(year, basePrice)

    val result = getDiscountedPrice(afterTax)

    println("total${result}It is a circle.");
}

fun getAfterTaxAmount(year: Int, basePrice: Int): Double {
    return basePrice * if (year < 2014) 1.05 else 1.08
}

fun getDiscountedPrice(amount: Int): Int {
    return if (amount < 2500) amount * 0.8 else amount
}

The advantages of extracting methods are as follows.

  1. You will be able to confidently make corrections because future changes will be trapped within the method.
  2. Being able to convey intent to code in the form of method names.
  3. It should be possible to reuse the process.

The following is an example of eliminating duplicate code in different classes.

User.kt


class User(name: String, prefecture: String) {
    fun isInTokyo(): Boolean {
        return prefecture == "Tokyo"
    }
}

Article.kt


class User(title: String, prefecture: String) {
    fun isInTokyo(): Boolean {
        return prefecture == "Tokyo"
    }
}

The above two objects have the same value of prefecture, and also have a method to process based on that value. This is a duplicate code.

In this case, the refactoring policy differs between the following two methods. i. There is no reference relationship between two objects ii. There is a reference relationship between two objects

i. There is no reference relationship between two objects

It's a good idea to create a class called Prefecture and bring duplicate logic to it. That way, if you add, modify, or delete decision logic based on prefecture, only the Prefecture class will be modified. At the same time that it is easier to fix, it is less likely that unexpected bugs will occur due to omission of correction.

Prefecture.kt


class Prefecture(name: String) {
    fun isInTokyo(): Boolean {
        return name == "Tokyo"
    }
}

ʻUser and ʻArticle are as follows. The ʻisInTokyo` method is realized by delegating each method. Therefore, there is no change in the client after the modification.

User.kt


class User(name: String, prefecture: Prefecture) {
    fun isInTokyo(): Boolean {
        return prefecture.isInTokyo()
    }
}

Article.kt


class User(title: String, prefecture: Prefecture) {
    fun isInTokyo(): Boolean {
        return prefecture.isInTokyo()
    }
}

ii. There is a reference relationship between two objects

Assuming the User has an Article, the implementation code looks like this: Make sure to call the method of the object you own. (Delegate.) (Article implementation remains the same)

User.kt


User(name: String, prefecture: String) {
    private val article: Article
    fun isInTokyo(): Boolean {
        return article.isInTokyo()
    }
}

3. Make it easy to understand and secure with value objects

As an extreme example, you may have seen the following code.

Basic data type.kt


class Item {
    val price: Int
    val quantity: Int
}

What's wrong with this is that in the above case, we declare that the allowable values for both price and quantity are -2.1 billion to +2.1 billion. Generally speaking, prices should have a reasonable upper limit. So you should define a Money class to represent it and have it in the Item class. By doing so, you can delegate the check for abnormal values that is required when processing price to the Money class, and the outlook for the code in the Item will be clearer.

For example, suppose you have a method for finding the total purchase price. In addition, it has the following restrictions:

  1. If the unit price is-, throw an error.
  2. If the unit price exceeds 10,000, an error will be issued.

Basic data type.kt


class Item(price: Int, quantity: Int) {
    private val price: Int
    private val quantity: Int

    init {
        this.price = price
        this.quantity = quantity
    }

    fun calculateTotalAmount(): Int {
        if (price < 0 || price > 100000) throw IllegalArgumentException()

        return price * quantity
    }
}

fun main(args: Array<String>) {
        val price = 10000000
        val item: Item = Item(price, 1)
        item.calculateTotalAmount()    
}

If true, all you want to do with calculateTotalAmount () should be one line, but it takes three times as much. This is because I had to check the value. It delegates this value check to the Money class.

ValueObject.kt


class Item(price: Money, quantity: Int) {
    private val price: Money
    private val quantity: Int

    init {
        this.price = price
        this.quantity = quantity
    }

    fun calculateTotalAmount(): Int {
        return price.getPrice() * quantity
    }
}

class Money(price: Int) {
    private val price: Int

    init {
        if (price < 0 || price > 100000) throw IllegalArgumentException()

        this.price = price
    }

    fun getPrice(): Int {
        return price
    }
}

Now the ʻItem # calculateTotalAmount` fits in one line and there is no extra processing, so the outlook is better. Even if you are responsible for processing the object, it is logically easier for the Money class to check the amount.

4. Collect and organize complex logic with collection objects

_ Write tomorrow! _

Impressions

Writing in Kotlin took a lot of time. .. .. I wonder if it can't be helped at first. When it comes to new languages and new information, it can be quite burdensome, so I'll keep a proper balance.

Recommended Posts

[Reading memo] System design principles that are useful in the field- "Small and easy to understand"
I learned a lot about "principles of system design that are useful in the field", so I summarized them ~ Chapter 1 ~
About "Dependency Injection" and "Inheritance" that are easy to understand when remembered together
Java classes and instances to understand in the figure
Memo that transitions to the login screen if you are not logged in with devise
Easy to understand the difference between Ruby instance method and class method.
[Bootstrap] How to use the "grid system" that supports responsive web design
Java reference to understand in the figure
[Java] What to do if the contents saved in the DB and the name of the enum are different in the enum that reflects the DB definition