Abstraction, Encapsulation and Modularity in Programming Languages Explained

Programmer working on software architecture with abstraction and modular code

Modern software as it grows in size, feature set, user base, and technical requirements can become very complex. A small program may start out with just a few functions and variables but a large scale application may consist of thousands of files, many databases, external services, user interfaces, business rules, security elements and may have many developers working in the same code base at the same time. Without which to handle this growth in complexity very simple changes may cause unforeseen issues in other parts of the application. 

This is the reason that programmers turn to principles like abstraction, encapsulation, modularity, information hiding, and organized code structure. These concepts present ways to separate what each part of the system is responsible for, to control how different parts of the system depend on each other, to keep out details that are not relevant to a given issue and in general make the software easier to understand. Also they are the fundamental principles of software development which in turn enable developers to build out systems that may grow in scale but do not fall into an unmanageable maintenance black hole.

Understanding Abstraction in Programming

Abstraction is the process which consists in looking at the main features of a thing, and overlooking the details which are not relevant for a given task. In programming, abstraction presents to the developers a simple version of a complex action instead of the in depth steps which take place. For example a programmer may use a function like sendEmail( which takes care of all the background processes related to connecting to a mail server, putting together the network request, handling of authentication, transfer of data and the server’s response. 

The function in question gives out what may be a very useful interface while the in depth implementation is kept out of sight. Also abstraction allows the developer to think about what each element in the system does as opposed to what goes on inside each element. This in turn makes large scale systems easier to understand as developers are put to work with high level issues that pertain to the problem at hand.

Programming abstraction showing a simple interface hiding complex implementation details

Levels of Abstraction in a Software System

Abstraction is present at many levels in a software system. A function is a basic kind of abstraction which puts a name to a set of operations which can be run as a unit and which we reuse without going into the detail of what goes on inside. We have higher levels of abstraction with classes and objects which we use to represent things like customers, payments, products, or accounts. Also we have application programming interfaces which are an abstraction that allows one system to ask for a service from another without knowing that other system’s internal structure. 

Also at the level of programming languages we have abstraction which hides the details of the computer hardware. We write loops, we use variables, we make functions and classes which all are abstractions that do not require us to go into the detail of what the processor is doing. By the way we structure our software we are in fact to a great extent abstracting from the low level computer goings on and instead we are solving application based problems with concepts which are much more related to what the application is for.

How Abstraction Helps Manage Complexity

One of the primary benefits of abstraction is that it manages complexity. As programmers work on large scale applications they are unable to hold in their head all of the implementation details at once. If each component presented full access to its internal operations what we’d see is that developers would have to familiarize themselves with a great deal of info before they could make even a small change. Abstraction which in turn lessens this burden by putting out only the info which is relevant to use a component properly. 

Take for example a database library which has methods for creating records, finding users, updating info, and deleting records. A developer using the library does not in fact need to know how database connections are pooled, how queries are optimized, or how low level communication with the database server takes place. The library sets a boundary between the developer and those details. That which does away with the need for a deep understanding of the inner workings also gives us the benefit of a stable external interface as the internal implementation changes.

Avoiding Unnecessary Abstraction

Good abstract design also requires care in judgment as we may over abstract or introduce unnecessary layers that in turn makes software hard to read. An abstraction should present a big picture idea and at the same time give a useful point of access for what needs to be done. Should a coder put forth extra classes, wrappers or functions just to add structure to the system what may result is greater complexity rather than the opposite. 

Also, proper abstraction is in fact a matter of which details to put forward at a given level of detail and what to keep out of the view behind an interface. We are not trying to hide everything but to present the relevant info to the relevant parts of the system. What is the goal of good abstraction design is that developers work at the level of detail that is appropriate and go into lower level implementation only when they have to to look into or change it.

Encapsulation and Control of Internal State

Encapsulation is very much a related but different concept from abstraction. It is more so concerned with the group of data and the functions which operate on that data at the same time which also includes controlling which of the elements internal aspects can be accessed or changed. In the field of object oriented programming we see that encapsulation is put into practice via the use of classes, access modifiers, methods, and properties. For example in a BankAccount class we may have an internal balance which we do not want the whole program to have access to instead we present to the rest of the program certain actions which may be performed like deposit( and withdraw(. 

That is, we do not allow the program at large to change the balance directly, rather we go through these methods which in turn perform the change. Also we may put in checks within these functions to ensure that crazy transactions do not go through and that all conditions are met before a given operation is finished. In this way we create a protected environment around an object’s internal state and also we are able to avoid the application as a whole breaking by outside parties which may try to put in invalid inputs.

Encapsulation in programming showing controlled access to internal object data

Controlled Access to Data and Behavior

Encapsulation also does not require that all of a component’s data be off limits to external elements. What it does require is that access be controlled which in turn supports the component’s intended function. A class may present public methods which in turn hide the method of implementation, thus other parts of the program interface with the object via pre-defined actions. In languages that support private, protected, or public members developers have the ability to set those boundaries. 

Other languages may use different methods like modules, closures or other means to achieve the same results. What is key is that a component has control over its internal state and what it presents to other components is what is relevant for their interaction. This in turn reduces accidental dependencies and makes it easy to change out the internal doings of a component without at the same time changing the greater application.

Information Hiding and Reduced Dependencies

Information is what we put into concealing implementation details which other parts of a program don’t need to know. Though information hiding and encapsulation are very much related they are not the same thing. Encapsulation is that which we see in the bundle of data and behavior and also in the control of access, while information hiding is more to do with the issue of what not to share about the implementation. For example a component may present an interface of findCustomer( which in turn may be storing customers in a relational database, a document database, a cache or some other storage which is internal. 

Other elements of the application only have to know how to get a customer via that component’s interface. Should the storage change at a later date the application may continue to use the same interface. Thus information hiding also reduces the amount of implementation detail which has to be passed around the code base.

Reducing Unnecessary Dependencies

Is to reduce the amount of extraneous knowledge between elements which in turn reduces dependencies. For example now we have a situation where dozens of files are very specific in their use of the database queries to get customer info out of the database. In the case of a change in the database structure, we may have wide ranging issues across the application. But if we put the database operations into a separate component that the rest of the app talks to through a defined interface instead of the database’s implementation details, we create a more clear division of what each part is responsible for. 

Also this will have the effect of fewer direct dependencies which in turn makes the software easier to test, refactor, debug and extend as changes in one component are less to impact other non related components. Thus information hiding is a key approach in the control of the spread of implementation details and in the prevention of a local change in a system from turning into a large-scale issue across the entire application.

Modular Design of Large Systems into Components.

Modularity is a practice that puts a software system’s components into separate and more manageable units which we call modules. In each module we try to have a very well defined responsibility and put in place a specific interface that which it uses to interact with other modules. As against viewing an application as one huge code base, developers are to break it into areas like authentication, payments, notifications, reporting, user management, and data storage. In these areas you will find the functions, classes, and other resources that they require for that particular area’s function. 

This structure also makes large scale applications easy to navigate since which area a certain feature belongs in is at once apparent. Also modularity supports parallel development which in turn means that different programmers or teams may work on different components at the same time without the need to edit the same files. Also when modules are given clear boundaries developers do not have to have a knowledge of all other modules in the application in order to understand what a component does.

image 549

High Cohesion and Low Coupling

A good design for a module usually goes for high cohesion and low coupling. What we mean by high cohesion is that which elements within a module are very much so related to the module’s responsibility. For example a module for payment should include only what is very much related to payment processing and not at all user interface features. Also, low coupling is when a module has minimal and irrelevant dependencies on other modules. 

When a component has many complex connections to other components a small change may cause a chain reaction of changes in the whole system. But with modules that have clean interfaces and few dependencies we see that which is easy to replace, test, and maintain. Modularity is not that modules never talk to each other in a system, they do. The goal is to have those interactions be a choice, easy to understand, and for what is really needed.

Code Organization and Maintainability

Code organization is the practice in which we arrange source code in a way that is easy for developers to understand, locate, modify and extend. This may include the use of certain naming conventions, directory structures, separate files, defined modules, single class responsibilities, small functions, documentation, and also the use of consistent architectural patterns. As a project grows, good organization becomes even more important because developers spend a large amount of time reading and reworking existing code instead of writing totally new code. 

Also when related features are put in unrelated files or one file has many unrelated responsibilities the application becomes needlessly complex to understand. Well organized code has predictable locations for features and also makes it easy to see the relationships between different parts of a program. Also it makes on-boarding new developers much easier as they can learn the structure of the system without trying to piece together the architecture from thousands of separate lines of source code.

Organizing Code Around Application Concepts

Code architecture does very well when it mirrors the thought structure of the application. For instance an e-commerce application may put product management, shopping carts, customer accounts, orders, payments, and notifications into their own components. Within those fields related features and classes are put according to what they do. Also consistent naming which is a practice may improve navigation as developers will be able to tell what a function, class, file, or module is for just by looking at the name. 

At the same time organization should be practical and not become the main goal. Too much fragmentation forces developers to go through too many files for a simple task. What we see in effective organization is a balance between separation of concerns and ease of access which puts related functions together but keeps unrelated issues separate.

How These Principles Work Together

Abstraction, encapsulation, information hiding, modularity, and code organization are often put forth separately, but in fact their best value is in how they play together. In a payment processing application we see that a payment module may present an interface like processPayment( which at the same time hides the interaction with third party payment providers. Encapsulation may protect internal payment data and also what methods may use that data. 

Information hiding may also prevent outside components from tying into provider specific details. Modularity may separate payment functions from customer accounts, product info, and order fulfillment. Also at play is the issue of where in the code base related payment classes, services, tests, and config should go. Each of these elements deal with a different issue of scale and complexity, but as a team they create a structure that allows the system to grow without making it a requirement that every component knows about all the others.

Developers reviewing organized software architecture with modular components and interfaces

Supporting Software Reuse

These principles also put forth the idea of software reuse. As functionality is put into a well defined component with a proper interface it tends to be reused by many parts of an application. A logging module, authentication service, validation library, or notification component may provide the type of functionality which many other components require. Instead of re Implementing the same feature over and over again developers can build it out once and present a consistent interface. 

Reuse does reduce duplicated code in many cases but it is to be approached with care. A component should be very general in what it does to support many use cases at the expense of possibly being too simple for some. Good abstractions and modularity help developers to identify what is reusable while at the same time encapsulation and information hiding protect users of that functionality from the implementation details which they do not need to know.

Applying the Practices in Real World Software Projects.

Developers do not have to reconstruct an entire application at once. It is better instead to begin from within a single code section, as a practical start point. See if a given class’/function’s set of tasks are related or if some are out of place. When a single class or function is found to be performing many different jobs out of which some are unrelated, take those out of it. Then it is a task of which other components in the system actually require to know these internal functions’ details and what can be created as an interface for those essential functions. 

The non-essential inner workings can stay within the private scope of the relevant component. Also, in case the same set of features are used by many different aspects of the application that functionality may be refactored into a separate module or service. Tests also will be a useful tool here as components which have clearly defined what they do and what their external interfaces are — that is, what they present to the rest of the system are easier to put through independent tests. Thus refactoring is done in small, incremental changes which as a whole reorganizes the system without the need for a full scale remake.

Choosing the Right Degree of Separation

The right degree of separation is based on the size and purpose of the application. In the case of a small scale project a complex architecture of many modules and interfaces is not a must. We see that in a simple program over engineering the abstraction may in fact make the code harder to read. At the same time a large scale application which has many developers and is very much in flux may greatly benefit from well defined boundaries between components. 

Thus it is to the developer’s dox to look at the actual complexity of the issue at hand when they are determining how much abstraction, encapsulation and modularity to put in. Also the goal is not to apply a certain architectural pattern by the book. Instead these principles should guide developers in making intentional decisions on issues of responsibility, dependency, interface and implementation details.

Common Issues in the Use of Abstraction and Modularity.

One issue is that we see developers introduce abstractions before they have a full grasp of the issue at hand. Also it is a practice that some developers get into by adding in extra layers, interfaces, or classes which may look very architectural at first glance, but which for very simple base functions are not needed. This plays out in what we sometimes term as unnecessary abstraction which in turn causes developers to wade through many layers of code to perform what is in fact a very basic task. Also we see that which is put forth as modular design which in reality is not that is when each module is very much a part of the other although they may be separate entities. 

If each module is living in the other’s business and requires in depth knowledge of them to function then we may have lots of pieces to our system but in fact very little true modularity. What we need in effective modularity is well defined roles for each component and minimal dependencies between them instead of just an increase in the number of elements.

Preventing Internal Details from Spreading

Another issue is when we see internal details spill over into the external code. If the latter starts to depend on the former’s database structures, private fields, vendor specific features or any other element of the implementation then we see that changes become more difficult. Also a class which puts too much of its state out for mutation can in turn make it hard to guarantee that its data stays valid. 

Developers may reduce these issues by putting in place defined interfaces, which have a single responsibility, and by review of dependencies during refactoring. We also see documentation and automated tests as tools which clarify how a component is to be used. The goal is not full isolation but to create a structure which is practical and in which we may have a fair idea of what other elements of the application will be affected by change.

Conclusion

Abstraction, encapsulation, modularity, information hiding and code organization present different but complementary approaches to deal with software complexity. Abstraction which allows us to work with high level concepts that ignore low level implementation details. Encapsulation which gives us control over our data and behavior, and information hiding which keeps internal implementation details private. Modularity which takes large applications and breaks them into more manageable and责任明确的 components. Code organization which in turn gives us a practical structure to easily navigate, understand, test and maintain those components. These principles become even more important as applications grow in size which is not only from the amount of code we write but also from the number of relationships and interactions between different elements of the system.

It is a framework which enables you to think through the structure of software and develop applications that will grow with the changing of their requirements.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
0
Would love your thoughts, please comment.x
()
x