Everything is an object and every action must be performed on an object by an object’s methods.
The 4 pillars of OOP: Abstraction, Encapsulation, Inheritance, and Polymorphism. notes
Composition vs Aggregation: both are types of association, former is a strong one, latter is a weak association.
// Composition: one can't exist without another
class Human {
final Heart heart; // final; because we will now need to init it always
Human(Heart heart){ // constructor; must recieve a heart and puts it into human
this.heart = heart;
}
}
// Aggregation: one can exist independently without the other; no need to provide value to water instance var below
class Glass {
Water water;
}
// can be made bi-directional too (optional)
class Water {
Glass glass;
}
Note that in Composition we can also instantiate the new Heart() object inside the class itself (either inline or inside the constructor) such that it gets created automatically when Human class is instantiated, but such code need not be present in case of Aggregation.
Help create clean, extensible, and maintainable code.
Has 5 principles within it:
Better framing - a class should be responsible to one, and only one, Actor. A class can do a single thing very well, but if multiple actors (Finance, Business Ops, Platform) are using it, it’ll be a pain to cater to each one when they start demanding changes to it that contradict each other.
Ex - Invoice class can be split into the following four classes based on reponsibility and who uses it:
Invoice class - primary entityConsoleService class - to print invoice details to consoleDatabaseService class - to save invoice details to databaseEmailService class - to send invoice details via emailclass Invoice{
long id;
double price;
Invoice(long id, double price, double discount){
this.id = id;
this.price = price - (price * discount / 100.0);
}
void printInvoice(){
System.out.println("Invoice: " + id + ", and price = " + price);
}
void saveInvoiceToDB(){ }
void sendInvoiceToEmail() { }
}
Problem: the above class can change because of multiple reasons (because multiple actors depend on it) such as changes in database storing logic, or price discount calc (e.g. adding GST taxation).
A better way to write the above class without violating SRP by splitting functionality across multiple classes (one for each Actor) is:
class Invoice{
long id;
double price;
Invoice(long id, double price, double discount){
this.id = id;
this.price = price - (price * discount / 100.0);
}
}
class ConsoleService{
void print(Invoice invoice){
System.out.println("Invoice: " + invoice.id + ", and price = " + invoice.price);
}
}
class DatabaseService{
void saveToDB(Invoice invoice){ }
}
class EmailService{
void sendEmail(Invoice invoice) { }
}
// in main()
Invoice invoice = new Invoice(1L, 100.0, 50.0);
InvoicePrinter invoicePrinter = new InvoicePrinter();
invoicePrinter.print(invoice);
In the example above, we can also keep Invoice as an instance member in service and inject it by supplying with constructor upon instantiation.
Ex - impl new functionality of payment via crypto
class PaymentProcessor {
void process(String type, double amount) {
if (type.equals("credit")) {
// credit card logic
} else if (type.equals("paypal")) {
// paypal logic
}
// adding crypto means modifying this method (VIOLATION!)
}
}
Instead, we can use interface to allow adding new payment types without modifying existing code:
interface PaymentMethod {
void process(double amount);
}
class CreditCardPayment implements PaymentMethod {
public void process(double amount) { /* credit card logic */ }
}
class PayPalPayment implements PaymentMethod {
public void process(double amount) { /* paypal logic */ }
}
class CryptoPayment implements PaymentMethod {
public void process(double amount) { /* crypto logic */ }
}
// usage
class PaymentProcessor {
void process(PaymentMethod method, double amount) {
method.process(amount);
}
}
“open for extension” doesn’t actually mean inheritance with the Java keyword extend here. It just means extending the behavior of your system i.e. adding new behaviour.
Ex - Penguin is a technically a Bird, but its flightless. We can’t replace Bird object with Penguin object and expect things to not break in the way the below example is written.
// Violation
class Bird {
void fly(){ } // assumption that all birds fly
}
class Sparrow extends Bird {
@Override
void fly(){
System.out.println("Ok!"); // makes sense
}
}
class Penguin extends Bird {
@Override
void fly(){
throw new AssertionError("I can't fly!"); // can't fly; narrows down superclass behaviour
}
}
We can’t replace Bird object with Penguin object wherever Bird object is being used, since Penguin object’s fly() method will break, whenever we call it (try to fly). A possible fix is to refactor the code as shown below:
interface Flight {
void fly();
}
class Bird { }
class Sparrow extends Bird implements Flight {
@Override
void fly(){ }
// flight capable; makes sense
}
class Penguin extends Bird {
// doesn't have fly() method; makes sense
}
// Penguin class can be substituted for Bird class anywhere now
Another example is electric car as it doesn’t have an engine. A Car class can have MotorCar and ElectricCar subclasses, but ElectricCar object can’t replace wherever Car object is used since it can cause NullPointerException when instance member engine is accessed as engine will be null for ElectricCar instance.
interface for each distinct functionality and later provide their respective implementation. By such fine-grained splitting, we won’t need to provide impl to interface methods which the impl concrete class doesn’t even need.// so much to do for a Bear Keeper
public interface BearKeeper {
void washTheBear();
void feedTheBear();
void petTheBear();
}
Split to diff interfaces acc to functionality:
public interface BearCleaner {
void washTheBear();
}
public interface BearFeeder {
void feedTheBear();
}
public interface BearPetter {
void petTheBear();
}
And then implement each interface as needed:
public class BearCarer implements BearCleaner, BearFeeder {
public void washTheBear() {
//I think we missed a spot...
}
public void feedTheBear() {
//Tuna Tuesdays...
}
}
public class CrazyPerson implements BearPetter {
public void petTheBear() {
//Good luck with that!
}
}
In simple words, classes should only depend upon (use) interfaces and not other concrete classes. Interfaces should also use other interfaces only.
The “inversion” refers to who defines the contract. Normally, your business logic conforms to whatever the implementation provides. With DIP, you flip this: define an interface based on what your business logic needs, then have implementations conform to that interface. The implementation adapts to the business logic, not the other way around.
Simply put, when components of our system have dependencies on each other, we don’t directly inject a component’s dependency (concrete class) into another. Instead, we create abstractions (interface) based on our needs and use them instead.
// tight coupling using concrete classes (WiredKeyboard and WiredMouse)
public class PC{
private final WiredKeyboard keyboard;
private final WiredMouse mouse;
public PC(WiredKeyboard keyboard, WiredMouse monitor) {
this.keyboard = keyboard;
this.monitor = monitor;
}
}
Instead, we can refactor the above class as:
// loose coupling using interface types
public class PC{
private final Keyboard keyboard;
private final Mouse mouse;
public PC(Keyboard keyboard, Mouse monitor) {
this.keyboard = keyboard;
this.monitor = monitor;
}
}
// then we can pass any kind of object to "PC" class as long as its of type "Keyboard" and "Mouse"
public class WiredKeyboard implements Keyboard { }
public class BluetoothKeyboard implements Keyboard { }
public class WiredMouse implements Mouse { }
public class BluetoothMouse implements Mouse { }
Program against abstractions: program by keeping interfaces and their relations and interactions in mind. Don’t take concrete classes into consideration while designing. Beware of Premature Abstraction though.
Composition over Inheritance: prefer composition over inheritance, it has none of the issues that come with inheritance:
// an Order can be a StandardOrder, ExpressOrder, or InternationalOrder
// with inheritance
class Order {
void ship(){
// shipping logic
}
void track(){
// tracking logic
}
}
class StandardOrder extends Order { } // StandardOrder is-a Order object
class ExpressOrder extends Order { }
class InternationalOrder extends Order { }
// if we impl digital orders in the future, ship() and track() methods will be useless (throw exception) and break
class DigitalOrder extends Order {
@Override
public void ship(){
throw new UnsupportedOperationException("No shipment for digital orders.");
}
}
Instead of creating parent-child hierarchy, we can create classes based on separate concerns which are composed of objects they need i.e. Order:
// composition - shipment is a separate concern so we take it out and create separate classes for it composed of Order object
class Order { }
class GroundShipper {
Order o; // each shipment has-a order
// constructor injection
GroundShipper(Order o){
this.o = o;
}
void ship(){
// call Truck API
}
}
class FreightShipper {
Order o;
void ship(){
// call Train API
}
}
class AirShipper {
Order o;
void ship(){
// call Airline API
}
}
class DigitalDelivery {
Order o;
void deliver(){
// send email with download link
}
}
Note: Ideally we should pass order as method argument void ship(Order o), but above code snippet is to show composition so I wrote it using dependency injection via constructor.
There is still scope of enhancement here. Inheritance is implicitly a combination of two things: Code Reuse and Abstraction. Abstraction because parent class reference can act as a common interface for all the child classes and parent’s methods are common no matter what subclass object is passed (shown below). We can lose this capability in composition and therefore use dependency injection with a common interface which becomes the common contract here.
// inheritance: implicit abstraction
class OrderService {
Order order;
void onCheckout() {
order.calculateTotal(); // all Order objects will have these methods in this form (common contract)
order.generateInvoice();
order.ship();
}
}
Fully refactored code utilizing composition:
// composition: abstraction using interface, composing with dependency injection
class Order { }
interface DeliveryMethod {
void deliver(Order o);
}
class GroundShipper implements DeliveryMethod { }
class FreightShipper implements DeliveryMethod { }
class AirShipper implements DeliveryMethod { }
class DigitalDelivery implements DeliveryMethod { }
class OrderService {
DeliveryMethod dm; // dependency injection
OrderService(DeliveryMethod dm){
this.dm = dm;
}
void onCheckout(Order o){
delivery.deliver(o);
}
}
Encapsulate what varies: identify parts of a system that change with new requirements and isolate them from the parts that stay the same.
// pseudocode
if (pet.type() == dog) {
pet.bark();
} else if (pet.type() == cat) {
pet.meow();
} else if (pet.type() == duck) {
pet.quack();
}
// encapsulate the varying behavior behind a stable interface
pet.speak();
// if we add Cow in the future, the speak() call remains unchanged as its on interface ref var now
Isolate each animal call’s logic in their respective class and create a common stable interface Pet. This is how it looks like after refactor in Java:
interface Pet {
void speak();
}
class Dog implements Pet {
@Override
public void speak() { bark(); }
}
class Cat implements Pet {
@Override
public void speak() { meow(); }
}
class Duck implements Pet {
@Override
public void speak() { quack(); }
}
// usage
Pet pet = new Dog();
pet.speak();
// adding new animal will now require creating its own class and the speak() call remains unchanged
Law of Demeter (Principle of Least Knowledge): Don’t talk to strangers. Call methods of only “closely” related objects and not foo.bar.baz.qux when foo and qux aren’t closely related but rather chained. Any change in the chain can break things for others.
Tell, don’t ask: reminds that OOP is about bundling data with the functions that operate on that data (encapsulation). Rather than asking an object for data and acting on that data, we should instead tell an object what to do. Ex - instead of fetchng the bank balance, subtracting withdrawal amount and writing back resulting amount to an object, we should just call withdraw(amount) on the object.
YAGNI (You Ain’t Gonna Need It): avoid implementing features that “may” be required in future; think ahead but don’t build ahead.
KISS (Keep It Simple Stupid): the simplest solution that works is usually the right one; avoid unnecessary complexity.
DRY (Don’t Repeat Yourself): not only code duplication, but each significant piece of functionality should be implemented in just one place in the source code; applies to code comments, docs, API design, etc. as well. Resist the temptation to cut down coincidental duplication like this though!
Separation of Concerns: different parts of your code should handle different responsibilities, and they shouldn’t know about each other’s internals.
Minimise Coupling / Maximize Cohesion / Be Orthogonal: writing decoupled, independent components where a change in one does not cause side effects or break others.
Inversion of Control (IoC) (Hollywood Principle - “Don’t call us, we’ll call you"): an external framework or container controls the flow of execution, rather than your own custom application code. Dependency Injection is the concrete design pattern used to impl IoC. Frameworks like Spring, Angular, ASP.NET are based on this and have components called IoC Containers which perform DI.
Dependency Injection: an object receives its required services or dependencies from an external source rather than creating them internally.
Benefits:
// 1. makes dependencies explicit
class CheckoutService {
CheckoutService(Database db, PaymentProvider provider, Mailer mailer){ }
}
// 2. reduces coupling
interface PaymentProvider { }
class UPIProvider implements PaymentProvider { }
class StripeProvider implements PaymentProvider { }
class CheckoutService {
CheckoutService(Database db, PaymentProvider provider, Mailer mailer){ }
}
Dependency Injection is a way to impl the Dependency Inversion principle. The example above with interface PaymentProvider should make it clear as the class depends upon a abstraction there.
The injection is actually performed by a DI framework like Spring, Angular, ASP.NET.
The GoF design patterns are discussed at length here.
Structural Languages: structured into blocks that interact with each other. A method, a class, everything is a code block.
Functional Paradigm: follows Lambda Calculus, and functions are first-class citizens (function can be an object) which allows for Higher Order Functions (HOF) i.e. functions that take other function objects are arguments.
Programming Languages can be put into the following categories:
Based on variable’s type:
auto in C++ or var in Java, Go, TypeScript) or an explicitly defined type. Variable’s type must be known at compile-time.# python - dynamic typed
a = 5
print(type(a)) # int
a = "five"
print(type(a)) # str
// js - dynamic typed
let a = 5;
console.log(typeof a); // number
a = "five";
console.log(typeof a); // string
// java, c, cpp - static typed
int a = 5;
a = "five"; // invalid; compiler-error
// go - static typed, inference can make it look like dynamic typed but its not
var a int = 5
var b = 5
c := "five"
Based on variable’s value casting at runtime:
# python - strong typed
a = 5 + "five" # TypeError
// js - weak typed
let a = 5 + "five"; // valid
The lines are blurry with Strong and Weak typing. Examples:
int to boolean cast isn’t possible, either explicitly or implicitly. But C, C++, Python allows cast from other types to boolAlso Java has overloaded + operator by default that allows adding other types to a String! (an exception)
// java - strong typed, but one exception
String a = 5 + "five"; // valid
Summary:
Dynamic - variables don’t have fixed type, we can put literal of any type in it (no type checking at compile-time, often there is no compile-time xD)
Weak - assigned literal values to variables don’t have fixed type (can be implicitly casted at runtime depending on usage context)
C/C++ - static, strong (weaker than Java though)
Java - static, strong
Go - static, strong
TypeScript - static, strong
Python - dynamic, strong
JavaScript - dynamic, weak
PHP - dynamic, weak
Shell - dynamic, weak
“If it looks like a duck and quacks like a duck, it’s a duck”
Duck typing is a programming style where an object’s suitability for use is determined by the presence of specific methods and properties rather than its explicit class or inheritance.
Used in Python, Ruby, Objective-C, etc.
class Duck:
def talk(self):
print("Quack!")
class Person:
def talk(self):
print("Hello!")
def make_it_talk(entity):
# It does not check if entity is a Duck or Person
# It just tries to call .talk() because it "quacks" like a duck
entity.talk()
# Both work because both have a talk() method
make_it_talk(Duck()) # Output: Quack!
make_it_talk(Person()) # Output: Hello!
Threads in the same process share memory.
Other jargon here.
Concurrency Toolbox:
AtomicIntegersynchronized, ReentrantLockSemaphoreObject.wait() / notify()LinkedBlockingQueueProblems with concurrency:
Concurrency in Python: Python’s Global Interpreter Lock (GIL) is a mutex lock that allows only one thread to execute Python bytecode at a given time, thus CPU-bound code doesn’t benefit from threads, but I/O-bound code does. Use multiprocessing module for CPU parallelism which creates distinct OS process (each with its own independent Python interpreter and its own GIL).
Reference: https://www.hellointerview.com/learn/low-level-design/concurrency/intro