Different Levels of Abstraction in Programming

Different Levels of Abstraction in Programming

Introduction

Abstraction is a process to see a thing as a one rather than as the sum of its parts. It can be rules or equations that are applicable to most situations. In programming, abstractions are used to simplify the process which are common to other tasks. In other words, they are reusable. They take in different levels which this page is all about.

There are 6 levels of abstraction listed below. They are ordered from easiest to hardest in implementation and flexibility.

  1. Package-level Abstraction
  2. Configuration-level Abstraction
  3. Variable-level Abstraction
  4. Function-level Abstraction
  5. Object-level Abstraction
  6. Interface-level Abstraction

Package-level Abstraction

This level of abstraction is the easiest to work on among other levels. However, a code at this level may have repetitive statements. Therefore, the program will be longer and will take time to read.

In addition, most software are only usable in this level. They are relatively easy to install as they can be standalone, in compressed portable format, or included with the help of package manager. The downside is that there is a limited flexibility which are usually offered through settings or preferences option. It is expected that the users of these applications are anyone who have basic computer literacy.

For example, a teacher use a spreadsheet software to calculate the grades of students. The teacher will just input the raw grade and the formula. After that, the final grade will be automatically calculated by the software as programmed.

Below is another example of a program which asks for user's first name, last name, age, and distance walked. Notice that there are repetitive statements and may take time to read.

import { stdin, stdout } from "process"
async function main() {
let firstName = ""
await new Promise<void>(resolve => {
stdout.write("What is your first name? ")
stdin.on("readable", function readRawInput() {
firstName = stdin.read().toString("utf-8").trim()
stdin.off("readable", readRawInput)
resolve()
})
})
let lastName = ""
await new Promise<void>(resolve => {
stdout.write("What is your last name? ")
stdin.on("readable", function readRawInput() {
lastName = stdin.read().toString("utf-8").trim()
stdin.off("readable", readRawInput)
resolve()
})
})
let age = 0
await new Promise<void>(resolve => {
stdout.write("What is your age? ")
stdin.on("readable", function readRawInput() {
age = Number(stdin.read().toString("utf-8"))
stdin.off("readable", readRawInput)
resolve()
})
})
let distance = 0
await new Promise<void>(resolve => {
stdout.write("How many meters did you walk? ")
stdin.on("readable", function readRawInput() {
distance = Number(stdin.read().toString("utf-8"))
stdin.off("readable", readRawInput)
resolve()
})
})
stdout.write(`You are ${firstName} ${lastName}.\n`)
stdout.write(`Your age is ${age} and you have walked for ${distance} meters.\n`)
}
main()

Content of src/package-level_abstraction.ts inmasterat KennethTrecy /demo_of_different_levels_of_abstraction

This is the output after running the package-level code.

npx ts-node src/package-level_abstraction.ts
What is your first name? Kenneth
What is your last name? Tobias
What is your age? 10
How many meters did you walk? 100
You are Kenneth Tobias.
Your age is 10 and you have walked for 100 meters.

Output of src/package-level_abstraction.ts

Configuration-level Abstraction

This level of abstraction allows the creation of different package-level software programs. Software programs at this level can be managed using environment variables (which are declared on the system, . env files, or other contexts) or command line arguments. Users who want to customize their software should know how to run or compile software.

For instance, some programming languages have a compiler. Those compilers allow developer(s) to create programs that run on different operating systems or environments. For every operating system that a developer want to support, the compiler would output a specific executable or artifact specialized and optimized for the targeted system.

Environment variables are usually used in web development too. Some variables are named like DOMAIN_NAME, SERVER_PORT, or BRAND_NAME. These variables are used different parts of the software and customized per machine.

Using the package-level abstraction's example, the program can be modified to allow configuration-level customization. It uses an external package named dotenv package to use the environment variables by using process .env.<variable name>. Note that the program uses logical OR operator (||) in order to use default messages.

import { stdin, stdout } from "process"
import { config } from "dotenv"
config()
async function main() {
let firstName = ""
await new Promise<void>(resolve => {
stdout.write(process.env.FIRST_NAME_QUESTION || "What is your first name? ")
stdin.on("readable", function readRawInput() {
firstName = stdin.read().toString("utf-8").trim()
stdin.off("readable", readRawInput)
resolve()
})
})
let lastName = ""
await new Promise<void>(resolve => {
stdout.write(process.env.LAST_NAME_QUESTION || "What is your last name? ")
stdin.on("readable", function readRawInput() {
lastName = stdin.read().toString("utf-8").trim()
stdin.off("readable", readRawInput)
resolve()
})
})
let age = 0
await new Promise<void>(resolve => {
stdout.write(process.env.AGE_QUESTION || "What is your age? ")
stdin.on("readable", function readRawInput() {
age = Number(stdin.read().toString("utf-8"))
stdin.off("readable", readRawInput)
resolve()
})
})
let distance = 0
await new Promise<void>(resolve => {
stdout.write(process.env.DISTANCE_QUESTION || "How many meters did you walk? ")
stdin.on("readable", function readRawInput() {
distance = Number(stdin.read().toString("utf-8"))
stdin.off("readable", readRawInput)
resolve()
})
})
stdout.write(`You are ${firstName} ${lastName}.\n`)
stdout.write(`Your age is ${age} and you have walked for ${distance} meters.\n`)
}
main()

Content of src/configuration-level_abstraction.ts inmasterat KennethTrecy /demo_of_different_levels_of_abstraction

There are different methods to declare environment variables. Below, it is an example of . env file to declare them. Note that some variables in the environment do not have a declared value. Therefore, the program will use default values for those empty variables.

FIRST_NAME_QUESTION=Enter your first name:
LAST_NAME_QUESTION=Enter your last name:
AGE_QUESTION=
DISTANCE_QUESTION=

Content of . env inmasterat KennethTrecy /demo_of_different_levels_of_abstraction

This is the output after running the configuration-level code. Notice that the questions for first name and last name have changed according to the questions in environment file.

npx ts-node src/configuration-level_abstraction.ts
Enter your first name:Kenneth
Enter your last name:Tobias
What is your age? 10
How many meters did you walk? 100
You are Kenneth Tobias.
Your age is 10 and you have walked for 100 meters.

Output of src/configuration-level_abstraction.ts

Variable-level Abstraction

This is almost similar to configuration-level abstraction. However, the environment variables are not included in this level. This level refers to the programs/codes customizable by globally-scoped variables and locally-scoped variables.

Should the user want to customize the program, knowledge in variable declaration (depending on the programming language used) is a must. They may also need to learn about enumerations or any different data types like boolean and integer.

This level can be seen when making embedded programs for microcontrollers. It is helpful to declare multiple constants for values that are repeatedly used like indicating on or off, pin to the LED, or length of intervals. In addition, it can be seen in other fields of Information Technology (I.T.) such as web development and game development.

Building from the example in configuration-level abstraction, default messages can be put in global scope allowing other developers find the default messages quickly.

import { stdin, stdout } from "process"
import { config } from "dotenv"
config()
const DEFAULT_FIRST_NAME_QUESTION = "What is your first name? "
const DEFAULT_LAST_NAME_QUESTION = "What is your last name? "
const DEFAULT_AGE_QUESTION = "What is your age? "
const DEFAULT_DISTANCE_QUESTION = "How many meters did you walk? "
const ENCODING = "utf-8"
async function main() {
let firstName = ""
await new Promise<void>(resolve => {
stdout.write(process.env.FIRST_NAME_QUESTION || DEFAULT_FIRST_NAME_QUESTION)
stdin.on("readable", function readRawInput() {
firstName = stdin.read().toString(ENCODING).trim()
stdin.off("readable", readRawInput)
resolve()
})
})
let lastName = ""
await new Promise<void>(resolve => {
stdout.write(process.env.LAST_NAME_QUESTION || DEFAULT_LAST_NAME_QUESTION)
stdin.on("readable", function readRawInput() {
lastName = stdin.read().toString(ENCODING).trim()
stdin.off("readable", readRawInput)
resolve()
})
})
let age = 0
await new Promise<void>(resolve => {
stdout.write(process.env.AGE_QUESTION || DEFAULT_AGE_QUESTION)
stdin.on("readable", function readRawInput() {
age = Number(stdin.read().toString(ENCODING))
stdin.off("readable", readRawInput)
resolve()
})
})
let distance = 0
await new Promise<void>(resolve => {
stdout.write(process.env.DISTANCE_QUESTION || DEFAULT_DISTANCE_QUESTION)
stdin.on("readable", function readRawInput() {
distance = Number(stdin.read().toString(ENCODING))
stdin.off("readable", readRawInput)
resolve()
})
})
stdout.write(`You are ${firstName} ${lastName}.\n`)
stdout.write(`Your age is ${age} and you have walked for ${distance} meters.\n`)
}
main()

Content of src/variable-level_abstraction.ts inmasterat KennethTrecy /demo_of_different_levels_of_abstraction

Function-level Abstraction

At this level of abstraction, a software/code file bundles different procedures in one or more functions. Example of this are libraries which may contain different functions to change the casing of characters in a string.

Generalizing multiple, yet similar procedures into one function is helpful during development. It saves lines of code and effort to scroll on a file to understand the operations. Other developers do not need to reimplement a function as they only need to install a dependency containing the process they need, therefore it saves time.

Reusing the variable-level abstraction's example, repeated statements on reading a string can be summarized into one function. Therefore, it is quicker to read.

import { stdin, stdout } from "process"
import { config } from "dotenv"
config()
const DEFAULT_FIRST_NAME_QUESTION = "What is your first name? "
const DEFAULT_LAST_NAME_QUESTION = "What is your last name? "
const DEFAULT_AGE_QUESTION = "What is your age? "
const DEFAULT_DISTANCE_QUESTION = "How many meters did you walk? "
const ENCODING = "utf-8"
async function readStringInput(question: string): Promise<string> {
let input = ""
await new Promise<void>(resolve => {
stdout.write(question)
stdin.on("readable", function readRawInput() {
input = stdin.read().toString(ENCODING).trim()
stdin.off("readable", readRawInput)
resolve()
})
})
return input
}
async function main() {
const firstNameQuestion = process.env.FIRST_NAME_QUESTION || DEFAULT_FIRST_NAME_QUESTION
const firstName = await readStringInput(firstNameQuestion)
const lastNameQuestion = process.env.LAST_NAME_QUESTION || DEFAULT_LAST_NAME_QUESTION
const lastName = await readStringInput(lastNameQuestion)
let age = 0
await new Promise<void>(resolve => {
stdout.write(process.env.AGE_QUESTION || DEFAULT_AGE_QUESTION)
stdin.on("readable", function readRawInput() {
age = Number(stdin.read().toString(ENCODING))
stdin.off("readable", readRawInput)
resolve()
})
})
let distance = 0
await new Promise<void>(resolve => {
stdout.write(process.env.DISTANCE_QUESTION || DEFAULT_DISTANCE_QUESTION)
stdin.on("readable", function readRawInput() {
distance = Number(stdin.read().toString(ENCODING))
stdin.off("readable", readRawInput)
resolve()
})
})
stdout.write(`You are ${firstName} ${lastName}.\n`)
stdout.write(`Your age is ${age} and you have walked for ${distance} meters.\n`)
}
main()

Content of src/function-level_abstraction_variant_1.ts inmasterat KennethTrecy /demo_of_different_levels_of_abstraction

Repeated statements on reading an integer can also be summarized.

import { stdin, stdout } from "process"
import { config } from "dotenv"
config()
const DEFAULT_FIRST_NAME_QUESTION = "What is your first name? "
const DEFAULT_LAST_NAME_QUESTION = "What is your last name? "
const DEFAULT_AGE_QUESTION = "What is your age? "
const DEFAULT_DISTANCE_QUESTION = "How many meters did you walk? "
const ENCODING = "utf-8"
async function readStringInput(question: string): Promise<string> {
let input = ""
await new Promise<void>(resolve => {
stdout.write(question)
stdin.on("readable", function readRawInput() {
input = stdin.read().toString(ENCODING).trim()
stdin.off("readable", readRawInput)
resolve()
})
})
return input
}
async function readNumericInput(question: string): Promise<number> {
let input = 0
await new Promise<void>(resolve => {
stdout.write(question)
stdin.on("readable", function readRawInput() {
input = Number(stdin.read().toString(ENCODING))
stdin.off("readable", readRawInput)
resolve()
})
})
return input
}
async function main() {
const firstNameQuestion = process.env.FIRST_NAME_QUESTION || DEFAULT_FIRST_NAME_QUESTION
const firstName = await readStringInput(firstNameQuestion)
const lastNameQuestion = process.env.LAST_NAME_QUESTION || DEFAULT_LAST_NAME_QUESTION
const lastName = await readStringInput(lastNameQuestion)
const ageQuestion = process.env.AGE_QUESTION || DEFAULT_AGE_QUESTION
const age = await readNumericInput(ageQuestion)
const distanceQuestion = process.env.DISTANCE_QUESTION || DEFAULT_DISTANCE_QUESTION
const distance = await readNumericInput(distanceQuestion)
stdout.write(`You are ${firstName} ${lastName}.\n`)
stdout.write(`Your age is ${age} and you have walked for ${distance} meters.\n`)
}
main()

Content of src/function-level_abstraction_variant_2.ts inmasterat KennethTrecy /demo_of_different_levels_of_abstraction

There are similarities between the two new functions. Generalizing it further, it becomes something like below.

import { stdin, stdout } from "process"
import { config } from "dotenv"
config()
const DEFAULT_FIRST_NAME_QUESTION = "What is your first name? "
const DEFAULT_LAST_NAME_QUESTION = "What is your last name? "
const DEFAULT_AGE_QUESTION = "What is your age? "
const DEFAULT_DISTANCE_QUESTION = "How many meters did you walk? "
const ENCODING = "utf-8"
async function readInput(question: string): Promise<string> {
let input = ""
await new Promise<void>(resolve => {
stdout.write(question)
stdin.on("readable", function readRawInput() {
input = stdin.read().toString(ENCODING)
stdin.off("readable", readRawInput)
resolve()
})
})
return input
}
async function readStringInput(question: string): Promise<string> {
const input = await readInput(question)
return input.trim()
}
async function readNumericInput(question: string): Promise<number> {
const input = await readInput(question)
return Number(input)
}
async function main() {
const firstNameQuestion = process.env.FIRST_NAME_QUESTION || DEFAULT_FIRST_NAME_QUESTION
const firstName = await readStringInput(firstNameQuestion)
const lastNameQuestion = process.env.LAST_NAME_QUESTION || DEFAULT_LAST_NAME_QUESTION
const lastName = await readStringInput(lastNameQuestion)
const ageQuestion = process.env.AGE_QUESTION || DEFAULT_AGE_QUESTION
const age = await readNumericInput(ageQuestion)
const distanceQuestion = process.env.DISTANCE_QUESTION || DEFAULT_DISTANCE_QUESTION
const distance = await readNumericInput(distanceQuestion)
stdout.write(`You are ${firstName} ${lastName}.\n`)
stdout.write(`Your age is ${age} and you have walked for ${distance} meters.\n`)
}
main()

Content of src/function-level_abstraction_variant_3.ts inmasterat KennethTrecy /demo_of_different_levels_of_abstraction

Object-level Abstraction

These are abstractions that may represent real-world things which may be composed of multiple functionalities. To have this level of abstraction, a developer needs to know about the object-oriented programming (OOP) concepts like polymorphism or composition.

When it becomes specialized or concrete, it is known as instance. Instances are useful in game development. A developer can use an instance represent the game's item, obstacles, or any other object to interact with other instances.

In web development, instances are also used like servers too. Indeed, they have multiple functionalities but uses different set of values inside. They may differ in software version, domain name, or files to name a few.

Below is an improved version of the function-level abstraction's last example. It uses classes and the concept of inheritance to reuse the code. There are four instances in this code which are contained in variables declared at line 47, 50, 53, and 56.

import { stdin, stdout } from "process"
import { config } from "dotenv"
config()
const DEFAULT_FIRST_NAME_QUESTION = "What is your first name? "
const DEFAULT_LAST_NAME_QUESTION = "What is your last name? "
const DEFAULT_AGE_QUESTION = "What is your age? "
const DEFAULT_DISTANCE_QUESTION = "How many meters did you walk? "
const ENCODING = "utf-8"
class InputReader {
constructor(protected question: string) {}
async read(): Promise<any> {
let input = ""
await new Promise<void>(resolve => {
stdout.write(this.question)
stdin.on("readable", function readRawInput() {
input = stdin.read().toString(ENCODING)
stdin.off("readable", readRawInput)
resolve()
})
})
return input
}
}
class InputStringReader extends InputReader {
async read(): Promise<string> {
const input = await super.read()
return input.trim()
}
}
class InputNumericReader extends InputReader {
async read(): Promise<number> {
const input = await super.read()
return Number(input)
}
}
async function main() {
const firstNameQuestion = process.env.FIRST_NAME_QUESTION || DEFAULT_FIRST_NAME_QUESTION
const firstName = await new InputStringReader(firstNameQuestion).read()
const lastNameQuestion = process.env.LAST_NAME_QUESTION || DEFAULT_LAST_NAME_QUESTION
const lastName = await new InputStringReader(lastNameQuestion).read()
const ageQuestion = process.env.AGE_QUESTION || DEFAULT_AGE_QUESTION
const age = await new InputNumericReader(ageQuestion).read()
const distanceQuestion = process.env.DISTANCE_QUESTION || DEFAULT_DISTANCE_QUESTION
const distance = await new InputNumericReader(distanceQuestion).read()
stdout.write(`You are ${firstName} ${lastName}.\n`)
stdout.write(`Your age is ${age} and you have walked for ${distance} meters.\n`)
}
main()

Content of src/object-level_abstraction.ts inmasterat KennethTrecy /demo_of_different_levels_of_abstraction

Interface-level Abstraction

Hardest level of abstraction that someone could work on. It can be in a form of abstract classes, templates, traits, generics, or macros.

If a generalized class or function has changed in identifiers, mechanism, or number of parameters, the developer has to change all codes that depend on the generalized class or function. It can be tedious process as the difficulty is relative to the number of changes applied on an interface-level code.

Codes at this level may consider multiple use cases. Modifying them should be careful to make sure the dependent systems do not break, or at least breaks slightly.

Below, is an example based from object-level abstraction's example but with the application of Typescript's generics. The code may appear longer than the example in package-level abstraction. However, making an interface-level abstraction has greater benefits on large projects than this example.

import { stdin, stdout } from "process"
import { config } from "dotenv"
config()
const DEFAULT_FIRST_NAME_QUESTION = "What is your first name? "
const DEFAULT_LAST_NAME_QUESTION = "What is your last name? "
const DEFAULT_AGE_QUESTION = "What is your age? "
const DEFAULT_DISTANCE_QUESTION = "How many meters did you walk? "
const ENCODING = "utf-8"
class InputReader<T extends string|number> {
constructor(protected question: string) {}
async read(): Promise<T> {
let input = ""
await new Promise<void>(resolve => {
stdout.write(this.question)
stdin.on("readable", function readRawInput() {
input = stdin.read().toString(ENCODING)
stdin.off("readable", readRawInput)
resolve()
})
})
return input as T
}
}
class InputStringReader extends InputReader<string> {
async read(): Promise<string> {
return (await super.read()).trim()
}
}
class InputNumericReader extends InputReader<number> {
async read(): Promise<number> {
return Number(await super.read())
}
}
async function main() {
const firstNameQuestion = process.env.FIRST_NAME_QUESTION || DEFAULT_FIRST_NAME_QUESTION
const firstName = await new InputStringReader(firstNameQuestion).read()
const lastNameQuestion = process.env.LAST_NAME_QUESTION || DEFAULT_LAST_NAME_QUESTION
const lastName = await new InputStringReader(lastNameQuestion).read()
const ageQuestion = process.env.AGE_QUESTION || DEFAULT_AGE_QUESTION
const age = await new InputNumericReader(ageQuestion).read()
const distanceQuestion = process.env.DISTANCE_QUESTION || DEFAULT_DISTANCE_QUESTION
const distance = await new InputNumericReader(distanceQuestion).read()
stdout.write(`You are ${firstName} ${lastName}.\n`)
stdout.write(`Your age is ${age} and you have walked for ${distance} meters.\n`)
}
main()

Content of src/interface-level_abstraction_variant_1.ts inmasterat KennethTrecy /demo_of_different_levels_of_abstraction

In addition, an example based from function-level abstraction's example but with the application of Typescript's generics too. It appears to be shorter than the code above.

import { stdin, stdout } from "process"
import { config } from "dotenv"
config()
const DEFAULT_FIRST_NAME_QUESTION = "What is your first name? "
const DEFAULT_LAST_NAME_QUESTION = "What is your last name? "
const DEFAULT_AGE_QUESTION = "What is your age? "
const DEFAULT_DISTANCE_QUESTION = "How many meters did you walk? "
const ENCODING = "utf-8"
async function readInput<T>(question: string, cast: (rawInput: string) => T): Promise<T> {
return await new Promise<T>(resolve => {
stdout.write(question)
stdin.on("readable", function readRawInput() {
const input = cast(stdin.read().toString(ENCODING))
stdin.off("readable", readRawInput)
resolve(input)
})
})
}
async function readStringInput(question: string): Promise<string> {
return await readInput<string>(question, rawInput => rawInput.trim())
}
async function readNumericInput(question: string): Promise<number> {
return await readInput(question, rawInput => Number(rawInput))
}
async function main() {
const firstNameQuestion = process.env.FIRST_NAME_QUESTION || DEFAULT_FIRST_NAME_QUESTION
const firstName = await readStringInput(firstNameQuestion)
const lastNameQuestion = process.env.LAST_NAME_QUESTION || DEFAULT_LAST_NAME_QUESTION
const lastName = await readStringInput(lastNameQuestion)
const ageQuestion = process.env.AGE_QUESTION || DEFAULT_AGE_QUESTION
const age = await readNumericInput(ageQuestion)
const distanceQuestion = process.env.DISTANCE_QUESTION || DEFAULT_DISTANCE_QUESTION
const distance = await readNumericInput(distanceQuestion)
stdout.write(`You are ${firstName} ${lastName}.\n`)
stdout.write(`Your age is ${age} and you have walked for ${distance} meters.\n`)
}
main()

Content of src/interface-level_abstraction_variant_2.ts inmasterat KennethTrecy /demo_of_different_levels_of_abstraction

Conclusion

Those are the various levels of abstraction that programmers may find on every software they build. Beginners may want to make their programs customizable by aiming for a package-level to variable-level abstraction. Thus, being overwhelmed can be prevented and focus at the current task.

Meanwhile, developers of a large project or proficient programmers may want to transform their code aiming for afunction-levelto interface-level abstraction to save time configuring the similar behaviors. Another benefit is that there is a consistency between the different components, mechanisms, or interfaces of the application. Yet, the price is that the other contributors need to know advance concepts to understand the advanced abstractions.

That said, the levels in this article allows programmers determine the complexity of a code. They are just one of the tools in order to communicate with other developers efficiently.

References

Some portions in this article were based on third-party work(s) for educational purposes. They are copyright of the respective groups, organizations, companies, or persons that have been attributed below. Note that these works may have been shared under different licenses and may have notices (e.g. disclaimer of warranties) in the linked licenses. Should a work has no existing license(s), a link to the work have been still provided.

Disclaimer: Otherwise noted, the views or interests expressed in this site are my views, and does not necessarily reflect the view or interest of any entity I have a connection to; whether it is an organization, or someone I have worked with. In addition, trademarks (that may be mentioned in different pages) are the property of their respective owners and should not be interpreted as indicating endorsement, affiliation, or sponsorship, unless stated otherwise.

logo

Copyright © 2024 Kenneth Trecy Tobias.

Website's code(not texts such as containing my personal information) are under MIT license.

Socials

LinkedIn GitHub