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
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
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
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
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
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
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
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
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
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
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.