Saturday, 17 May 2025

SOLID Pattern Quick Notes - Design Patterns for Writing Classes in OOPS

SOLID is a famous design pattern for writing classes in OOPS

S: Single Responsibility Principle

O: Open Closed Principle

L: Liskov's Substitution Principle

I: Interface Segregation Principle

D: Dependency Inversion Principle


Single Responsibility Principle (SRP)

- Class should have one and only one responsibility

- Write a Class to achieve one goal

- SRP helps to provide high maintainability and better visibility to control across application module

Open Closed Principle (OCP)

- Software components should be open for extension, but closed for modification.

- Our classes should not contain constraints that will require other developers to modify our classes in order to accomplish their job – only by extend our classes to accomplish their job.

- It helps to achieve  software extensibility in a versatile, intuitive, and non-harmful way.

 Liskov's Substitution Principle (LSP)

 -Derived types must be completely substitutable for their base types.

- Objects of subclasses must behave in the same way as the objects of super classes.

- This principle useful for runtime-type identification followed by the cast.

Interface Segregation Principle (ISP)

-Clients should not be forced to implement unnecessary methods that they will not use.  

-splits an interface into two or more interfaces until clients are not forced to implement methods that they will not use.

This principle stands for Clients should not be forced to implement unnecessary methods that they will not use. In other words, we should split an interface into two or more interfaces until clients are not forced to implement methods that they will not use. For example, consider the Connection interface, which has three methods: connect(), socket(), and http(). A client may want to implement this interface only for connections via HTTP. Therefore, they don't need the socket() method. Most of the time, the client will leave this method empty, and this is a bad design. In order to avoid such situations, simply split the Connection interface into two interfaces; SocketConnection with the socket() method, and HttpConnection with the http() method. Both interfaces will extend the Connection interface that remains with the common method, connect()


Dependency Inversion Principle (DIP).

-Depend on abstractions, not on concretions.

- sustains the use of abstract layers to bind concrete modules together instead of having concrete modules that depend on other concrete modules.

- sustains the decoupling of concrete modules.

This principle stands for Depend on abstractions, not on concretions. This means that we should rely on abstract layers to bind concrete modules together instead of having concrete modules that depend on other concrete modules. To accomplish this, all concrete modules should expose abstractions only. This way, the concrete modules allow extension of the functionality or plug-in in another concrete module while retaining the decoupling of concrete modules. Commonly, high coupling occurs between high-level concrete modules and low-level concrete modules.

Ex: A database JDBC URL, PostgreSQLJdbcUrl, can be a low-level module, while a class that connects to the database may represent a high-level module, such as ConnectToDatabase#connect().

Time Complexity of an Algorithm

 

Time Complexity Description Example
O(1) Constant time     Accessing an element in an array
O(log n) Logarithmic time     Binary search
O(n) Linear time     Iterating over an array
O(n log n) Linearithmic time     Merge sort, quicksort (avg case)
O(n²) Quadratic time     Nested loops over array
O(2ⁿ) Exponential time     Recursive Fibonacci
O(n!) Factorial time     Permutations / Travelling Salesman


O(1) - Constant Time - The time doesn't change no matter how big the input is

Ex: Accessing element of an array . int x = nums[0];  // Always takes the same time

O(log n) - Logarithimic Time : You reduce the input size by half each step i.e Guessing a number between 1 and 100 by halving the range each time

Ex: Binary Search 

 while (low <= high) {
    int mid = (low + high) / 2;
    if (nums[mid] == target) return mid;
    else if (nums[mid] < target) low = mid + 1;
    else high = mid - 1;
}

O(n) - Linear time - Time grows directly with input size

Ex : Looping over an array i.e. Reading every page in a book once
for (int i : nums) {
    System.out.println(i);
}

O(n log n) - Linearithmic Time - A combination of linear and logarithimic Time - common in efficient sorting

Ex; Merge Sort, quick sort (average case) - merge sort splits the array ( log n) and merges each part (O (n))

Sorting a phone book - split , sort and merge

O(n2) - Quadratic Time - Time grows with the square of the input - often from the nested loops
Ex; Two loops over an array - i.e. comparing every student with every other student

for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        // O(n²)
    }
}

O(2n) - Exponential Time - Each step doubles the number of operations

Ex : Recursive fibonacci 

int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}

O(n!) - Factorial Time - Insanely slow for even modest input sizes — involves all permutations.

Ex : Generating all permutations - Trying every possible seating arrangement for n people.

void permute(List<Integer> nums, int index) {
    if (index == nums.size()) return;
    for (int i = index; i < nums.size(); i++) {
        Collections.swap(nums, i, index);
        permute(nums, index + 1);
        Collections.swap(nums, i, index);
    }
}








Monday, 5 May 2025

System Design Preparation

 Following are the system design preparation materials

https://github.com/ashishps1/awesome-system-design-resources?tab=readme-ov-file

https://dev.to/somadevtoo/9-software-architecture-patterns-for-distributed-systems-2o86

https://martinfowler.com/articles/patterns-of-distributed-systems/