Abstraction between High-level and Low-level classes

Rahul Rastogi
EXSQ Engineering Hub
4 min readMar 3, 2019
Photo by Nathan Anderson on Unsplash

A class may be dependent on other classes to perform some task. For instance, a Client class may be dependent on FileLogger class to log some details on the file system.

The class which is performing a task with the help of other class is a High-level class and the class which receives command or helps high-level class to perform a task is often regarded as a Low-level class.

A high-level class of a particular layer may be a treated as a low-level class in upper layer classes. For e.g.: A data-layer class is a low-level class for domain-layer class. Similarly, a domain-layer class is a low-level class for the UI-layer class.

It’s obvious that high-level class needs low-level classes to perform tasks. So, how should we model the relation between high-level and low-level classes?

A very simple dependency between high-level and low-level classes (Client and FileLogger class) may look like:

/**
* A high-level class which needs FileLogger to log details on the file
* system.
*/
public class Client {
private String data;
private FileLogger fileLogger;

public Client(String data, FileLogger fileLogger) {
this.data = data;
this.fileLogger = fileLogger;
}

public void logRecords(){
//Logging data on file system
fileLogger.log(data);
}
}

/**
* A low-level class which facilitates writing logs on the file system.
*/
public class FileLogger {
private FileWriter fileWriter;

public void log(String data){
fileWriter.write(data);
}
}

/**
* A sample class demonstrating use of above high level and low level
* classes.
*/
public class Sample {

public static void main(String... args) {
FileLogger fileLogger = new FileLogger();

Client client = new Client("I faced an issue", fileLogger);
client.logRecords();
}
}

Do you notice some problem in code above?

What if we want the Client class to start using DisplayLogger class to show logs on screen?

For doing this in above code, we’ll have to make changes in Client class also. Here, Client and FileLogger classes are strongly coupled with each other. This is making Client class not open for accepting other types of loggers.

The other problem here is that:

In above design, Client class can’t be written until the the FileLogger class has been written.

Since, the Client class directly uses FileLogger class so, we’ll have to write FileLogger class first before we start writing Client class. But if we’ve two developers who are working simultaneously on same module then one developer will get stuck if it takes significant time to other developer to write FileLogger class. This hampers the productivity.

For solving these issues:

We’ll first make FileLogger class related changes to make sure it is open for extension.

At first, we’ll create a contract for all the logger classes:

/**
* Contract for all the logger classes.
*/
interface Logger {
void log(String data);
}

Now, we want all logger classes to follow the above contract to make logger classes open for extension at any given point of time.

/**
* This class logs the data on file system.
*/
public class FileLogger implements Logger {
private FileWriter fileWriter = new FileWriter();

@Override
public void log(String data) {
fileWriter.write(data);
}
}
/**
* This class logs the data on display screen.
*/
public class DisplayLogger implements Logger {

private DisplayIO displayIO = new DisplayIO();

@Override
public void log(String data) {
displayIO.write(data);
}
}

As a second step,

We’ll make our high-level class to be dependent on abstraction rather than concretion (concrete implementation) for promoting loose-coupling.

Now, our high-level class Client will be dependent on abstract layer rather than concrete implementation of low-level (e.g.: FileLogger) class.

public class Client {
private String data;
private Logger logger;

public Client(String data, Logger logger) {
this.data = data;
this.logger = logger;
}

public void logRecords() {
//Logging data on file system
logger.log(data);
}
}

As per sub-type polymorphism, the base-type variable can hold reference to child class object. So, in Client class constructor, we can pass any concrete implementation of Logger class like: FileLogger, DisplayLogger etc.

This is illustrated in following code:

public class Sample {

public static void main(String... args) {
String data = "I like it!";

//Logging on file-system.
Logger fileLogger = new FileLogger();
Client firstClient = new Client(data, fileLogger);
firstClient.logRecords();

//Logging on display screen.
Logger displayLogger = new DisplayLogger();
Client secondClient = new Client(data, fileLogger);
secondClient.logRecords();
}
}

After modifying our classes. Now:

The logger class is made open for extension by implementing an interface (abstract class could also be used to replace interface). Hence, various type of loggers can be facilitated in system.

The Client class isn’t dependent on any particular type of logger. It’s now made capable to work with any implementation of Logger.

Client class and logger classes made loosely-coupled.

The Client class is now unaware of the implementation details of used Logger. Because, client class knows only the Logger, not its concrete class.

Two developers can simultaneously write Client and Logger’s concrete implementation classes.

Unit testing made easy, as we can now inject mock object of Logger in Client class.

These are some ways to keep in mind while working with high-level and low-level classes. As source-code grows by time, these ways will help us to keep our code clean.

Thanks! Don’t forget to clap if you liked this post.

--

--

Rahul Rastogi
EXSQ Engineering Hub

Mobile Apps @EX2 solutions, India. Technology enthusiast, Flutter, Android and more.