Low Level Design Interview ‐ Object oriented Design || Design pattern and Popular libs - ashish-ghub/docs GitHub Wiki

Low level design:

Design pattern

Design Principle:(head first book)

    1. Program to an interface, not an implementation.
    1. Identify the aspects of your application that vary and separate them from what stays the same.

    take the parts that vary and encapsulate them, so that later you can alter or extend the parts that vary without affecting those that don’t.

    1. Favor composition over inheritance : HAS-A can be better than IS-A

Some more principal :

  • Don't Repeat Yourself (DRY) : The DRY principle is stated as "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system".
  • Curly's Law - Do One Thing
  • Keep It Simple Stupid (KISS)

https://workat.tech/machine-coding/tutorial/software-design-principles-dry-yagni-eytrxfhz1fla

SOLID Design principles

The SOLID principles were introduced by Robert C. Martin in 2000 . They all serve the same purpose: "To create understandable, readable, and testable code that many developers can collaboratively work on."

  • i.) Single Responsibility Principle : This principle states that a class should only have one responsibility. Furthermore, it should only have one reason to change.

    Its benefits:

    • Testing – A class with one responsibility will have far fewer test cases.
    • Lower coupling – Less functionality in a single class will have fewer dependencies.
    • Organization – Smaller, well-organized classes are easier to search than monolithic ones.
  • ii.) Open-closed principle (Open for Extension, Closed for Modification) : We stop ourselves from modifying existing code and causing potential new bugs in an otherwise happy application.

    example : As part of a new project, imagine we've implemented a Guitar class. But after a few months, we decide the Guitar is a little boring and could use a cool flame pattern to make it look more rock and roll. At this point, it might be tempting to just open up the Guitar class and add a flame pattern — but who knows what errors that might throw up in our application. Instead, let's stick to the open-closed principle and simply extend our Guitar class and create a new class and use it.

  • iii) Liskov substitution Principle (LSP)

This principle states that “Derived or child classes must be substitutable for their base or parent classes”. In other words, if class A is a subtype of class B, then we should be able to replace B with A without interrupting the behavior of the program.

To better understand it with below example: Let’s consider I have an abstract class called SocialMedia , who supported all social media activity for user to entertain them like below

public interface SocialMedia {

   public void chatWithFriend();

   public void publishPost(Object post);

   public void sendPhotosAndVideos();

}

Social media can have multiple implementation or can have multiple child like Facebook, WhatsApp ,instagram and Twitter etc.. now let’s assume Facebook want to use this features or functionality .

public class Facebook implements SocialMedia {

   public void chatWithFriend() {
    //logic  
   }

   public void publishPost(Object post) {
    //logic  
   }

   public void sendPhotosAndVideos() {
    //logic  
   }
}

We can consider Facebook is complete substitute of SocialMedia class , both can be replaced without any interrupt .

let’s discuss WhatsApp class

public class WhatsApp extends SocialMedia {
   public void chatWithFriend() {
    //logic
   }

  public void publishPost(Object post) {
    //Not Applicable
     throw new Exception("can not publish a post!");
  }

  public void sendPhotosAndVideos() {
  //logic
  }
}

Due to publishPost() method whatsapp child is not substitute of parents SocialMedia so it doesn’t follow LSP .

create a Social media interface and SocialPostManager for publish .

public interface SocialMedia {  
   public void chatWithFriend();
   public void sendPhotosAndVideos()
}

public interface SocialPostManager { 
    public void publishPost(Object post);
}

So Facebook class can be fixed as below :

public class Facebook implements SocialMedia, SocialPostManager  {

   public void chatWithFriend() {
    //logic  
   }

   public void publishPost(Object post) {
    //logic  
   }

   public void sendPhotosAndVideos() {
    //logic  
   }
}

Note: Here this rule LSP seems like it to follow above design principal rule 2 , here we are separating out varying part across the Childs.

  • Vi :Interface Segregation Principle (ISP)

Segregation means keeping things separated, and the Interface Segregation Principle is about separating the interfaces.

The principle states that many client-specific interfaces are better than one general-purpose interface. Clients should not be forced to implement a function they do no need.

This is a simple principle to understand and apply, so let's see an example.

public interface ParkingLot {

	void parkCar();	// Decrease empty spot count by 1
	void unparkCar(); // Increase empty spots by 1
	void getCapacity();	// Returns car capacity
	double calculateFee(Car car); // Returns the price based on number of hours
	void doPayment(Car car);
}

We modeled a very simplified parking lot. It is the type of parking lot where you pay an hourly fee. Now consider that we want to implement a parking lot that is free.

public class FreeParking implements ParkingLot {

@Override
public void parkCar() {
	
}
    //other method here
.......

@Override
public double calculateFee(Car car) {
	return 0;
}

@Override
public void doPayment(Car car) {
	throw new Exception("Parking lot is free");
}
}

But it is too specific. Because of that, our FreeParking class was forced to implement payment-related methods that are irrelevant. Let's separate or segregate the interfaces.

better design

  • V. Dependency Inversion Principle (DIP)

The principle states that we must use abstraction (abstract classes and interfaces) instead of concrete implementations. High-level modules should not depend on the low-level module but both should depend on the abstraction

let’s consider an best use case

let’s assume you have two option to do payments Debit card and Credit card

public class DebitCard{
  public void doTransaction(int amount){
    System.out.println("tx done with DebitCard");
  }

}

Credit Card

 public class CreditCard{
   public void doTransaction(int amount){
    System.out.println("tx done with CreditCard");
  }
 }

Now with this two card you went to shopping mall and purchased some order and decided to pay using CreditCard

public class ShoppingMall {

private DebitCard debitCard;

public ShoppingMall(DebitCard debitCard) {
        this.debitCard = debitCard;
   }
   
public void doPayment(Object order, int amount){              
    debitCard.doTransaction(amount); 
 }
 
public static void main(String[] args) {
     DebitCard debitCard=new DebitCard();
     ShoppingMall shoppingMall=new ShoppingMall(debitCard);
     shoppingMall.doPayment("some order",5000);
    }
}

If you observe this is wrong design of coding , now ShoppingMall class tightly coupled with DebitCard

because to follow DIP we need to design our application in such a way so that my shopping mall payment system should accept any type of ATM Card (it shouldn’t care whether it is debit or credit card)

To simplify this designing principle further i am creating a interface called Bankcards like bellow and Credit and Debit care class should implement BankCard.

public interface BankCard {
   public void doTransaction(int amount);
}

Ref:

1. Strategy Pattern :

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

2. Factory:

  • The Factory Method Pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses. A superclass specifies all standard and generic behavior (using pure virtual "placeholders" for creation steps), and then delegates the creation details to subclasses that are supplied by the client.

  • logic for object creation needs to encapsulated, as remove code duplication , central place to manage and in future if need to add new Implementation type only one place to change , client logic remains same

  • Ref: https://refactoring.guru/design-patterns/factory-method

3. Abstract Factory:

Abstract Factory is a creational design pattern that lets you produce families of related objects without specifying their concrete classes


  • Object Orient Design

https://www.tutorialspoint.com/object_oriented_analysis_design/index.htm


DDD (domain driven design)


  • User Interface (or Presentation Layer)

Responsible for showing information to the user and interpreting the user's commands. The external actor might sometimes be another computer system rather than a human user.

  • Application Layer

Defines the jobs the software is supposed to do and directs the expressive domain objects to work out problems. The tasks this layer is responsible for are meaningful to the business or necessary for interaction with the application layers of other systems. This layer is kept thin. It does not contain business rules or knowledge, but only coordinates tasks and delegates work to collaborations of domain objects in the next layer down. It does not have state reflecting the business situation, but it can have state that reflects the progress of a task for the user or the program.

  • Domain Layer (or Model Layer)

Responsible for representing concepts of the business, information about the business situation, and business rules. State that reflects the business situation is controlled and used here, even though the technical details of storing it are delegated to the infrastructure. This layer is the heart of business software.

  • Infrastructure Layer

Provides generic technical capabilities that support the higher layers: message sending for the application, persistence for the domain, drawing widgets for the UI, and so on. The infrastructure layer may also support the pattern of interactions between the four layers through an architectural framework.


Popular Library:

Cache Lib guava

A builder of LoadingCache and Cache instances having any combination of the following features:

  • automatic loading of entries into the cache
  • least-recently-used eviction when a maximum size is exceeded
  • time-based expiration of entries, measured since last access or last write
  • keys automatically wrapped in weak references
  • values automatically wrapped in weak or soft references
  • notification of evicted (or otherwise removed) entries
  • accumulation of cache access statistics

Usage example:

` LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
   .maximumSize(10000)
   .expireAfterWrite(10, TimeUnit.MINUTES)
   .removalListener(MY_LISTENER)
   .build(
       new CacheLoader<Key, Graph>() {
         public Graph load(Key key) throws AnyException {
           return createExpensiveGraph(key);
         }
       }); `

https://guava.dev/releases/17.0/api/docs/com/google/common/cache/CacheBuilder.html

Examples


Decorator design pattern

  Collections
  {
     public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
       return new SynchronizedMap<>(m);
     }

/**
 * @serial include
 */
private static class SynchronizedMap<K,V>
    implements Map<K,V>, Serializable {
    private static final long serialVersionUID = 1978198479659022715L;

    private final Map<K,V> m;     // Backing Map
    final Object      mutex;        // Object on which to synchronize

    SynchronizedMap(Map<K,V> m) {
        this.m = Objects.requireNonNull(m);
        mutex = this;
    }

    SynchronizedMap(Map<K,V> m, Object mutex) {
        this.m = m;
        this.mutex = mutex;
    }

    public int size() {
        synchronized (mutex) {return m.size();}
    }
    public boolean isEmpty() {
        synchronized (mutex) {return m.isEmpty();}
    }
    public boolean containsKey(Object key) {
        synchronized (mutex) {return m.containsKey(key);}
    }
    public boolean containsValue(Object value) {
        synchronized (mutex) {return m.containsValue(value);}
    }
    public V get(Object key) {
        synchronized (mutex) {return m.get(key);}
    }

    public V put(K key, V value) {
        synchronized (mutex) {return m.put(key, value);}
    }
    public V remove(Object key) {
        synchronized (mutex) {return m.remove(key);}
    }
    public void putAll(Map<? extends K, ? extends V> map) {
        synchronized (mutex) {m.putAll(map);}
    }
    public void clear() {
        synchronized (mutex) {m.clear();}
    }

    private transient Set<K> keySet;
    private transient Set<Map.Entry<K,V>> entrySet;
    private transient Collection<V> values;

    public Set<K> keySet() {
        synchronized (mutex) {
            if (keySet==null)
                keySet = new SynchronizedSet<>(m.keySet(), mutex);
            return keySet;
        }
    }

    public Set<Map.Entry<K,V>> entrySet() {
        synchronized (mutex) {
            if (entrySet==null)
                entrySet = new SynchronizedSet<>(m.entrySet(), mutex);
            return entrySet;
        }
    }

    public Collection<V> values() {
        synchronized (mutex) {
            if (values==null)
                values = new SynchronizedCollection<>(m.values(), mutex);
            return values;
        }
    }

    public boolean equals(Object o) {
        if (this == o)
            return true;
        synchronized (mutex) {return m.equals(o);}
    }
    public int hashCode() {
        synchronized (mutex) {return m.hashCode();}
    }
    public String toString() {
        synchronized (mutex) {return m.toString();}
    }

    // Override default methods in Map
    @Override
    public V getOrDefault(Object k, V defaultValue) {
        synchronized (mutex) {return m.getOrDefault(k, defaultValue);}
    }
    @Override
    public void forEach(BiConsumer<? super K, ? super V> action) {
        synchronized (mutex) {m.forEach(action);}
    }
    @Override
    public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        synchronized (mutex) {m.replaceAll(function);}
    }
    @Override
    public V putIfAbsent(K key, V value) {
        synchronized (mutex) {return m.putIfAbsent(key, value);}
    }
    @Override
    public boolean remove(Object key, Object value) {
        synchronized (mutex) {return m.remove(key, value);}
    }
    @Override
    public boolean replace(K key, V oldValue, V newValue) {
        synchronized (mutex) {return m.replace(key, oldValue, newValue);}
    }
    @Override
    public V replace(K key, V value) {
        synchronized (mutex) {return m.replace(key, value);}
    }
    @Override
    public V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction) {
        synchronized (mutex) {return m.computeIfAbsent(key, mappingFunction);}
    }
    @Override
    public V computeIfPresent(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        synchronized (mutex) {return m.computeIfPresent(key, remappingFunction);}
    }
    @Override
    public V compute(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        synchronized (mutex) {return m.compute(key, remappingFunction);}
    }
    @Override
    public V merge(K key, V value,
            BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        synchronized (mutex) {return m.merge(key, value, remappingFunction);}
    }

    private void writeObject(ObjectOutputStream s) throws IOException {
        synchronized (mutex) {s.defaultWriteObject();}
    }
}

//##### ---------------------------------

 public static <T> List<T> unmodifiableList(List<? extends T> list) {
     return (list instanceof RandomAccess ?
            new UnmodifiableRandomAccessList<>(list) :
            new UnmodifiableList<>(list));
  }

/**
 * @serial include
 */
static class UnmodifiableList<E> extends UnmodifiableCollection<E>
                              implements List<E> {
    private static final long serialVersionUID = -283967356065247728L;

    final List<? extends E> list;

    UnmodifiableList(List<? extends E> list) {
        super(list);
        this.list = list;
    }

    public boolean equals(Object o) {return o == this || list.equals(o);}
    public int hashCode()           {return list.hashCode();}

    public E get(int index) {return list.get(index);}
    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }
    public int indexOf(Object o)            {return list.indexOf(o);}
    public int lastIndexOf(Object o)        {return list.lastIndexOf(o);}
    public boolean addAll(int index, Collection<? extends E> c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void replaceAll(UnaryOperator<E> operator) {
        throw new UnsupportedOperationException();
    }
    @Override
    public void sort(Comparator<? super E> c) {
        throw new UnsupportedOperationException();
    }

    public ListIterator<E> listIterator()   {return listIterator(0);}

    public ListIterator<E> listIterator(final int index) {
        return new ListIterator<E>() {
            private final ListIterator<? extends E> i
                = list.listIterator(index);

            public boolean hasNext()     {return i.hasNext();}
            public E next()              {return i.next();}
            public boolean hasPrevious() {return i.hasPrevious();}
            public E previous()          {return i.previous();}
            public int nextIndex()       {return i.nextIndex();}
            public int previousIndex()   {return i.previousIndex();}

            public void remove() {
                throw new UnsupportedOperationException();
            }
            public void set(E e) {
                throw new UnsupportedOperationException();
            }
            public void add(E e) {
                throw new UnsupportedOperationException();
            }

            @Override
            public void forEachRemaining(Consumer<? super E> action) {
                i.forEachRemaining(action);
            }
        };
    }

    public List<E> subList(int fromIndex, int toIndex) {
        return new UnmodifiableList<>(list.subList(fromIndex, toIndex));
    }

    private Object readResolve() {
        return (list instanceof RandomAccess
                ? new UnmodifiableRandomAccessList<>(list)
                : this);
    }
}

} // end of Collections

⚠️ **GitHub.com Fallback** ⚠️