The original article was published on February 27, 2025.
Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.
This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.
Do not "hit" the protruding nail
The following compressFile function, as the name suggests, is a function that compresses the specified file.
fun compressFile(...) {
try {
val workingDirectory = createDirectory(...)
val fileToCompress = file.copyTo(workingDirectory)
CompressionUtil.compress(fileToCompress)
} catch (exception: IOException) {
val logger = LoggerService.get()
val logMessage = ...
val params = ...
logger.log(Log.Error, logMessage, params)
}
}
Currently, it is a function of about 10 lines, but as the code increases, you may want to refactor it by splitting it into private functions. A developer considered splitting this function according to a programming principle called SLAP (Single Level of Abstraction Principle). SLAP is a programming principle that aligns the level of abstraction of code within a scope (such as a function). In the following code, the code within compressFile is divided into "main compression processing" and "error handling" to maintain consistency of abstraction while attempting to simplify compressFile.
fun compressFile(...) {
try {
compressFileInternalOrThrow(...)
} catch (exception: IOException) {
logError(...)
}
}
// May throw IOException
private fun compressFileInternalOrThrow(...) {
val workingDirectory = createDirectory(...)
val fileToCompress = file.copyTo(workingDirectory)
CompressionUtil.compress(fileToCompress)
}
private fun logError(...) {
val logger = LoggerService.get()
val logMessage = ...
val params = ...
logger.log(LogLevel.Error, logMessage, params)
}
Is there room for improvement in this refactoring?
Do not over-slap the main logic
This refactoring has deteriorated in terms of readability of the main logic. In the pre-refactoring compressFile, the flow of actions such as "prepare a working directory", "copy the file", and "compress" was understandable. However, as a result of the refactoring, reading the contents of compressFile only reveals that "compression may fail". If you want to read the contents of the function, you probably want to know the behavior that cannot be read from the signature or documentation, but for that, you have to read compressFileInternalOrThrow. (Additionally, the caller now needs to be careful about what exceptions compressFileInternalOrThrow might throw.)
In some cases, it may be better to maintain important code as concrete without being overly conscious of SLAP. In other words, it may be better to lower the abstraction level of the main logic compared to the sub-logic. In the following example, the main logic of compression is not extracted into a function, and only the abstraction level of error handling is raised.
fun compressFile(...) {
try {
val workingDirectory = createDirectory(...)
val fileToCompress = file.copyTo(workingDirectory)
CompressionUtil.compress(fileToCompress)
} catch (exception: IOException) {
logError(...)
}
}
private fun logError(...) {
val logger = LoggerService.get()
val logMessage = ...
val params = ...
logger.log(LogLevel.Error, logMessage, params)
}
Examples of logic that correspond to "sub" include the following:
- Handling edge cases
- Error handling
- Setup and teardown
- Resource management
- Type conversion for return values or other function calls
When such sub-logic is written at length, it can hinder the understanding of the main logic. To understand edge cases and error cases, you need to know what the function is doing in the first place, so it is reasonable to structure it in a way that allows you to understand the main logic first.
SLAP can be rephrased as the principle of "maintaining consistency in abstraction level". The assertion that code consistency should be maintained is made in many ways other than SLAP. However, it is important to note that maintaining code consistency is a means to improve code quality, such as readability and ease of change. Be careful not to make the structure more difficult to understand or increase technical debt by trying to maintain consistency unnecessarily.
In a nutshell
Sometimes it is better to lower the abstraction level of the main logic compared to the sub-logic.
Keywords: abstraction level, function, extraction