The TOP 30 questions in a technical interview about OOP (Python & Java)

Efim Shliamin
69 min readMay 10, 2024

--

My website:

Welcome to this article, where I aim to comprehensively overview the top 30 technical interview questions on object-oriented programming (OOP)! 🙂 I’ve encountered these questions during my career in computer science. We will use Python 3 and Java 8 to explain it, and I will show you simple-world analogies & how scientists use OOP in genomic data analysis. This article benefits those looking for a Software Engineer job and those who want to see the differences and similarities between Python and Java syntax with live examples. Let’s start! 🛫

Note: Python and Java are powerful programming languages widely used in various fields. Both are employed in web development: Python with frameworks like Django and Flask and Java with Spring and Hibernate. Python excels in scientific and analytical computing with libraries such as NumPy, SciPy, and pandas, making it ideal for data processing and statistical analysis. It’s also dominant in machine learning and artificial intelligence, using libraries like TensorFlow, Keras, and PyTorch. Additionally, Python is used for scripting to automate tasks such as system administration and web scraping. Java, known for its stability, scalability, and reliability, is favored in enterprise environments for backend systems and enterprise applications and is commonly used in financial sectors and Android development.

Okay, let’s start with the very first question 🙂:

1. What is object-oriented programming (OOP)?

🎯: Object-oriented programming is a programming paradigm based on the concept of objects rather than functions and logic, which can be defined as a data field that has unique attributes and behavior.

To begin with, let’s imagine we have a house with two elevators and ten floors. How would we describe it in a programming language using OOP? 👨🏼‍💻

In object-oriented programming (OOP), several classes can be created to represent a building with elevators, each responsible for a specific functionality. Here is an example of how such a system can be organized (using Python naming style):

class Building:

  • Attributes:
    floors: the number of floors in the building.
    elevators: list of elevators in the building.
  • Methods:
    call_elevator(floor, direction): method to call an elevator to the floor with direction (up or down).

class Elevator:

  • Attributes:
    current_floor: the current floor on which the elevator is located.
    state: the state of the elevator (e.g., moving up, moving down, waiting).
    max_capacity: the maximum capacity of the elevator.
    occupancy: the current number of people in the elevator.
  • Methods:
    move_up(): moves the elevator one floor up.
    move_down(): moves the elevator one floor down.
    open_door(): opens the elevator doors.
    close_door(): closes the elevator doors.
    go_to_floor(target_flor): sends the elevator to the specified target_floor.

class Controller (optional):

  • Methods:
    assign_elevator(request): determines which elevator will respond to the request based on all elevators' current state and locations.

Here, I’m presenting an example of such code in Python, which you don’t necessarily need to understand 100% for now. Later, we will deal with some aspects of the following questions, and at the end of this article, I will explain everything much more deeply (again). 🤓 So, let’s start:

class Building:
def __init__(self, number_of_floors, number_of_elevators):
self.floors = number_of_floors
self.elevators = [Elevator() for _ in range(number_of_elevators)]

def call_elevator(self, floor, direction):
# Here's the logic for choosing the right elevator
pass

class Elevator:
def __init__(self):
self.current_floor = 0
self.state = "waiting"
self.max_capacity = 10
self.occupancy = 0

def move_up(self):
if self.current_floor < building.floors - 1:
self.current_floor += 1
self.state = "moving up"

def move_down(self):
if self.current_floor > 0:
self.current_floor -= 1
self.state = "moving down"

def open_door(self):
self.state = "doors open"

def close_door(self):
self.state = "doors closed"

def go_to_floor(self, target_floor):
while self.current_floor != target_floor:
if self.current_floor < target_floor:
self.move_up()
elif self.current_floor > target_floor:
self.move_down()
self.open_door()
self.close_door()

# Creation of a building with ten floors and two elevators
building = Building(10, 2)

… and what about Java? Here’s the answer:

import java.util.ArrayList;
import java.util.List;

class Building {
private int floors;
private List<Elevator> elevators;

public Building(int numberOfFloors, int numberOfElevators) {
this.floors = numberOfFloors;
this.elevators = new ArrayList<>();
for (int i = 0; i < numberOfElevators; i++) {
this.elevators.add(new Elevator());
}
}

public void callElevator(int floor, String direction) {
// Here's the logic for choosing the right elevator
}
}

class Elevator {
private int currentFloor;
private String state;
private int maxCapacity;
private int occupancy;

public Elevator() {
this.currentFloor = 0;
this.state = "waiting";
this.maxCapacity = 10;
this.occupancy = 0;
}

public void moveUp() {
if (this.currentFloor < building.floors - 1) {
this.currentFloor++;
this.state = "moving up";
}
}

public void moveDown() {
if (this.currentFloor > 0) {
this.currentFloor--;
this.state = "moving down";
}
}

public void openDoor() {
this.state = "doors open";
}

public void closeDoor() {
this.state = "doors closed";
}

public void goToFloor(int targetFloor) {
while (this.currentFloor != targetFloor) {
if (this.currentFloor < targetFloor) {
this.moveUp();
} else if (this.currentFloor > targetFloor) {
this.moveDown();
}
this.openDoor();
this.closeDoor();
}
}
}

public class Main {
// Creation of a building with ten floors and two elevators
public static Building building = new Building(10, 2);
}

We used new words like attribute, method, and class to describe a simple real-world situation. So, let’s continue our list of questions like this:

2. What is a class in OOP?

🎯: A class is a blueprint or prototype from which objects are created. It defines the attributes and methods.

In the example above, we saw how I created a class for an elevator, which we consider an object in our OOP. Let’s give some more code examples for this class:

Python demonstrates the class Elevator with one attribute:

class Elevator:
# A class named Elevator with one attribute (current_floor)
def __init__(self):
self.current_floor = 0

Java demonstrates the public class Elevator with one attribute as well:

public class Elevator {
// A class named Elevator with one attribute (currentFloor)
private int currentFloor;
}

The code examples show subtle differences in class definitions between Java and Python: they have different traditional naming styles, and Python does not support explicit access modifiers like private or public in the same way that Java does. Instead, Python uses a naming convention to signal the intended access level for class attributes.

In Java, the attribute currentFloor is declared private, meaning it cannot be directly accessed or modified outside the Elevator class. This encapsulation is enforced strictly by the Java language using access modifiers.

While explicit access modifiers are not supported in Python, the language uses naming conventions to indicate the accessibility of attributes. A single underscore prefix _ (e.g., _protectedAttribute) suggests protected access (meant for use within the class and its subclasses), and a double underscore prefix __ (e.g., __privateAttribute) is used for private attributes, which Python enforces through name mangling. This makes accessing or modifying these attributes outside the class harder.

3. What are the attributes and the methods in the context of OOP?

🎯: Attributes are variables defined within a class that store the state of the objects. Methods are functions defined within a class that determine the behavior of the objects.

Python demonstrates thego_to_floor(self, floor) method in the Elevator class so that you can change the floor the elevator is on:

class Elevator:
def __init__(self):
self.current_floor = 0

def go_to_floor(self, floor):
self.current_floor = floor # Method to change the floor

Java demonstrates thegoToFloor(int floor) method in the Elevator class so that you can change the floor the elevator is on as well:

public class Elevator {
private int currentFloor;

public void goToFloor(int floor) {
this.currentFloor = floor; // Method to change the floor
}
}

☝️ One crucial aspect of the difference between the two languages I did not mention in the previous question, which the attentive reader could surely have noticed. 😅 There are several essential aspects to consider, including the language type system (static vs. dynamic and strong vs. weak typing), the data types available (primitive [int, float, char, boolean], composed [arrays, lists, tuples, sets, classes, dictionaries, stacks, queues, linked lists], special [null, void, enums, any, pointer, union]), and how they impact programming style, performance, and safety.

Note: This article will not describe all the data types and where they are used in detail, but we will cover the other aspects more deeply.

First of all, what are strong and weak typing? Languages like Python and Java require you to convert types when needed explicitly. This helps avoid errors and makes the code more transparent. On the other hand, languages like PHP and JavaScript are more flexible and perform type conversions automatically. This can be convenient but may lead to unexpected bugs.

Static or dynamic typing? In statically typed languages like Java, C, and C++, variable types must be explicitly declared and are determined at compile-time. This pre-runtime type awareness helps catch type errors early, enhancing code reliability and improving performance through compiler optimizations. Conversely, dynamically typed languages like Python and JavaScript determine variable types at runtime. This adds flexibility by allowing variables to hold different types without explicit declarations. However, it may increase runtime bugs due to unexpected type errors.

In the last Python code snippet, dynamic typing is demonstrated rather than static. The current_floor attribute is initialized to 0 without an explicit type declaration. In Python, variables do not need an explicit type specified; they can hold any data type. Similarly, the floor parameter in the go_to_floor method does not have a declared type, highlighting Python’s dynamic type system. The type of floor is determined based on the value passed to the method at runtime. The current_floor attribute can be assigned values of different types throughout the program’s execution. For instance, although it is initialized as an integer 0, it could be reassigned to another type, like a string or a float, without causing a compile-time error. This flexibility is a characteristic feature of dynamically typed languages. Because Python does not enforce type constraints at compile time, errors related to unexpected types might only appear when the affected part of the code is executed.

In the last provided Java code snippet, static typing is implemented by explicitly declaring the variable type. Here’s how it’s demonstrated: The variable currentFloor is expressly stated with the type int within the Elevator class. This declaration specifies that currentFloor can only hold integer values, adhering to Java's static typing system. The goToFloor method is defined with a parameter floor explicitly declared as an int. This means any value passed to goToFloor must be an integer, and this type of check is enforced at compile time. The currentFloor can only be assigned throughout the Elevator class, as defined by its declaration. Attempting to assign a value of another type (like a string or a floating-point number) would result in a compile-time error, a fundamental aspect of static typing. This ensures that type mismatches are caught early in development, preventing type-related runtime errors.

Conclusion: Strong, static typing generally makes code easier to understand but more verbose, while dynamic, weakly typed languages offer concise code that can be faster to write but harder to maintain. Statically typed languages usually run faster because the system can optimize based on known types. Dynamically typed languages might be slower because types are checked during execution. Strong and static typing can reduce runtime errors and simplify debugging. Weak and dynamic typing speeds up development but increases the risk of runtime issues.

4. What is the difference between a class and an object?

🎯: A class is the definition, while an object is an instance of that class.

Simple real-world analogy: if the object is a house 🏡, then the house plan 📄 is a class.

Python (my_elevator is an object of classElevator):

class Elevator:
def __init__(self):
self.current_floor = 0

my_elevator = Elevator() # my_elevator is an object of class Elevator

Java (myElevator is an object of class Elevator):

public class Elevator {
private int currentFloor;
}
Elevator myElevator = new Elevator(); // myElevator is an object of class Elevator

5. What are the advantages and disadvantages of using OOP?

🎯: Advantages include modularity, reusability, and a clear structure. Disadvantages can be increased complexity and slower execution time.

We won’t need code samples here & can easily answer the next question. 👇

6. Explain the basic principles of OOP.

🎯: In OOP, there are four main principles:

  1. Encapsulation hides an object’s internal state and requires all interaction through methods.
  2. Inheritance creates a new class based on an existing class.
  3. Polymorphism is the ability to redefine methods in derived classes.
  4. Abstraction defines a base class with generic behavior.

To answer this question, we will individually review all four OOP principles and explain them in two programming languages. We will briefly describe these four OOP principles using simple real-world analogies.

1.1 A simple real-world analogy of encapsulation: Consider a coffee machine: add water and beans, press a button, and get your coffee. Inside, the machine handles complex tasks like grinding beans and heating water, but you don’t see or manage these processes directly. The buttons and slots you interact with are like the public methods in a programming class, designed for user interaction, while the internal mechanics are hidden. This setup illustrates encapsulation in programming, where a class shields its internal workings but provides a user-friendly interface. Just as a coffee machine manufacturer can upgrade internal components without affecting how you make coffee, software developers can change code internals without impacting external users as long as the external interfaces remain unchanged.

In simple words, encapsulation helps to make using a program as easy and safe as using an elevator in a large building without requiring people to know about its structure’s complexities. This makes programs understandable and easy for everyone, even if they don’t know how it works from the inside. 🙂

1.2 The principle of encapsulation in Python:

class Building:
def __init__(self):
self.elevator = Elevator() # Encapsulation

# Method that uses an encapsulated object's functionality:
def call_elevator(self, floor):
self.elevator.go_to_floor(floor)

In the Building class, theElevator instance is created as an attribute. This elevator attribute is an example of data (an object in this case), and it's bundled with methods like call_elevator. Although not fully shown, the Elevator class typically contains methods like go_to_floor and possibly its attributes (such as current_floor). This bundling of data and methods within the Elevator class is also called encapsulation. The elevator attribute in the Building class is used to demonstrate encapsulation by providing controlled access to the elevator's functionality through the Building class's methods.

1.3 In Java, the principle of encapsulation is similar:

public class Building {
private Elevator elevator; // Encapsulation

public Building() {
this.elevator = new Elevator(); // Composition
}

// Method that uses an encapsulated object's functionality
public void callElevator(int floor) {
elevator.goToFloor(floor);
}
}

So, encapsulation is a fundamental concept in OOP that involves bundling the data (attributes) and the methods that operate on the data into a single unit or class. It also restricts direct access to some of an object’s components, which can prevent the accidental modification of data. Encapsulation allows complex operations to be hidden behind simple method calls. For example, the call_elevator method of the Building class provides a simple interface for moving the elevator. The details of how the elevator moves, how it tracks its current floor, and how it handles errors are hidden inside the Elevator class. 🔐

2.1 A simple real-world analogy of inheritance: Inheritance in programming is like a family tree where traits pass from parents to children. In object-oriented programming (OOP), a base class (like grandparents) shares common attributes and behaviors (like weight or color) that derived classes (like children) inherit. For example, from a base class Vehicle, subclasses Car and Bicycle might inherit attributes such as color and methods like start. Still, they also add unique features like engine size for Car and gears for Bicycle. Derived classes can also modify inherited methods to suit their needs. This concept helps reduce code redundancy and simplifies maintenance, much like knowing family traits helps understand family members without re-describing each one thoroughly.

In simple terms, inheritance is when a class takes on the properties and methods of another class.

2.2 The principle of inheritance in Python:

class Building:
def __init__(self, floors):
self.floors = floors

class Skyscraper(Building):
def __init__(self, floors, offices):
super().__init__(floors) # Inherits floors from Building
self.offices = offices

As you can see, if we have a class called Building and we want to create a particular type of building, such as a Skyscraper, the Skyscraper could inherit properties from the Building, such as having elevators and multiple floors. The word super refers to a function or keyword used to call methods from a parent class (also known as a superclass). The super() function is used primarily to avoid the direct naming of the parent class, thus making the code more maintainable and flexible.

2.3 The principle of inheritance in Java:

// Define a base class for Building
public class Building {
private int floors;

// Constructor for Building
public Building(int floors) {
this.floors = floors;
}

// Getter method for floors
public int getFloors() {
return floors;
}
}

// Define a subclass for Skyscraper that inherits from Building
public class Skyscraper extends Building {
private int offices;

// Constructor for Skyscraper
public Skyscraper(int floors, int offices) {
super(floors); // Call the superclass constructor with the floors
this.offices = offices;
}

// Getter method for offices
public int getOffices() {
return offices;
}
}

In Java, classes are inheritable by default unless marked with the final keyword. Java does not have primary constructors. Properties are typically declared separately from constructors, and you must explicitly assign parameter values to these properties within the constructor body. In Java, fields are declared independently, and you must expressly define getters and setters if you want to encapsulate the fields correctly. In Java, the superclass constructor is used inside the subclass constructor, an inheritance mechanism.

3.1 A simple real-world analogy of polymorphism: Imagine a universal remote control 🎮 that operates multiple devices like TVs, Blu-ray players, and stereo systems, each with functionalities such as turning on/off, changing channels, playing music, or stopping playback. This remote control uses a standard set of buttons to interface with all these devices, regardless of their distinct functions. This concept mirrors polymorphism in programming, where a single method in a superclass (e.g., powerOn()) can be invoked by any subclass object, each implementing the method in a way suitable to its specific functionality. Similarly, while some buttons like "volume up" work universally across TVs and stereos, others like "play" are specific to devices that can play media. This setup in programming allows writing code that operates on a general class type (superclass). Still, each subclass responds based on its unique method implementations, akin to how the universal remote controls different devices.

In simple terms, polymorphism is using a unified interface to operate on objects of different classes. It’s like being able to perform the same action in various ways. For example, if you have a method in Building that is use_space(), you might use the space differently in a Skyscraper than in a regular Building. For example, maybe in a Building, use_space() refers to living areas, and in a Skyscraper, it relates to office spaces. Despite these differences, you can call use_space() on any building type, which will behave appropriately for that specific building type.

3.2 The principle of polymorphism in Python:

class Building:
def use_space(self):
print("Using space as a residential area.")

class Skyscraper(Building):
def use_space(self):
print("Using space as an office area.")

# Example of polymorphism
def use_building_space(building):
building.use_space() # Correct use_space method is called depending on the building type

And what is the output? Well, for example, we can create this code in Python to understand the polymorphism impact:

# Testing the function with instances of Building and Skyscraper
b = Building()
s = Skyscraper()

use_building_space(b) # Outputs: Using space as a residential area.
use_building_space(s) # Outputs: Using space as an office area.

3.3 The principle of polymorphism in Java:

class Building {
void useSpace() {
System.out.println("Using space as a residential area.");
}
}

class Skyscraper extends Building {
@Override
void useSpace() {
System.out.println("Using space as an office area.");
}
}

public class Main {
public static void main(String[] args) {
Building b = new Building();
Skyscraper s = new Skyscraper();
useBuildingSpace(b); // Outputs: Using space as a residential area.
useBuildingSpace(s); // Outputs: Using space as an office area.
}

static void useBuildingSpace(Building building) {
building.useSpace();
}
}

There are two classes: Building and Skyscraper. The Skyscraper class extends the Building class, meaning Skyscraper is a specialized form of Building. This is an example of inheritance, where a Skyscraper inherits properties and methods from a Building. Both classes have a method named useSpace(). In Building, it prints "Using space as a residential area." In Skyscraper, this method is overridden (as indicated by the @Override annotation) to print "Using space as an office area." This shows that Skyscraper, based on Building, uses its space differently. The primary method in the Main class creates instances of both Building and Skyscraper. It then calls the useBuildingSpace method with each instance. Despite useBuildingSpace being defined to take a Building object, it can also accept a Skyscraper object because a Skyscraper is a Building due to inheritance. This capability to process objects of a subclass where a superclass object is expected is known as polymorphism.

4.1 A simple real-world analogy of abstraction: Consider how you drive a car. Driving a vehicle involves using a simplified interface — steering wheel, pedals, and indicators — without understanding the complexities of the car’s engine, transmission, or braking mechanisms. This abstraction by engineers allows anyone to drive a car by focusing on essential controls like speed and fuel levels rather than the underlying technical details. This user-friendly design ensures safe and effective driving by hiding the complexities and presenting only the necessary information to the driver. 🚙

Abstraction means hiding complex reality while exposing only the necessary parts. Building could define an abstract method called managed_security() in our Building example. It doesn't specify how security is managed. Specific types of buildings might implement this in different ways.

4.2 The principle of abstraction in Python:

from abc import ABC, abstractmethod

class Building(ABC):
@abstractmethod
def manage_security(self):
pass # We don't define how to manage security here

class OfficeBuilding(Building):
def manage_security(self):
print("Security managed through keycard access.")

class ResidentialBuilding(Building):
def manage_security(self):
print("Security managed through door locks and security personnel.")

To demonstrate how this code works, you must create instances of OfficeBuilding and ResidentialBuilding and call their manage_security methods. For example:

office = OfficeBuilding()
residential = ResidentialBuilding()

# This will print: Security managed through keycard access:
office.manage_security()
# This will print: Security managed through door locks and security personnel:
residential.manage_security()

4.2 The principle of abstraction in Java:

abstract class Building {
// Declare an abstract method
abstract void manageSecurity();
}

class OfficeBuilding extends Building {
// Provide an implementation of the abstract method for OfficeBuilding
@Override
void manageSecurity() {
System.out.println("Security managed through keycard access.");
}
}

class ResidentialBuilding extends Building {
// Provide an implementation of the abstract method for ResidentialBuilding
@Override
void manageSecurity() {
System.out.println("Security managed through door locks and security personnel.");
}
}

public class Main {
public static void main(String[] args) {
OfficeBuilding office = new OfficeBuilding();
ResidentialBuilding residential = new ResidentialBuilding();

// This will print: Security managed through keycard access:
office.manageSecurity();
// This will print: Security managed through door locks and security personnel:
residential.manageSecurity();
}
}

Building is defined as an abstract class using the abstract keyword. It has one abstract method, manageSecurity(), which was declared but not implemented in the Building class. This method serves as a template for subclasses to follow, ensuring that any class that extends Building must implement manageSecurity(). OfficeBuilding and ResidentialBuilding extend Building and provide specific implementations of the manageSecurity() method. This is a classic example of abstraction in OOP, where the base class provides a general framework, and subclasses provide detailed behaviors. The @Override annotation is used in Java to indicate that the method overrides a superclass method. It helps catch common bugs, such as misnaming the overridden method or not correctly matching parameters. In the Main class, instances of OfficeBuilding and ResidentialBuilding are created, and their manageSecurity method is called. The output will be specific to the type of building, demonstrating polymorphism alongside abstraction.

7. How is inheritance implemented in your primary programming language?

🎯: In Java and Python, inheritance is implemented by extending a base class.

We discussed this in great detail in the sixth question, so here, we will show only examples of codes and go straight to the eighth question. 😃

class Building:
def __init__(self, floors):
self.floors = floors

class Skyscraper(Building):
def __init__(self, floors, offices):
super().__init__(floors) # Inherits floors from Building
self.offices = offices
// Define a base class for Building
public class Building {
private int floors;

// Constructor for Building
public Building(int floors) {
this.floors = floors;
}

// Getter method for floors
public int getFloors() {
return floors;
}
}

// Define a subclass for Skyscraper that inherits from Building
public class Skyscraper extends Building {
private int offices;

// Constructor for Skyscraper
public Skyscraper(int floors, int offices) {
super(floors); // Call the superclass constructor with the floors
this.offices = offices;
}

// Getter method for offices
public int getOffices() {
return offices;
}
}

8. Parallel vs. Asynchronous Programming. Can you name the difference?

🎯: While parallel and asynchronous programming are used to improve the efficiency of applications, they serve different purposes and are structured differently.

Parallel Programming

Meaning: Parallel programming involves executing multiple operations at the same time. It is usually employed to speed up processing tasks that can be performed concurrently. This technique is often used in systems with multi-core processors where tasks can genuinely run in parallel.

Architecture: In a parallel programming model, tasks are typically divided into smaller sub-tasks that run simultaneously on separate cores or processors. This can involve complex coordination and synchronization of threads or processes to manage shared resources and task dependencies.

Example: Processing large datasets or performing complex calculations on arrays where each element or subset of the data can be processed independently.

Python Example for Parallel Programming: Using concurrent.futures

Python’s concurrent.futures module provides a high-level way of creating asynchronous execute calls via thread pools or process pools. Here’s an example of using ThreadPoolExecutor for parallel execution of a function.

import concurrent.futures
import time

def compute_square(n):
time.sleep(1) # simulate some computation time
return n * n

numbers = [1, 2, 3, 4, 5]

with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
results = executor.map(compute_square, numbers)

print(list(results)) # Outputs: [1, 4, 9, 16, 25]

In this example, each number in the numbers list is processed in parallel and takes a second to compute. Since they are processed in parallel, the total time is just over a second rather than 5 seconds.

Java Example for Parallel Programming: Using java.util.concurrent

Java provides the ExecutorService to manage threads in a thread pool. Here’s how you can achieve parallel execution.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ParallelExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);

for (int i = 1; i <= 5; i++) {
final int number = i;
executor.submit(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(number * number);
});
}

executor.shutdown();
try {
executor.awaitTermination(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

This Java code creates a pool of 5 threads and submits five tasks. Each task calculates the square of a number and prints it. All tasks are executed in parallel. 🤌

Asynchronous Programming

Meaning: Asynchronous programming is about performing tasks non-blocking, allowing a program to continue executing other tasks while waiting for previous tasks to complete. It’s commonly used in I/O operations, user interfaces, or network requests.

Architecture: Asynchronous programming often uses callbacks, promises, or events to handle tasks that take some time to complete, such as fetching data from a network or writing to a file. This allows the main program to run without waiting for these tasks to finish.

Example: Web servers handle HTTP requests, which might involve waiting for database queries or external API calls. Using asynchronous operations allows the server to handle multiple requests without waiting for each operation to complete.

Python Example for Asynchronous Programming: Using asyncio

Python’s asyncio library writes concurrent code using the async/await syntax. Here’s a simple example:

import asyncio

async def greet(delay, name):
await asyncio.sleep(delay)
print(f'Hello {name}')

async def main():
await asyncio.gather(
greet(1, 'Alice'),
greet(2, 'Bob'),
greet(3, 'Cindy')
)

asyncio.run(main())

This code will asynchronously greet three names at different delays. Although the total sleep time is 6 seconds, it will be completed in approximately 3 seconds because the waits happen concurrently.

Java Example for Asynchronous Programming: Using CompletableFuture

Java 8 introduced CompletableFuture, providing a non-blocking way to execute and chain asynchronous operations.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class AsyncExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Hello";
}).thenAccept(result -> System.out.println(result + " World"));

future.get();
}
}

After waiting asynchronously for approximately 1 second (due to Thread.sleep(1000) in the asynchronous task), the program will print: “Hello World”. This output is generated by the thenAccept stage, which appends " World" to "Hello" and prints the combined result. The main thread waits for the completion of the future using get(). 🙂

9. What is an abstract class, and what is it used for?

🎯: An abstract class cannot be instantiated and is used to provide a base definition for other classes.

We discussed this in great detail in the sixth question. But let’s explain this in another example (Python and Java):

from abc import ABC, abstractmethod

class Elevator(ABC):
@abstractmethod
def go_to_floor(self, floor):
pass

class PassengerElevator(Elevator):
def go_to_floor(self, floor):
print(f"Going to floor {floor}")
public abstract class Elevator {
public abstract void goToFloor(int floor);
}

public class PassengerElevator extends Elevator {
@Override
public void goToFloor(int floor) {
System.out.println("Going to floor " + floor);
}
}

In the last example, the Elevator class is declared abstract, meaning it cannot be instantiated independently and must be subclassed. It contains an abstract method goToFloor which also cannot have a body and must be implemented by any subclasses. The PassengerElevator class extends the Elevator and provides a concrete implementation of the goToFloor method. The override keyword indicates that this method implements an abstract method from the superclass. 🙌

10. How does the encapsulation mechanism work in OOP?

🎯: Encapsulation protects the data within an object from direct access from the outside and enforces methods.

There are code examples in question six. ☝️🤓

11. What is an interface, and how does it differ from an abstract class?

🎯: An interface is an abstract class containing only method signatures. Unlike abstract classes, it cannot store state.

A simple real-world analogy explaining the difference between an interface and an abstract class in programming: Think of an interface like a job description 📃 that lists duties without describing how to perform them, leaving the actual method implementation to the classes that adopt the interface. On the other hand, an abstract class acts like a team leader 🦸‍♀️ who defines some everyday tasks but also leaves room for specific roles to tailor certain duties. For instance, while an abstract class might provide a general way to start and end work, like logging into a computer system, it allows roles like Programmers or Designers to define how they perform their core duties. Thus, interfaces specify what must be done, and abstract classes outline how some things should be done, leaving specifics open to customization.

from abc import ABC, abstractmethod

class ElevatorControl(ABC):
@abstractmethod
def call_elevator(self, floor):
pass

class BasicElevatorControl(ElevatorControl):
def call_elevator(self, floor):
print(f"Elevator called to floor {floor}")
public interface ElevatorControl {
void callElevator(int floor);
}

public class BasicElevatorControl implements ElevatorControl {
@Override
public void callElevator(int floor) {
System.out.println("Elevator called to floor " + floor);
}
}

ElevatorControl is an interface in Java. It declares the callElevator(int floor) method but does not implement it. Interfaces in Java are used to define a contract for the classes that implement them, specifying what must be done without any instruction for a specific implementation. BasicElevatorControl implements the ElevatorControl interface and provides the particular implementation of the callElevator method. This is marked with the @Override annotation, indicating that this method fulfills the contract established by the interface.

In both the Python and Java examples, the principles of interfaces and abstract classes are showcased, albeit in slightly different technical contexts, due to the nature of each language. In Java, the difference is quite distinct; interfaces cannot have any method implementations (until Java 8 introduced default methods), while abstract classes can have a mix of implemented and abstract methods. Python’s abstract classes share similarities with Java’s interfaces but can include fully implemented and abstract methods. Python does not have a separate interface keyword; abstract classes are used to achieve similar functionality. 🙂

12. Microservice architecture. What is REST API from the architectural point of view?

🎯: Microservice Architecture is a method of developing software applications as a suite of small, independently deployable, modular services. Each service runs a unique process and communicates through a well-defined, lightweight mechanism to serve a business goal.

REST API in Microservice Architecture:

  • REST (Representational State Transfer) API (Application Programming Interface) is an architectural style that uses standard HTTP methods like GET, POST, PUT, DELETE, etc. It’s used for building scalable web services.
  • Architectural Perspective: REST APIs are stateless, meaning that each request from a client contains all the information the server needs to understand and respond to the request. This statelessness is particularly beneficial in a microservices architecture because it makes the services loosely coupled and more scale-accessible. The API interfaces are designed around resources (objects, data), and standard operations on these resources are provided via HTTP methods.

From an architectural point of view, REST APIs in a microservices architecture facilitate:

  • Decoupling of client and server: Clients and the server can be developed and deployed independently as long as the interface is not altered.
  • Scalability: Because the state is not stored on the server, scaling applications horizontally is easier.
  • Uniform Interface: REST APIs provide a standard way of interacting with the underlying service, making it easier for different services to communicate.

Microservices often communicate internally through RESTful interfaces, which help maintain a modular and extensible architecture. This setup allows individual microservice teams to implement updates and improvements independently of other services, provided the interfaces remain consistent.

Integrating REST APIs into microservice architectures is crucial for modern web applications, promoting agility, scalability, and flexibility.

I’ll provide simple examples for each language to demonstrate how microservices and REST APIs can be implemented in Python and Java. These examples include a basic REST API that could be part of a more extensive microservice architecture. 🏛️

Python Example with Flask:

To run this example, you need Flask installed. You can install it using pip:

pip install flask

Python code example for REST API:

from flask import Flask, jsonify, request

app = Flask(__name__)

# Simulated data store
pets = [
{"id": 1, "name": "Rex", "type": "Dog"},
{"id": 2, "name": "Mittens", "type": "Cat"}
]

# Get all pets
@app.route('/pets', methods=['GET'])
def get_pets():
return jsonify(pets)

# Get pet by id
@app.route('/pets/<int:pet_id>', methods=['GET'])
def get_pet(pet_id):
pet = next((pet for pet in pets if pet['id'] == pet_id), None)
if pet is not None:
return jsonify(pet)
else:
return jsonify({"error": "Pet not found"}), 404

# Add a new pet
@app.route('/pets', methods=['POST'])
def add_pet():
pet_data = request.get_json()
pets.append(pet_data)
return jsonify(pet_data), 201

if __name__ == '__main__':
app.run(debug=True)

This service has three endpoints:

  1. GET /pets — Returns a list of all pets.
  2. GET /pets/{pet_id} — Returns a single pet by ID.
  3. POST /pets — Adds a new pet to the list.

Java Example with Spring Boot:

Spring Boot is an extension of the Spring framework that simplifies the initial setup and development of new Spring applications. It’s particularly suited for building microservices and has built-in support for REST API development.

First, you’ll need to set up a new Spring Boot project. This is easy to do using Spring Initializr, which allows you to select Spring Web and Spring Boot DevTools dependencies.

Java code example for REST API:

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

@RestController
public class PetController {
private final List<Pet> pets = new ArrayList<>();
private final AtomicLong counter = new AtomicLong();

public PetController() {
// Initialize with some pets
pets.add(new Pet(counter.incrementAndGet(), "Rex", "Dog"));
pets.add(new Pet(counter.incrementAndGet(), "Mittens", "Cat"));
}

@GetMapping("/pets")
public List<Pet> getAllPets() {
return pets;
}

@GetMapping("/pets/{id}")
public ResponseEntity<Pet> getPetById(@PathVariable long id) {
return pets.stream()
.filter(pet -> pet.getId() == id)
.findFirst()
.map(pet -> ResponseEntity.ok(pet))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}

@PostMapping("/pets")
public Pet addPet(@RequestBody Pet pet) {
pet.setId(counter.incrementAndGet());
pets.add(pet);
return pet;
}
}

class Pet {
private long id;
private String name;
private String type;

// Constructors, getters, and setters
}

Important Concepts:

  • @RestController: This annotation marks the class as a controller where every method returns a domain object instead of a view. It’s shorthand for including both @Controller and @ResponseBody.
  • @GetMapping and @PostMapping: These annotations handle HTTP GET and POST requests.
  • ResponseEntity: Used to create HTTP responses with status codes.

To run the Java example, you would typically compile and run it using Maven or Gradle, which are integrated into the Spring Initializr project setup. 👌

13. What are the differences between static and non-static methods?

🎯: Static methods belong to the class, not a specific object, while non-static methods operate at the instance level.

A simple real-world analogy: In a school analogy, non-static methods are like daily activities 💃 🧗‍♂️ that students do individually, such as homework, which depends on each student’s unique attributes. Static methods are like the school bell 🔔— universal and independent of any student, signaling times for everyone. Non-static methods need an instance of the class to function because they operate on data specific to each instance, such as a car’s engine needing to be started by its specific start method, depending on its state. Static methods don’t require a class instance. They work independently like a school bell and are helpful for general purposes across all instances, such as a utility function to calculate interest that doesn’t depend on individual instance data.

In summary, non-static methods are instance-specific and manipulate or access data unique to each object. In contrast, static methods are general, not tied to any object, and can be called on the class.

Python static and non-static methods usage example:

class Elevator:
@staticmethod
def emergency_alert():
print("Emergency alert for all elevators!")

def go_to_floor(self, floor):
print(f"Elevator going to floor {floor}")

Theemergency_alert() method is static, as indicated by the @staticmethod decorator before its definition. Static methods do not take an implicit first argument like self (which refers to the instance of the class), indicating that they do not operate on instance-specific data. The method emergency_alert is designed to perform a function that applies generally, not to any specific instance of the Elevator. In this case, it prints an "Emergency alert for all elevators!" message, which conceptually could apply to an entire system of elevators rather than a single unit. This method can be called directly on the class itself without creating an instance of the class.

On the other hand, thego_to_floor(self, floor) method is a non-static or instance method that requires an instance of the class to be called because it operates on instance-specific data. The self parameter is used to access or modify the state of a specific elevator instance. In the method go_to_floor, the action performed (moving to a particular floor) pertains to an individual elevator instance. This method needs to know which specific elevator (instance) it is moving; hence, it's not static.

Here is how you might use these methods in practice:

# Creating an instance of an Elevator
elevator_instance = Elevator()

# Calling the non-static method on the instance
elevator_instance.go_to_floor(5) # Output: Elevator going to floor 5

# Calling the static method directly on the class
Elevator.emergency_alert() # Output: Emergency alert for all elevators!

Java static and non-static methods usage example:

public class Elevator {
public static void emergencyAlert() {
System.out.println("Emergency alert for all elevators!");
}

public void goToFloor(int floor) {
System.out.println("Elevator going to floor " + floor);
}
}

The emergencyAlert() method is marked with the static keyword, and emergencyAlert() is a static method. Static methods in Java are associated with the class rather than any instance of the class. This means they can be called without creating an instance of the class and are used for general operations in all instances. The method emergencyAlert prints a message "Emergency alert for all elevators!" which logically applies to all elevators, not just a specific one. It doesn't rely on any instance-specific data, which makes it suitable as a static method.

The goToFloor(int floor) method is a non-static or instance method, which means it operates on or affects the state of a specific class instance. This method requires an object of the class to be instantiated and called because it needs context particular to that instance (which elevator is going to which floor). In goToFloor, the action (moving an elevator to a particular floor) is specific to an individual elevator instance. The method uses the state of the instance it is called on, reflecting instance-specific behavior. 🙂

14. What is method overloading and overriding?

🎯: Method overloading allows defining multiple methods with the same name but different parameters. Method overriding means redefining an inherited method.

A simple real-world analogy: Method overloading and overriding are programming concepts illustrated using a multifunctional kitchen appliance. Method Overloading occurs when different tasks use the same method name but different parameters. For instance, an appliance’s “Start” button might blend when a blender jar is attached and grind with a grinder jar. This allows the same method, start(), to perform based on the attachment used. Method Overriding happens when a subclass changes how a method from its superclass works. For example, a professional blender might modify the default “Start” method to include extra functions like pulsing, enhancing the basic blending functionality. In short, overloading uses the same method name for different operations while overriding customizes an existing method for more specific or improved functionality.

These mechanisms enhance flexibility and reusability in object-oriented programming by allowing more specific behaviors in subclasses and accommodating different types of data inputs in methods.

Python example for overloading and overriding:

class Elevator:
def go_to_floor(self, floor, announce=None):
if announce is not None:
print(f"Please prepare to go to floor: {floor}")
print(f"Going to floor: {floor}")

class FreightElevator(Elevator): # Subclass that inherits from Elevator
def go_to_floor(self, floor):
# This overrides the method from the superclass Elevator
print(f"Freight elevator going to floor: {floor}")

# Example Usage
if __name__ == "__main__":
# Instance of Elevator
elevator = Elevator()
elevator.go_to_floor(3) # Simple call, no announcement
elevator.go_to_floor(4, announce=True) # Call with an announcement

# Instance of FreightElevator
freight_elevator = FreightElevator()
freight_elevator.go_to_floor(2) # This will use the overridden method

It is called method overloading in Python: in the Elevator class, go_to_floor is designed to handle different behaviors based on whether an announce parameter is provided. This is Python's way of implementing functionality similar to overloading, where a method can behave differently based on its input arguments. Here, it either prints an additional announcement or skips it based on the presence of the announce argument. It is called method overriding in Python: the FreightElevator class, a subclass of Elevator, overrides the go_to_floor method. It provides a specific implementation indicating a freight elevator moving to a floor. This overriding changes the method's behavior when called on an instance of FreightElevator.

Java example for overloading and overriding:

// Base class
class Elevator {
// Method to go to a floor
void goToFloor(int floor) {
System.out.println("Going to floor: " + floor);
}

// Overloaded method to include an announcement
void goToFloor(int floor, boolean announce) {
if (announce) {
System.out.println("Please prepare to go to floor: " + floor);
}
goToFloor(floor); // Calls the original method
}
}

// Subclass that inherits from Elevator
class FreightElevator extends Elevator {
// Overriding the goToFloor method
@Override
void goToFloor(int floor) {
System.out.println("Freight elevator going to floor: " + floor);
}
}

// Main class to run the example
public class Main {
public static void main(String[] args) {
// Instance of Elevator
Elevator elevator = new Elevator();
elevator.goToFloor(3); // Simple call, no announcement
elevator.goToFloor(4, true); // Call with an announcement

// Instance of FreightElevator
FreightElevator freightElevator = new FreightElevator();
freightElevator.goToFloor(2); // This will use the overridden method
}
}

It is called method overloading in Java: the Elevator class has two methods named goToFloor. The first takes a single int parameter (the floor number), and the second takes an int and a boolean (to indicate whether an announcement is needed). This approach handles the absence of optional parameters in Java by providing two separate methods that perform similar functions but with different signatures. It is called method overriding in Java: the FreightElevator class overrides the goToFloor(int floor) method of the Elevator class to provide a custom message indicating that it is specifically a freight elevator going to a specific floor.

15. What access modifiers exist in your primary programming language?

🎯: In Java, there are public, private, protected, and package-private. In Python, attributes and methods are public by default, and private attributes are often indicated by underscores (_ or __).

We discussed all of this in detail in question two. But now we can explain in more detail the differences between all these access modifiers in Python and Java. 🤓

Java Access Modifiers:

Public:

  • What it means: Methods and attributes declared public can be accessed from any other class in the application or any other application.
  • Example: If a car’s method to start the engine is public, anyone can start the engine from anywhere in the code.

Private:

  • What it means: Methods and attributes declared private can only be accessed within their declared class. No outside access is allowed.
  • Example: If a car’s internal mechanism to process fuel is private, you can't access or modify this process from outside the car's class.

Protected:

  • What it means: Methods and attributes declared protected can be accessed within the same class, by subclasses, or within the same package.
  • Example: If a vehicle’s feature is protected, it can be accessed by any vehicle type, such as a car or a motorcycle, that extends the vehicle class.

Package-Private (default, no modifier needed):

  • What it means: If no access modifier is specified, it defaults to package-private, meaning methods and attributes can be accessed within the same package but not outside.
  • Example: If you set a vehicle’s specification, such as its size, to package-private, other classes within the same package (like a garage package) can access and use the size, but those outside this package cannot.

Python Access Modifiers:

Note: Python does not have built-in keywords like Java for public, private, or protected access modifiers. Instead, it uses naming conventions to signify the intent of accessibility.

Public (Default):

  • What it means: All Python attributes and methods are public by default. They can be accessed from anywhere within the application.
  • Example: A method in a Python class to get the car’s color can be accessed from anywhere other classes or files after importing the class.

Private (Prefix with double underscores __):

  • What it means: By convention, prefixing an attribute or method with double underscores indicates that it should be treated as private. Python enforces this by name mangling, which makes it more complicated (but possible) to access from outside its class.
  • Example: If you have a method __updateSoftware() in a car's class, it's meant to be accessed only within the class itself.

Protected (Prefix with a single underscore _):

  • What it means: By convention, prefixing an attribute or method with a single underscore indicates that it is protected. It should be accessed only within the class and its subclasses but can technically still be accessed publicly.
  • Example: If you have an attribute _mileage in a vehicle class, it suggests that this attribute should be accessed only within subclasses like cars or motorcycles.

16. Explain what constructors and destructors are.

🎯: Constructors initialize new objects, while destructors release resources before an object is destroyed.

Python:

class Elevator:
def __init__(self): # Constructor
print("Elevator created.")

def __del__(self): # Destructor
print("Elevator is being destroyed.")

Constructor (__init__): This method is automatically called when a new class instance is created. Its primary purpose is to initialize the newly created object. Destructor (__del__): This method is used when an instance is about to be destroyed, which happens when there are no more references to the object, meaning the garbage collector will reclaim its memory. Destructors are less commonly used in Python because Python has a garbage collector that handles memory management automatically. However, you might need to ensure some cleanup activities (like closing files or database connections), and you might define a destructor.

Java:

public class Elevator {
public Elevator() { // Constructor
System.out.println("Elevator created.");
}

protected void finalize() throws Throwable { // Destructor
System.out.println("Elevator object is being garbage collected.");
}
}

In Java, constructors have the same name as the class and may include parameters (this example has none). The constructor is invoked when a new object instance is created using the new keyword. Destructor Equivalent (finalize): Java does not have destructors in the way C++ or Python does because Java handles memory deallocation automatically through its garbage collector. The finalize() method in Java can be considered a destructor equivalent, but it's important to note that its execution is neither guaranteed nor predictable. The garbage collector calls finalize() when it determines no more references to the object.

In both languages, constructors are used to set up an object's initial state, such as allocating resources or initializing instance variables. Destructors in Python and the finalize() method in Java serve to clean up resources before an object is destroyed. However, their use differs due to the languages' distinct memory management systems. Python's __del__ may be called more predictably when an object is about to be destroyed, while Java's finalize() is less reliable and generally not recommended for critical resource cleanup. 👌

17. How can you manage memory in OOP?

🎯: Programmers can handle Memory management explicitly or through automatic mechanisms like garbage collection.

Python uses a form of automatic garbage collection known as reference counting. Each object in Python has a reference count that tracks how many references point to it. When this count reaches zero, the object is automatically removed from memory. Python supplements reference counting with a generational garbage collection system that helps clean up reference cycles — situations where objects reference each other but are no longer used by the program.

Java: Java uses a garbage collector run by the Java Virtual Machine (JVM), which automatically handles memory management, ers from the need to manage memory manually. The JVM uses a generational garbage collection approach, which assumes that most objects are short-lived. Younger (or newer) objects are checked more frequently than older ones.

C++ gives developers direct control over memory allocation and deallocation through operators like new and delete. Unlike Python and Java, C++ requires developers to manually manage object lifetime, which can lead to issues like memory leaks and dangling pointers if not handled carefully. Best Practices: Use smart pointers (std::unique_ptr, std::shared_ptr), which the C++ Standard Library provides to manage memory automatically. These pointers automatically deallocate memory when it's no longer used, preventing common memory management errors. Utilize RAII where object lifecycles are tied to resource management, ensuring that resources are correctly released when objects go out of scope. When possible, avoid using raw pointers for memory management. 💡 Instead, use smart pointers or other resource-managing constructs.

Note: while Python and Java abstract much of the memory management complexity away from developers through garbage collection, C++ requires explicit management, offering more control and greater responsibility. Understanding and utilizing each language's tools and models can significantly enhance application performance and stability.

18. What is UML, and how is it used in OOP?

🎯: UML stands for Unified Modeling Language and is used to visualize software designs through diagrams.

For example, the code at the beginning, which represented a building with floors and elevators, we can now represent as PlantUML code:

@startuml
class Building {
-floors : int
-elevators : list
+call_elevator(floor : int, direction : String) : void
}

class Elevator {
-current_floor : int
-state : String
-max_capacity : int
-occupancy : int
+move_up() : void
+move_down() : void
+open_door() : void
+close_door() : void
+go_to_floor(target_floor : int) : void
}

Building "1" *-- "many" Elevator : contains
@enduml

This will give us the following UML diagram:

The image displays a UML class diagram featuring two classes, Building and Elevator, illustrating their attributes and methods. A relationship indicates that a Building contains many Elevators.

19. Give an example of using inheritance and polymorphism in an actual project.

⚠️: This question is particular. It implies that you already have programming experience, which you can demonstrate. I advise preparing for it in advance, as it will give you a massive advantage over your competitors.

I am a Computer Scientist with a science degree in Computer Science, which I obtained at the University of Applied Sciences Berlin (how many times did you count the word “science”? 😅). I’ve been intensely into health informatics and have my own real-world projects. I will show you one of them now using my Python code. But if you’re interested in any of my other projects, then check them out on my GitHub portfolio:

This Python code provides a framework for handling genetic sequencing data. 🧬 It includes classes for managing sequences, aligning short reads to reference sequences, correcting potential errors in the reads, and outputting the results in a format suitable for further biological analysis. This functionality is crucial in bioinformatics for tasks such as genome assembly, variant calling, and more (but don’t panic! 🙂):

class Sequence:
def __init__(self, lines):
self.name = lines[0].strip()[1:]
self.bases = "".join([x.strip() for x in lines[1:]]).upper()

def __str__(self):
return self.name + ": " + self.bases[:20] + "..."

def __repr__(self):
return self.__str__()


class Read(Sequence):
def get_seed(self, seedlength):
return self.bases[:seedlength]

def replace_kmers(self, replacements):
for kmer, replacement in replacements.items():
self.bases = self.bases.replace(kmer, replacement)


class Reference(Sequence):
def __init__(self, lines):
self.kmers = None
super().__init__(lines)

def calculate_kmers(self, kmersize):
self.kmers = {}
for pos in range(0, len(self.bases) - kmersize + 1):
kmer = self.bases[pos:(pos + kmersize)]
if kmer not in self.kmers:
self.kmers[kmer] = []
self.kmers[kmer] += [pos]

def get_kmer_positions(self, kmer):
if self.kmers is None or len(next(iter(self.kmers))) != len(kmer):
self.calculate_kmers(len(kmer))
if kmer not in self.kmers:
return []
return self.kmers[kmer]

def count_mismatches(self, read, position):
mismatches = 0
for pos in range(position, position + len(read.bases)):
if pos >= len(self.bases):
break
if read.bases[pos - position] != self.bases[pos]:
mismatches += 1
# Count every base of the read that goes out past the end of the reference as a mismatch
mismatches += position + len(read.bases) - pos - 1
return mismatches


class Mapping:
def __init__(self, reference):
self.reference = reference
self.reads = {}

def add_read(self, read, position):
if position not in self.reads:
self.reads[position] = []
self.reads[position] += [read]

def get_reads_at_position(self, position):
if position not in self.reads:
return []
return self.reads[position]

def __str__(self):
res = ["Mapping to " + self.reference.name]
for pos in self.reads:
res += [" " + str(len(self.reads[pos])) + " reads mapping at " + str(pos)]
return "\n".join(res)


class SAMWriter:
def __init__(self, mapping):
self.mapping = mapping

def write_mapping(self, filename):
myfile = open(filename, "w")
refname = self.mapping.reference.name.split(" ")[0]
myfile.write("@SQ\tSN:" + refname + "\tLN:" + str(len(self.mapping.reference.bases)) + "\n")
for pos in range(0, len(self.mapping.reference.bases)):
for read in self.mapping.get_reads_at_position(pos):
myfile.write("\t".join([read.name, "0", refname, str(pos + 1), "255",
str(len(read.bases)) + "M", "*", "0", "0", read.bases, "*"]))
myfile.write("\n")
myfile.close()


class ReadPolisher:
def __init__(self, kmerlen):
self.kmer_length = kmerlen
self.spectrum = {}

def add_read(self, readseq):
kmers = []
k = self.kmer_length
for i in range(len(readseq) - k + 1):
kmer = readseq[i:i + k]
kmers.append(kmer)
self.create_spectrum(kmers)

def create_spectrum(self, kmers):
for kmer in kmers:
if kmer in self.spectrum:
self.spectrum[kmer] += 1
else:
self.spectrum[kmer] = 1

def get_replacements(self, minfreq):
corrections = {}
for kmer, count in self.spectrum.items():
if count < minfreq:
candidate_kmers = []
for i, base in enumerate(kmer):
for new_base in ['A', 'G', 'T', 'C']:
candidate_kmer = kmer[:i] + new_base + kmer[i + 1:]
if candidate_kmer in self.spectrum and self.spectrum[candidate_kmer] >= minfreq:
candidate_kmers.append(candidate_kmer)
if candidate_kmers:
most_frequent_candidate = max(candidate_kmers, key=lambda x: self.spectrum[x])
corrections[kmer] = most_frequent_candidate
return corrections


def read_fasta(fastafile, klassname):
klass = globals()[klassname]
f = open(fastafile, "r")
readlines = []
reads = []
for line in f:
if line[0] == '>' and len(readlines) != 0:
reads += [klass(readlines)]
readlines = []
readlines += [line]
reads += [klass(readlines)]
f.close()
return reads


def map_reads(reads, reference, kmersize, max_mismatches):
mapping = Mapping(reference)
reference.calculate_kmers(kmersize)
for read in reads:
seed = read.get_seed(kmersize)
seed_positions = reference.get_kmer_positions(seed)
for position in seed_positions:
mismatches = reference.count_mismatches(read, position)
if mismatches < max_mismatches:
mapping.add_read(read, position)
return mapping


def main():
# ---------- Tablet-Aufgabe ---------
# Mappen wir nun die Datei (z.B. data/fluA_reads.fasta auf data/fluA_reads.fasta )
# und speichern wir das Ergebnis als z.B. fluA_mapping.sam.
# reads = read_fasta("data/fluA_reads.fasta", Read.__name__)
# reference = read_fasta("data/fluA.fasta", Reference.__name__)[0]
# mapping = map_reads(reads, reference, 8, 5)
# print("Mapping reads: " + len(mapping.reads))
# writer = SAMWriter(mapping)
# writer.write_mapping("data/fluA_mapping.sam")

# ---------- Antibiotika-Resistenzen ---------
# Mappen wir die Read-Sequenzen der 4 Personen (1 bis 4):
# reads = read_fasta("data/patient4.fasta", Read.__name__)
# reference = read_fasta("data/rpoB.fasta", Reference.__name__)[0]
# mapping = map_reads(reads, reference, 11, 5)
# writer = SAMWriter(mapping)
# writer.write_mapping("data/patient4.sam")

# ---------- Anwendung ---------
# Verwenden wir nun unsere ReadPolisher:
reads = read_fasta("data/patient4.fasta", Read.__name__)
reference = read_fasta("data/rpoB.fasta", Reference.__name__)[0]
polisher = ReadPolisher(15)
for read in reads:
polisher.add_read(read.bases)
replacements = polisher.get_replacements(3)
nrep = 0
for read in reads:
nrep += 1
if nrep % 1000 == 0:
print(str(nrep) + "/" + str(len(reads)))
read.replace_kmers(replacements)
mapping = map_reads(reads, reference, 15, 3)
writer = SAMWriter(mapping)
writer.write_mapping("data/mapping_p4_corrected.sam")


if __name__ == "__main__":
main()

It looks big, I know. 😅 This code is like a sophisticated text-editing tool but for DNA. It helps organize small snippets of DNA, finding where they belong in a larger context and ensuring accuracy. This process is vital for scientific research and medical diagnostics, helping us understand and potentially cure diseases at a genetic level. The main goal of this code is to process DNA sequences to find where small DNA fragments (reads) match a more extended DNA sequence (reference). The final result is a detailed report showing where each fragment matches within the more extensive sequence.

Steps in the Code:

  1. Reading DNA Data: The code starts by reading DNA sequences from files. These files contain small DNA fragments (reads) and the extended reference sequence. Each sequence comes with a name and the actual sequence of DNA letters (A, C, G, T).
  2. Storing DNA Information: Once read, these sequences are stored in a structured way using classes:
  • Read: Stores the small DNA fragments and can perform actions like getting a small portion of the fragment or replacing certain sections based on a set of rules (like fixing errors).
  • Reference: Stores the long reference sequence. It can identify and store smaller sections of itself, which helps locate where the small fragments might fit.

3. Analyzing DNA Matches:

  • The code compares the small DNA fragments to the reference sequence to determine where they match the best. It considers how similar the fragment is to a section of the reference and records the position if they match well.
  • It also calculates and notes any mismatches (differences) between the fragment and the reference at each possible matching position.

4. Correcting Errors: Before finalizing where each fragment fits, the code tries to correct any potential errors in the DNA fragments. This ensures that the matches it finds are as accurate as possible.

5. Generating a Report: The final step is to generate a detailed report in a standard format known as SAM. This format is widely used in bioinformatics and contains:

  • Information about each DNA fragment.
  • Where it matches in the reference sequence.
  • The quality of each match (how well it fits).

Final result:

The final result is a SAM file with a comprehensive map 🗺️ showing where each DNA fragment fits into the more extended reference DNA sequence. This file includes details like the exact position of the match, the sequence of the DNA fragment, and other technical information helpful for further analysis. This code was, for example, written to identify resistance to certain types of antibiotics in 4 different patients to give them a more appropriate prescription of a different antibiotic that did not cause such problems. This file will be used in a particular program for bioinformaticians called Tablet:

At the top, you can see a schematic view of the entire reference with the mapped reads. Below, you can see a detailed view: first, the sequence is translated into amino acids, then the nucleotide sequence of the reference, and then the individual reads.

Let me also demonstrate the UML of this code for clarity and then tell you how the principles of inheritance and polymorphism work in it:

This diagram shows how different classes work together to analyze DNA sequences.

The Sequence class serves as a basic model for DNA sequences. The Read and Reference classes specialize the Sequence class for different purposes: Read for analyzing small DNA fragments and Reference for managing a complete reference sequence. The Mapping class organizes where each DNA fragment fits within the more extensive reference sequence, and the SAMWriter outputs this information into a usable file format. The ReadPolisher helps improve the accuracy of the DNA reads by identifying and suggesting corrections for rare sequences. 🙌

Inheritance in the Code (allows a class to inherit attributes and methods from another class): The Read and Reference classes inherit from the Sequence class. This means Read and Reference will have access to the Sequence class's methods and attributes, such as __init__, __str__, and __repr__, along with any new methods they define or existing methods they override.

Polymorphism in the Code (allows methods to be implemented in different ways across different classes): The __init__ method of the Reference class overrides the __init__ method of the Sequence class by adding its functionality before calling the Sequence's __init__ method using super().__init__(lines) (The Reference class calculates and stores kmers to index all possible kmers within the reference sequence and their positions. This index allows for rapid searching and matching of read sequences against the reference.). This is a classic example of polymorphism where the __init__ method behaves differently in Reference than in Sequence.

20. How do you implement exception handling in OOP?

🎯: Exceptions are handled through particular constructs like try, catch, or except, and finally blocks in Java or Python.

Exception handling is a fundamental concept in object-oriented programming (OOP) that deals with unexpected conditions or errors during a program's execution. It handles runtime errors so the program can be maintained even when an unusual situation arises. It makes your program more robust by catching exceptions that could otherwise lead to a crash. 💥 By handling these errors gracefully, you ensure that your program can continue to run or terminate cleanly. ✅ Exception handling helps track and debug errors. The system can log detailed information about the application's state when the exception occurred. It improves user experience by providing meaningful error messages instead of cryptic system errors. It also ensures that resources like files, databases, or network connections are closed or appropriately released in case of errors, preventing resource leaks.

Python:

class Elevator:
def go_to_floor(self, floor):
try:
if floor < 0:
raise ValueError("Floor must be non-negative")
print(f"Going to floor {floor}")
except ValueError as e:
print(f"Error: {str(e)}")

This go_to_floor method checks if the requested floor number is non-negative. If the floor is negative, it raises a ValueError with a message stating, “Floor must be non-negative.”. If a ValueError is raised (due to a negative floor number), the method catches this error in the except block and prints an error message.

Java:

public class Elevator {
public void goToFloor(int floor) {
try {
if (floor < 0) {
throw new IllegalArgumentException("Floor must be non-negative");
}
System.out.println("Going to floor: " + floor);
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
}

Similar to the Python code, this Java method checks if the floor number is non-negative. If not, it throws an IllegalArgumentException with the message “Floor must be non-negative.”. If an IllegalArgumentException is thrown, the method catches it in the catch block and prints the error message included with the exception. ⚠️

Note: The principle is the same in Python and Java: attempt an operation, watch for a specific error, and react accordingly to either resolve the issue or inform the user. This proactive approach to error management is crucial in user-friendly software.

How Exception Handling Works in OOP:

  • Try Block: You wrap the code that might throw an exception in a try block.
  • Catch Block: Following the try block, one or more catch blocks handle specific exceptions, allowing the programmer to process or log errors as needed.
  • Finally Block (in some languages like Java): A finally block can be used to execute code regardless of whether an exception was thrown or caught. It is often used for clean-up activities.

21. Explain what virtual functions are.

🎯: Virtual functions are methods in a base class that can be overridden in derived classes to enable polymorphism.

In Python, methods are virtual by default, but here is how you might explicitly define behavior that intends to be overridden.

class Elevator:
def go_to_floor(self, floor):
raise NotImplementedError("This method should be overridden by subclasses.")

class PassengerElevator(Elevator):
def go_to_floor(self, floor):
print(f"Passenger elevator going to floor {floor}")

The method go_to_floor(self, floor) in the Elevator class is intended to be a placeholder within the base class. It’s implemented to raise a NotImplementedError, which forces any subclass to override this method with its implementation. This ensures that any subclass of Elevator must provide its specific behavior for going to a floor. The go_to_floor(self, floor) method in the PassengerElevator class offers the actual implementation for the go_to_floor method for a passenger elevator. When called, it sends a message indicating that the passenger elevator is going to the specified floor. 🥳

In Java, the equivalent of virtual functions are methods that are marked with ‘abstract’ or those that can be overridden:

public abstract class Elevator {
public abstract void goToFloor(int floor); // This is an abstract method similar to a virtual function.
}

public class PassengerElevator extends Elevator {
@Override
public void goToFloor(int floor) {
System.out.println("Passenger elevator going to floor " + floor);
}
}

22. What is late binding?

🎯: Late binding is a concept where the method to be called is determined at runtime, typically used to implement polymorphism.

Why is Late Binding Needed?

  1. Flexibility: Late binding allows developers to write more flexible and reusable code. Functions that operate on base class references can work with objects of derived classes without modification.
  2. Extensibility: It enables developers to extend a program’s functionalities without altering the existing codebase significantly. New classes can be added that behave differently from existing classes.
  3. Maintainability: It makes the codebase easier to maintain and understand. Changes in the behavior of derived classes do not affect the parts of the code that use references to the base class.

Example in Python:

class Animal:
def speak(self):
return "Some sound"

class Dog(Animal):
def speak(self):
return "Bark"

class Cat(Animal):
def speak(self):
return "Meow"

# Late Binding in action
animals = [Dog(), Cat()]
for animal in animals:
print(animal.speak()) # Outputs 'Bark' for Dog and 'Meow' for Cat

In Python, all method calls are late-bound by default. Python determines the method to invoke at runtime, allowing it to support very dynamic behavior. Python uses a dynamic dispatch mechanism. When a method is called on an object, Python looks up the method in the object’s class and its superclasses at runtime to determine the correct execution method.

Example in Java:

class Animal {
void speak() {
System.out.println("Some sound");
}
}

class Dog extends Animal {
void speak() {
System.out.println("Bark");
}
}

class Cat extends Animal {
void speak() {
System.out.println("Meow");
}
}

public class Main {
public static void main(String[] args) {
Animal[] animals = new Animal[]{new Dog(), new Cat()};
for (Animal animal: animals) {
animal.speak(); // Outputs 'Bark' for Dog and 'Meow' for Cat
}
}
}

Like Python, Java also uses dynamic method dispatch for instance methods, a form of late binding. The specific implementation of a method that gets called is determined at runtime. In Java, all instance methods are virtual by default unless declared as final. The final keyword prevents a method from being overridden in subclasses, thus turning off late binding for that method.

In Python and Java, late binding allows a program to dynamically decide which method to call on an object at runtime, supporting polymorphism and inheritance principles. This capability is crucial for building flexible and extendable software systems where the behavior of derived classes can be customized independently of the base class code. 🙂

23. How is multiple inheritance implemented in your primary programming language?

🎯: In Python, multiple inheritance is directly supported by specifying multiple base classes. Java does not support multiple inheritance directly but uses interfaces instead.

Python supports multiple inheritance directly:

class Alarm:
def trigger_alarm(self):
print("Alarm triggered!")

class Elevator:
def go_to_floor(self, floor):
print(f"Elevator going to floor {floor}")

class ServiceElevator(Elevator, Alarm):
pass

In Java, multiple inheritance of classes is not supported due to potential issues such as the “Diamond Problem”, where a class can inherit from various classes that have methods with the same signature. To manage this complexity and ambiguity, Java does not allow a class to inherit from more than one class. However, Java provides an alternative to achieve similar functionality using interfaces:

interface Swimmer {
void swim();
}

interface Runner {
void run();
}

// Implementing multiple interfaces
class Athlete implements Swimmer, Runner {
@Override
public void swim() {
System.out.println("Swimming");
}

@Override
public void run() {
System.out.println("Running");
}
}

public class Main {
public static void main(String[] args) {
Athlete athlete = new Athlete();
athlete.swim(); // Outputs: Swimming
athlete.run(); // Outputs: Running
}
}

Java allows a class to implement multiple interfaces. Interfaces can declare methods but cannot provide implementations (until Java 8 introduced default methods). This allows Java to support a form of multiple inheritance by enabling a class to inherit multiple sets of method signatures from different interfaces. 🤓

Here’s how you can achieve multiple inheritance-like behavior using interfaces in Java:

  1. Define Multiple Interfaces: Create multiple interfaces, each declaring one or more methods.
  2. Implement Multiple Interfaces: A single class can implement multiple interfaces, thereby inheriting the method declarations from those interfaces.
  3. Provide Implementations: The implementing class must provide concrete implementations for all the methods declared in the interfaces.

24. Can you give an example of a Singleton design pattern?

🎯: The Singleton design pattern is a software design pattern that ensures a class has only one instance while providing a global access point to this instance. It is commonly used when a single object is needed to coordinate actions across a system.

Note: Design patterns are essential for solving common design issues in software engineering. They provide a tested and proven solution to these issues, which can help to make the system more robust and maintainable. Along with other design patterns, Singleton plays a crucial role in software development, especially when designing complex systems that ensure consistency, efficiency, and operation flexibility.

Python Example: Singleton Pattern in Logging

class Logger:
_instance = None

def __new__(cls):
if cls._instance is None:
cls._instance = super(Logger, cls).__new__(cls)
cls._instance.setup_logging()
return cls._instance

def setup_logging(self):
self.log_file = open("app.log", "a")

def log(self, message):
self.log_file.write(message + '\n')
self.log_file.flush()

# Usage
logger1 = Logger()
logger2 = Logger()
assert logger1 is logger2 # Both references point to the same instance
logger1.log("This is a log message.")

Class Variable:

  • _instance: A class-level variable that stores the single instance of the Logger class. Initially, it is set to None to indicate that the instance still needs to be created.

Special Method __new__:

  • This particular method in Python controls the creation of a new class instance. It’s a class method (not an instance method), which means it gets the class itself as the first argument instead of an instance of the class.
  • __new__ is responsible for creating and returning an instance of the class. We override this method to implement the Singleton pattern.
  • Inside __new__, we check if _instance is None. If it is, this means an instance of the Logger does not exist yet, so we create one by calling super(Logger, cls).__new__(cls), which delegates the call to the parent class's __new__ method, effectively creating a new instance of the Logger class.
  • Once the instance is created, it calls setup_logging to initialize the logging environment, which only happens the first time an instance is created.
  • Finally, _instance is returned, regardless of whether it was just created or was already available.

Method setup_logging:

  • This method sets up the logging environment by opening a log file in append mode. This ensures that every time a log is written, it gets appended to the end of the file, preserving previous log entries.
  • self.log_file = open("app.log", "a"): This opens app.log (or creates it if it doesn’t exist) and sets the file mode to append.

Method log:

  • This method takes a message string as an argument and writes it to the log file, followed by a newline character. It ensures that all log messages are written immediately in the file with self.log_file.flush(), essential for maintaining up-to-date logs, especially when debugging.

Usage Example:

  • Creating Instances:logger1 = Logger(): Attempts to create a new Logger instance.logger2 = Logger(): Attempts again to create a new Logger instance. Both attempts will return the same instance due to the Singleton implementation.
  • assert logger1 is logger2: This assertion checks that both logger1 and logger2 are indeed the same instance, demonstrating the Singleton behavior.
  • Logging a Message:logger1.log("This is a log message."): This line logs a message using the logger instance. Since logger1 and logger2 are the same instance, this message can be logged using either.

This code demonstrates how the Singleton pattern can be used in Python to ensure that only one logger instance is created, thereby centralizing the logging mechanism for an entire application. It avoids multiple file handles to the same log file and centralizes log management, crucial for maintaining clear and coherent application logs.

The Singleton pattern is frequently utilized in logging, configuration settings, and managing access to resources across a system due to several compelling reasons that align well with the needs of these specific areas. Here’s why:

1. Consistency

  • Singleton in Logging: When different parts of an application generate logs, it’s crucial to have a consistent format and behavior in how logs are recorded, whether written to a file, displayed on a console, or sent to a remote server. A Singleton logger ensures that all log messages are handled uniformly and that configurations (like log level and output format) are consistent throughout the application.
  • Singleton in Configuration Settings: Configuration settings must be consistent across the entire application. Using a Singleton to manage these settings ensures that any changes to the configuration are instantly reflected throughout the system without inconsistencies.

2. Controlled Access

  • Singleton in Resource Management: Managing resources like database connections or network sockets is critical as these resources are expensive to create and limited in number. A Singleton can manage pools of resources, ensuring that they are reused efficiently and not exceeded, which prevents resource leaks and potential system failures.

3. Shared Access

  • Global Point of Access: Singleton provides a globally accessible instance, making it easy to access from any part of the system without needing to recreate the object or pass it around. This is particularly useful for functionalities like logging or configurations where many application parts need to access the same functionality.

4. Memory and Performance Efficiency

  • Reducing Overhead: Instantiating a class multiple times, especially one that handles tasks like logging or configuration, can consume unnecessary system resources and complicate the management of its state. A Singleton ensures that there’s only one instance of the class, reducing memory usage and improving the application’s performance.

5. Synchronization

  • Thread Safety in Singletons: In multi-threaded applications, ensuring that only one Singleton class instance is created, even when multiple threads try stance simultaneously, is crucial. Singleton pattern can be implemented thread-safe, which ensures that the class behaves correctly even under concurrent access, avoiding potential bugs and issues in scenarios like logging where multiple threads might log messages simultaneously.

A simple real-world analogy: Think of a government having only one official president at any time. This president is responsible for making executive decisions for the entire country. Similarly, the Singleton pattern ensures that only one class instance controls actions in the application. 👩‍⚖️

Other Design Patterns

Design patterns are broadly categorized into Creational, Structural, and Behavioral. Each category serves a different purpose but aims to solve complex design issues, ensuring code is flexible and reusable. By applying these design patterns, developers can solve common software design issues more efficiently and elegantly, analogous to solving everyday problems with well-known and tested solutions.

Creational Patterns

  • Factory Method: This method provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created.
  • Abstract Factory: Offers an interface for creating families of related or dependent objects without specifying their concrete classes.
  • Builder: Constructs complex objects step by step. The pattern allows the production of different types and representations of an object using the same construction code.
  • Prototype: Allows copying existing objects without making your code dependent on their classes.

Structural Patterns

  • Adapter: Allows objects with incompatible interfaces to collaborate.
  • Decorator: This method lets you attach new behaviors to objects by placing them inside special wrapper objects containing them.
  • Facade: Provides a simplified interface to a library, a framework, or any other complex set of classes.
  • Proxy: Provides a placeholder for another object to control access to it.

Behavioral Patterns

  • Observer: Allows one object to notify other objects about changes in its state.
  • Strategy: This lets you define a family of algorithms, put each into a separate class, and make their objects interchangeable.
  • Command: Turns a request into a stand-alone object containing all the request’s information. This transformation allows parameterizing objects with operations.
  • State: Let an object alter its behavior when its internal state changes. It appears as if the object changed its class.
source — https://McDonaldLand.wordpress.com

25. Explain the difference between association, aggregation, and composition. What type of dependence occurs, and what examples when one is better than the other? What is DI (dependency injection)?

🎯: These concepts describe relationships between classes in object-oriented programming, focusing on how objects associate, aggregate, or compose other objects.

  1. Association: Think of association as a casual friendship. In programming, it refers to a relationship where one object “knows” another object but isn’t necessarily closely dependent on each other. This can be a one-to-many relationship (like a teacher and their students), where the teacher needs to know their students to teach, but the teacher’s existence doesn’t depend on the student’s existence, and vice versa.
  • Dependency Type: Weak dependency. Objects are aware of each other but can operate independently.
  • Object Lifetime: Independent. The destruction of one object does not affect the existence of the other.
  • When Better: When objects need to interact, but their lifespans are not linked. For example, a Customer object might be associated with a Service Ticket object in customer service software, but customers exist independently of specific service tickets.

Python example of the association:

class Doctor:
def __init__(self, name):
self.name = name

class Patient:
def __init__(self, name, doctor):
self.name = name
self.doctor = doctor # Patient is aware of Doctor but they are independent

# Example usage
doc = Doctor("Dr. Smith")
pat = Patient("John Doe", doc)

Java example of the association:

class Teacher {
private String name;

public Teacher(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

class Student {
private String name;
private Teacher teacher; // Student knows the teacher but can exist independently

public Student(String name, Teacher teacher) {
this.name = name;
this.teacher = teacher;
}

public String getName() {
return name;
}
}

// Example usage
Teacher t = new Teacher("Mrs. Johnson");
Student s = new Student("Alice", t);

This type of relationship is when two objects are aware of each other.

2. Aggregation is a particular form of association that represents a “has-a” relationship but with less strict dependence. It’s like a team leader and a team member. The team will still exist if a member leaves, and the member doesn’t cease to exist if they leave the team. Objects can exist independently.

  • Dependency Type: Weak to medium dependency. Represents a whole/part relationship, but parts can exist independently of the whole.
  • Object Lifetime: Child objects can outlive the parent object.
  • When Better: You need a more structured relationship but still require flexibility. For instance, a university has departments that can exist even if the university is restructured or closed.

Python example of the aggregation:

class Team:
def __init__(self, name):
self.name = name
self.players = []

def add_player(self, player):
self.players.append(player)

class Player:
def __init__(self, name):
self.name = name

# Example usage
team = Team("Sharks")
player1 = Player("Bob")
team.add_player(player1)

Java example of the aggregation:

import java.util.ArrayList;
import java.util.List;

class Library {
private List<Book> books;

public Library() {
this.books = new ArrayList<>();
}

public void addBook(Book book) {
books.add(book);
}
}

class Book {
private String title;

public Book(String title) {
this.title = title;
}
}

// Example usage
Library library = new Library();
Book book = new Book("1984");
library.addBook(book);

In aggregation, one object contains or owns objects of another class, but these owned objects can exist independently.

3. Composition is a stronger form of aggregation. Think of it as a very tight “part-of” relationship containing an object’s life, which depends on the life of the container object. It’s like the relationship between a house and its rooms. If the house is destroyed, the rooms are too.

  • Dependency Type: Strong dependency. The child object’s lifecycle is tightly coupled to the parent object.
  • Object Lifetime: Dependent. If the parent object is destroyed, the child objects are also destroyed.
  • When Better: In scenarios where parts cannot functionally exist without the whole. An example is a payment system, where a Payment object might contain PaymentDetails that are meaningless outside of their associated Payment.

Python example of the composition:

class House:
def __init__(self):
self.rooms = [Room("Kitchen"), Room("Living Room")]

class Room:
def __init__(self, name):
self.name = name

# Example usage
house = House() # Rooms are created and destroyed with the House

Java example of the composition:

class Computer {
private List<Component> components;

public Computer() {
components = new ArrayList<>();
components.add(new Component("CPU"));
components.add(new Component("GPU"));
}

class Component {
private String name;

Component(String name) {
this.name = name;
}
}
}

// Example usage
Computer computer = new Computer(); // Components exist as long as the computer exists

In composition, the contained object’s lifecycle depends on the lifecycle of the container object.

Composition vs Aggregation:

🎯: Composition means a class contains other classes and manages their lifecycle. Aggregation is similar to composition but without lifecycle control.

Python examples of composition and aggregation:

class Building:
def __init__(self):
self.elevator = Elevator() # Composition

class ControlPanel:
def __init__(self, elevator):
self.elevator = elevator # Aggregation

In the Building class, an Elevator object is created inside the Building's constructor (__init__) with self.elevator = Elevator(). This means every Building object contains and directly manages its Elevator object. When a Building object is created, an Elevator object is also automatically created. When the Building object is no longer in use (e.g., if it's destroyed), the Elevator object it contains should also cease to exist. This shows the ownership and lifecycle dependency, which are characteristic of composition.

In the ControlPanel class, an Elevator object is passed to the constructor (__init__). Here, ControlPanel does not create the Elevator object but instead receives a reference to an existing Elevator object (self.elevator = elevator). This indicates that the ControlPanel might control or interact with an Elevator but doesn’t own it. The Elevator object could be part of multiple ControlPanels or continue to exist even if a ControlPanel is destroyed. This setup, where ControlPanel uses but does not own the Elevator, is a classic example of aggregation.

Java examples of composition and aggregation:

// Composition
public class Building {
private Elevator elevator;

public Building() {
this.elevator = new Elevator(); // Elevator is part of Building
}
}

// Aggregation
public class ControlPanel {
private Elevator elevator;

public ControlPanel(Elevator elevator) {
this.elevator = elevator; // Elevator is provided from outside
}
}

Principles of operation are fundamental in Java and Python for designing robust and maintainable object-oriented systems. They help clearly define relationships and dependencies between objects, which is crucial for managing complex software applications.

Dependency Injection (DI) is a design pattern used to manage dependencies between objects. DI allows objects to receive their dependencies from an external source rather than creating them internally. This can significantly decouple the classes and make the system easier to manage, test, and maintain. 🦾

When to Prefer Each Type Considering DI:

  • Association and DI: When you have many interchangeable parts or interactions, DI helps by managing these interactions without complex dependencies. This is great in systems like plugins where modules interact but are not dependent on each other’s lifecycles.
  • Aggregation and DI: Useful in applications where parts need to be replaced or managed dynamically. For example, a graphic design program might use DI to manage different shapes or tools that can be added to a canvas.
  • Composition and DI: Best for scenarios requiring strong consistency and reliability. For instance, in an application handling financial transactions, DI can ensure that all components of a transaction are properly initialized and managed, maintaining data integrity and security.

26. How do you test an OOP project? What are TDD and Moq? How do you create code to make it unit-testable? What is the difference between integration and unit test?

Test Driven Development (TDD) is a software development approach in which you write tests before you write the actual code.

The basic steps are:

  1. Write a Test: Start by writing a test that defines a function or improvements of a function.
  2. Run the Test: The test should fail since the functionality isn’t implemented yet.
  3. Write the Code: Write the minimal code required to pass the test.
  4. Run Tests: Rerun all tests to ensure the new test passes along with all existing tests.
  5. Refactor: Clean up the code, ensuring it adheres to good coding practices.
  6. Repeat: Repeat this process for every new feature or improvement.

Example of the TDD in Python:

# Step 1: write a test
import unittest
from unittest.mock import MagicMock

class Calculator:
def add(self, a, b):
pass # Implementation will be added later

class TestCalculator(unittest.TestCase):
def test_add(self):
calculator = Calculator()
calculator.add = MagicMock(return_value=3)
self.assertEqual(calculator.add(1, 2), 3)

if __name__ == '__main__':
unittest.main()

# Step 2: run the test
# Running this test now should fail since add does nothing.

# Step 3: write the code
class Calculator:
def add (self, a, b):
return a + b

# Step 4: run the test again
# Rerunning the tests, they should now pass because the implementation matches the expectation.

# Step 5: refactor
# Our implementation is already clean in this case, but this step would involve cleaning up any complexity or unnecessary parts.

Example of the TDD in Java:

// Step 1: write a test
import org.junit.Assert;
import org.junit.Test;

public class Calculator {
public int add(int a, int b) {
return 0; // Implementation will be added later
}
}

public class CalculatorTest {
@Test
public void testAdd() {
Calculator calculator = new Calculator();
Assert.assertEquals(3, calculator.add(1, 2));
}
}

// Step 2: run the test
// This test will initially fail because the add method returns 0.

// Step 3: write the code
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}

// Step 4: run test again
// The test should now pass, as the implementation correctly handles the addition.

// Step 5: refactor
// Any necessary refactoring would be done here.

Moq is a popular mocking framework for .NET used to create mock objects in unit testing. A mock object is a simulated object that mimics the behavior of real objects in controlled ways.

Moq is particularly useful in TDD because it allows developers to:

  • Create mock implementations for interfaces or classes without actually implementing them.
  • Set expectations on the mock object, specifying how it should be used.
  • Verify that the object was used as expected.

Example Python Code (with Mock):

Python has a built-in library called unittest.mock provides a powerful way to create mock objects during testing. Let's create an example where we mock a simple database access class.

import unittest
from unittest.mock import MagicMock

class Database:
def connect(self):
pass # Imagine this connects to a real database

def get_user(self, user_id):
pass # Returns user data from the database

class UserService:
def __init__(self, database):
self.database = database

def user_exists(self, user_id):
self.database.connect()
return self.database.get_user(user_id) is not None

class TestUserService(unittest.TestCase):
def test_user_exists(self):
# Create a mock object for the Database class
mock_db = MagicMock()
mock_db.get_user.return_value = {'id': '123', 'name': 'John'}

user_service = UserService(mock_db)
self.assertTrue(user_service.user_exists('123'))

# Check that connect and get_user were called
mock_db.connect.assert_called_once()
mock_db.get_user.assert_called_once_with('123')

if __name__ == '__main__':
unittest.main()

MagicMock 🪄 creates a mock instance of the Database class in this example. We can specify return values and check if certain methods were called, which makes it very useful for testing the logic in UserService without needing to access a real database.

Example Java Code (using Mockito):

For Java, one popular mocking library is Mockito. It’s not part of the standard JDK, so you must add it to your project as a dependency, commonly via Maven or Gradle.

import static org.mockito.Mockito.*;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.*;

class Database {
public boolean connect() {
// Connects to a database
return true;
}

public String getUser(String userId) {
// Returns user data
return null;
}
}

class UserService {
private Database database;

public UserService(Database database) {
this.database = database;
}

public boolean userExists(String userId) {
database.connect();
return database.getUser(userId) != null;
}
}

public class UserServiceTest {
@Test
public void testUserExists() {
Database mockDatabase = mock(Database.class);
when(mockDatabase.getUser("123")).thenReturn("John");

UserService userService = new UserService(mockDatabase);
Assert.assertTrue(userService.userExists("123"));

verify(mockDatabase).connect();
verify(mockDatabase).getUser("123");
}
}

Here, Mockito is used to mock the Database class. We can specify behaviors, such as returning a specific value when methods are called, and verify interactions.

Making Code Unit Testable

To make code unit testable, consider these practices:

  1. Use Interfaces and Dependency Injection: This helps decouple the components, making it easier to replace real dependencies with mock objects during testing.
  2. Keep Methods Focused on a Single Functionality: This simplifies writing tests and ensures that tests only test one thing.
  3. Avoid Static Methods and Singletons: These make it hard to replace dependencies during testing.
  4. Pass Dependencies from Outside: Rather than creating dependencies inside methods or classes, pass them as parameters or via constructors (Dependency Injection).

Unit Tests vs. Integration Tests

Unit Tests:

  • Purpose: To test individual components (classes or methods) in isolation from the rest of the system.
  • Speed: They are fast because they test something small and do not interact with databases, file systems, or external systems.
  • Examples: Testing if a method returns the correct value given a specific input.

Integration Tests:

  • Purpose: To test how different parts of the application work together or to test interaction with external dependencies like databases or web services.
  • Speed: It is slower than unit tests because they cover more functionality and involve more setup, like configuring a database.
  • Examples: Testing if a database correctly stores the expected values after an operation.

Python integration test example:

import unittest

class Database:
def connect(self):
# Actual database connection
pass

def get_user(self, user_id):
# Query the actual database
pass

class TestDatabaseIntegration(unittest.TestCase):
def test_database_connection(self):
db = Database()
db.connect()
user = db.get_user('123')
self.assertIsNotNone(user)

if __name__ == '__main__':
unittest.main()

Java Integration Test Example:

import org.junit.Test;
import org.junit.Assert;

public class DatabaseTest {
@Test
public void testDatabaseConnection() {
Database db = new Database();
db.connect();
String user = db.getUser("123");
Assert.assertNotNull(user);
}
}

These integration tests test the connection and data retrieval methods of systems (like a database). These examples give you a clear idea of how mocking differs from integration tests, with mocks isolating parts of the system for unit testing and integration tests confirming the overall system functionality. 🤓

27. What are SOLID, DRY, YAGNI, KISS, and Holywood principles?

1. SOLID Principles

SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.

  • Single Responsibility Principle (SRP): A class should have one, and only one, reason to change, meaning it should have only one job.
  • Open/Closed Principle (OCP): Software entities should be open for extension but closed for modification.
  • Liskov Substitution Principle (LSP): Objects in a program should be replaceable with instances of their subtypes without altering the program's correctness.
  • Interface Segregation Principle (ISP): Many client-specific interfaces are better than one general-purpose interface.
  • Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).

Python example for SOLID:

class User:
def __init__(self, name):
self.name = name

def get_name(self):
return self.name

class UserDB:
def __init__(self, user):
self.user = user

def save_user(self):
print(f"User {self.user.get_name()} saved to the database")

# Usage
user = User("John")
user_db = UserDB(user)
user_db.save_user()

Java example for SOLID:

public class User {
private String name;

public User(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

public class UserDB {
private User user;

public UserDB(User user) {
this.user = user;
}

public void saveUser() {
System.out.println("User " + user.getName() + " saved to the database");
}
}

// Usage
User user = new User("John");
UserDB userDB = new UserDB(user);
userDB.saveUser();

2. DRY (Don’t Repeat Yourself)

It aims to reduce the repetition of terms by replacing them with abstractions or using data normalization to avoid redundancy.

Python example for DRY:

def calculate_area(base, height):
return 0.5 * base * height

area1 = calculate_area(10, 5)
area2 = calculate_area(20, 10)

Java example for DRY:

public class Geometry {
public static double calculateArea(double base, double height) {
return 0.5 * base * height;
}
}

// Usage
double area1 = Geometry.calculateArea(10, 5);
double area2 = Geometry.calculateArea(20, 10);

3. YAGNI (You Aren’t Gonna Need It)

Encourages developers only to add functionality once deemed necessary. It’s a principle against over-engineering.

4. KISS (Keep It Simple, Stupid)

Advocates that systems function best if they are kept simple rather than made complicated; simplicity should be a key goal, and unnecessary complexity should be avoided.

Python example for KISS:

def add(a, b):
return a + b

result = add(1, 2)

Java example for KISS:

public class SimpleMath {
public int add(int a, int b) {
return a + b;
}
}

// Usage
SimpleMath math = new SimpleMath();
int result = math.add(1, 2);

5. Hollywood Principle

The Hollywood Principle 😎 “Don’t call us, we’ll call you.” also known as the “Inversion of Control” (IoC) principle, is used to prevent dependencies between components, where low-level components give control to high-level components. It allows lower-level components to have their logic called by higher-level components. This principle can be effectively demonstrated through callback functions, event listeners, or frameworks that use inversion of control, like Spring for Java.

Here’s a straightforward example in Python and Java demonstrating the Hollywood Principle through a simple callback mechanism.

Python example:

In Python, we can implement this principle by passing a function as a parameter to another function, which allows the higher-level function to call the lower-level function’s logic.

class EventManager:
def __init__(self):
self.listeners = []

def register_listener(self, listener):
self.listeners.append(listener)

def notify_listeners(self, event):
for listener in self.listeners:
listener(event)

def my_listener(event):
print(f"Received event: {event}")

# Usage
manager = EventManager()
manager.register_listener(my_listener)
manager.notify_listeners("Hello Hollywood Principle!")

In this example, the EventManager is the high-level module that controls when the low-level module my_listener is executed. The low-level module isn't called the high-level module; instead, it defines what should happen when it is called. This inversion of control typifies the Hollywood Principle.

Java example:

In Java, this principle is often demonstrated using interfaces and dependency injection. Still, a simple callback example with an interface as a function parameter can serve well as a good illustration of the concept.

interface EventListener {
void handleEvent(String event);
}

class EventManager {
private List<EventListener> listeners = new ArrayList<>();

public void registerListener(EventListener listener) {
listeners.add(listener);
}

public void notifyListeners(String event) {
for (EventListener listener: listeners) {
listener.handleEvent(event);
}
}
}

class MyListener implements EventListener {
@Override
public void handleEvent(String event) {
System.out.println("Received event: " + event);
}
}

// Usage
public class Main {
public static void main(String[] args) {
EventManager manager = new EventManager();
MyListener listener = new MyListener();
manager.registerListener(listener);
manager.notifyListeners("Hello Hollywood Principle in Java!");
}
}

In this Java example, EventManager acts as the high-level component, while MyListener implements the EventListener interface, a low-level component. The EventManager controls when MyListener executes but does not know what exactly happens when the event is handled, adhering to the Hollywood Principle.

Both examples demonstrate how higher-level modules can defer the exact processing method to lower-level modules, thus implementing the Hollywood Principle and promoting loose coupling and flexibility in software applications. 🙂

28. What is circular dependency, and how can it be managed?

🎯: Circular dependency occurs when two or more modules depend on each other directly or indirectly. This creates a cycle that can lead to various problems in software applications, including but not limited to difficulties in maintenance, testing, and even deployment.

Examples of Circular Dependencies:

  1. Direct Circular Dependency: Module A depends on Module B; Module B depends on Module A.
  2. Indirect Circular Dependency: Module A depends on Module B; Module B depends on Module C; Module C depends on Module A.

Such dependencies can make it hard to understand, modify, or scale the application. They are particularly problematic in large systems where the interdependencies can become complex and challenging to manage.

How to Manage Circular Dependencies

1. Redesign the Architecture:

  • Decouple the Modules: Look for ways to break the dependency cycle through redesign. This often involves rethinking the responsibilities of the involved modules. Can the shared functionality that creates the dependency be isolated into a separate module that both dependent modules can access independently?
  • Use Interface Abstraction: In OOP, you can often resolve circular dependencies by introducing interfaces or abstract classes. Both modules depend on abstractions rather than on each other directly.

2. Dependency Injection (DI):

  • DI can be used to manage complex dependencies. By injecting dependencies at runtime rather than at compile time, you can avoid many issues related to circular dependencies.

3. Inversion of Control (IoC):

  • Like DI, IoC refers to transferring the control of objects or portions of a program to a container or framework. By centralizing the management of dependencies, you’re less likely to encounter circular dependencies.

4. Merge the Modules:

  • If two modules are so interdependent that they continually reference each other, combining them into a single module might make sense. However, this is typically a last resort, as it can lead to a module that’s too large and violates the Single Responsibility Principle.

5. Event-Driven Approaches:

  • Use events to handle interactions between modules. Instead of calling each other directly, modules can emit events and listen for events from different modules. This reduces direct dependencies and can help untangle complex dependency chains.

Python example (before (with Circular Dependency)):

class ClassA:
def __init__(self):
self.b = ClassB(self)

class ClassB:
def __init__(self, a):
self.a = a

And after (using Interface Abstraction):

from abc import ABC, abstractmethod

class InterfaceA(ABC):
@abstractmethod
def method_a(self):
pass

class ClassA(InterfaceA):
def __init__(self, b):
self.b = b

def method_a(self):
print("Method A")

class ClassB:
def __init__(self, a: InterfaceA):
self.a = a

# Usage
a = ClassA(None) # Temporary None to create an instance of B
b = ClassB(a)
a.b = b # Establish the back-reference

In the refactored example, ClassA and ClassB do not directly depend on each other's concrete implementations. Instead, ClassB depends on an abstraction (InterfaceA), which ClassA implements. This arrangement resolves the circular dependency.

Managing circular dependencies is crucial for maintaining clean, manageable, scalable code. It’s often best addressed during the design phase to avoid costly refactoring later. 💰

29. How does object serialization work in your programming language?

🎯: Object serialization converts an object’s state into a format that can be stored, transmitted, and reconstructed later. This can be useful in various scenarios, such as saving an object’s state to a file, sending it across a network, or storing it in a database.

Example of Python Serialization with pickle:

In Python, serialization is commonly done using the pickle module. This module can serialize most Python objects to a binary or textual representation and then deserialize them back to Python objects.

import pickle

# Example class
class Dog:
def __init__(self, name):
self.name = name

# Creating an object
my_dog = Dog("Rex")

# Serializing the object
with open('dog.pickle', 'wb') as file:
pickle.dump(my_dog, file)

# Deserializing the object
with open('dog.pickle', 'rb') as file:
loaded_dog = pickle.load(file)
print(loaded_dog.name) # Outputs: Rex

This code snippet demonstrates saving a Dog object to a file and then reading it back. pickle handles the conversion between the object representation and a byte stream.

Example of Java Serialization:

In Java, serialization is supported natively by the language through the Serializable interface. Any class that implements this interface can be serialized using the ObjectOutputStream and deserialized using the ObjectInputStream.

import java.io.*;

// Example class
public class Dog implements Serializable {
private String name;

public Dog(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

public class Main {
public static void main(String[] args) {
Dog myDog = new Dog("Rex");

try {
// Serializing the object
FileOutputStream fileOut = new FileOutputStream("dog.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(myDog);
out.close();
fileOut.close();

// Deserializing the object
FileInputStream fileIn = new FileInputStream("dog.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Dog loadedDog = (Dog) in.readObject();
in.close();
fileIn.close();

System.out.println("Loaded Dog's Name: " + loadedDog.getName()); // Outputs: Rex
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

In this example, a Dog object 🐶 is serialized to a file and then deserialized back into an object. To enable this functionality, the class must implement Serializable.

30. Explain how multithreading can be implemented in OOP.

🎯: Implementing multithreading in an object-oriented programming (OOP) environment involves creating and managing threads to execute multiple tasks concurrently. This approach can significantly enhance an application's performance, particularly when it involves operations that can be performed in parallel, such as data processing or handling multiple user requests simultaneously.

Examples of Multithreading Use Cases:

  • Web Servers: Handle multiple requests simultaneously. Each request can be processed in a separate thread, allowing the server to serve multiple clients concurrently.
  • Multimedia Applications: Manage user interface responsiveness while performing intensive background processing like video rendering or music playback.
  • Financial Applications: Real-time calculation and processing of large volumes of financial data, where tasks are distributed across multiple threads to speed up processing.
  • Games: Manage user input, update game state, render graphics, and play audio, often on different threads, to maintain smooth performance and responsiveness.

Implementations in Programming Languages

  • Java: Provides extensive support for multithreading through the java.lang.Thread class and the Runnable interface. Synchronization can be achieved using synchronized blocks or methods.
  • Python: Supports threading through the threading module, but due to the Global Interpreter Lock (GIL), threads can only execute parallel I/O-bound tasks. CPU-bound tasks may not improve performance because the GIL allows only one thread to perform simultaneously.
  • C++: Offers sophisticated multithreading capabilities with the Standard Thread Library (introduced in C++11), including various mutex types for synchronization.

In OOP, threads can be encapsulated as objects. Here’s how you can implement multithreading in OOP languages like Java and Python:

Python example:

Python supports threading via the threading module, part of the standard library.

import threading

class MyThread(threading.Thread):
def run(self):
print("Hello from a thread!")

# Usage
if __name__ == '__main__':
threads = [MyThread() for _ in range(5)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()

# The output will be:

# Hello from a thread!
# Hello from a thread!
# Hello from a thread!
# Hello from a thread!
# Hello from a thread!

These messages may appear in any order and not necessarily in the same sequence from one program run to another. This is due to the nature of thread scheduling and concurrency. Each “Hello from a thread!” is printed by one of the five threads created and started by the script.

Java example:

Java provides built-in support for multithreading through its java.lang.Thread class and java.lang.Runnable interface. Here's a simple example:

class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}

public static void main(String[] args) {
HelloThread t = new HelloThread();
t.start();
}
}

class HelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from a runnable!");
}

public static void main(String[] args) {
Thread t = new Thread(new HelloRunnable());
t.start();
}
}

In these examples:

  • HelloThread extends Thread and overrides the run() method.
  • HelloRunnable implements Runnable and its run() method. A Thread object is then created and executed with a HelloRunnable instance.
  • Each class has its main method. Therefore, the output depends on which class's main method is executed. If you run each class separately, each one will independently create and start a thread, leading to these possible outputs.
  • If HelloThread's main is run: “Hello from a thread!”
  • If HelloRunnable's main is run: “Hello from a runnable!”
  • If you were to compile and run both classes in a single execution context (which typically isn’t standard practice as each class has its main method intended to be an entry point), the actual sequence of the outputs could vary because the JVM's thread scheduler controls thread scheduling and execution order and are not deterministic. However, typically, both messages would be printed; the order may vary. 🤷‍♂️

Of course, learning OOP is not limited to these 30 questions. 🙂 Still, if you can answer all of them, you already have a solid understanding of the basics of OOP, significantly increasing your chances of becoming a Software Engineer! My choice of two programming languages, Java and Python, for this article, was not accidental: firstly, they are two of the most popular programming languages today, and secondly, they are different. This allowed me to explain the same OOP principles from various angles, and in question 17, I had to explain more about C++ for comparison. Anyway, thanks for reading, and good luck! 😉

P.S. Here’s a list of some programming languages categorized by their paradigms:

Object-Oriented Programming (OOP) Languages:

  • Java: Extensively used in enterprise environments, known for its portability across platforms.
  • C++: An extension of C that includes object-oriented features like classes and inheritance.
  • Python: A versatile language that supports OOP, procedural, and functional programming paradigms.
  • Ruby: Known for its elegant syntax, it is fully object-oriented, with everything being an object.
  • Swift: Developed by Apple, primarily used for iOS and macOS applications, and supports OOP principles.

Procedural Programming Languages:

  • C: The foundational language for many modern languages, used extensively in system programming.
  • Pascal: Initially designed as a teaching tool for programming concepts, known for its clear syntax.
  • Fortran: One of the oldest programming languages, designed for numeric and scientific computing.

Functional Programming Languages:

  • Haskell: A pure functional language with strong static typing and lazy evaluation.
  • Scala: Integrates object-oriented and functional programming features and runs on the JVM.
  • Erlang: Used for building massively scalable real-time systems with high availability.

Domain Specific Languages (DSL):

  • SQL (Structured Query Language): Designed for managing and manipulating relational databases.
  • HTML (HyperText Markup Language): The standard markup language for creating web pages.
  • CSS (Cascading Style Sheets) describes the presentation of a document written in HTML or XML.

Scripting Languages:

  • JavaScript: Essential for web development, used to create interactive effects within web browsers.
  • Perl: Known for its text-processing capabilities, it is used for system administration, web development, and more.
  • PHP: Widely used for server-side web development.

Logical Programming Languages:

  • Prolog: Predominantly used in AI and computational linguistics, based on formal logic.

Concurrent Programming Languages:

  • Go: Designed at Google, it is known for its simplicity and support for high-performance networking and multiprocessing.
  • Erlang: Used for building distributed and fault-tolerant systems.

Declarative Programming Languages:

  • SQL: Describes what data to retrieve, not how to retrieve it.
  • HTML: Describes the structure of web pages semantically.

Assembly Languages:

  • Assembly: Low-level programming language closely correlated with the architecture’s machine code instructions.

--

--

Efim Shliamin

Proficient Computer Scientist, B.Sc., with expertise in Software Engineering & Data Science, adept in solving complex biological and medical software problems.