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 bool
Also 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
Everything is an object and every action must be performed on an object by an object’s methods.
Composition vs Aggregation: both are associations, former is a strong one, the 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 initizalized and/or instantiated, but such code need not be present in case of Aggregation.
For cleaner code, functions should be:
Helps create extensible, maintainable, and understandable code.
Reference: https://www.baeldung.com/solid-principles
Ex - Invoice
class can be split into the following three classes:
Invoice
class - main entityInvoicePrinter
class - to print entity values to the consoleInvoiceDAO
class - to persist entity values into a databaseCode:
class Invoice{
// members
long id;
int price;
// constructor
Invoice(long id, int price, int discount){
this.id = id;
this.price = price - (price * discount / 100);
}
// methods
void printInvoice(){
System.out.println("Invoice: " + id + ", and price = " + price);
}
void saveInvoiceToDB(){
// save to DB logic
}
}
Problem: The above class can change because of multiple reasons 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 is:
class Invoice{
// members
long id;
int price;
// constructor
Invoice(long id, int price, int discount){
this.id = id;
this.price = price - (price * discount / 100);
}
}
class InvoicePrinter{
Invoice invoice;
InvoicePrinter(Invoice invoice){
this.invoice = invoice;
}
void print(){
System.out.println("Invoice: " + invoice.id + ", and price = " + invoice.price);
}
}
class InvoiceDao{
Invoice invoice;
InvoiceDao(Invoice invoice){
this.invoice = invoice;
}
void saveToDB(){
// save to DB logic
}
}
// in main()
Invoice invoiceObj = new Invoice(1L, 100, 50);
InvoicePrinter invoicePrinterObj = new InvoicePrinter(invoiceObj);
invoicePrinterObj.print();
In the example above, we can also avoid composition and supply Invoice
object directly as a method parameter void print(Invoice invoiceObj){ }
just as we do in Controller, Service, and Repository layers in SpringBoot appication.
Ex - implement new functionality so that we can save invoices to a file in a filesystem (FS) as well
class InvoiceDao{
Invoice invoice;
InvoiceDao(Invoice invoice){
this.invoice = invoice;
}
void saveToDB(){
// save to DB logic
}
void saveToFile(){ // added new method in existing class (VIOLATION!)
// save to file logic
}
}
class InvoiceDaoFS extends InvoiceDao{
Invoice invoice;
InvoiceDao(Invoice invoice){
this.invoice = invoice;
}
void saveToFile(){ // added new method in new class extending existing DAO
// save to file logic
}
}
Ex - Penguin is a technically a Bird, but it is 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 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’ll return NullPointerException
when instance member engine
is accessed as engine == null
for ElectricCar
instance.
Liskov Substitution is also about “making sense” rather than our code breaking if it’s violated (narrow down superclass). A subclass inherits all members from its parent so it is a valid substitute for its parent, but as we saw in the penguin example above, it doesn’t make any sense to treat a penguin as a “normal” bird. Or an electric car as a “normal” Car.
Other extreme examples - Hotdog as a Dog, Rubber Duck as a Duck, etc. We can totally do it if the superclasses are empty, but it doesn’t make any sense.
Additionally, there is nothing really stopping us from inheriting “Burger” from “Metal” class even if they both aren’t empty, but it should make sense too.
interface
for each distinct functionality and later provide their respective implementation. By such fine-grained splitting, we won’t need to provide impl to unrequired interface methods in the impl concrete class.// 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!
}
}
Simply put, when components of our system have dependencies, we don’t want directly inject a component’s dependency (concrete class
) into another. Instead, we should use a level of abstraction (interface
) between them.
// 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 { }
YAGNI: You Ain’t Gonna Need It (avoid implementing features that “may” be required in future)
KISS: Keep It Simple Stupid
DRY: Don’t Repeat Yourselves (not only line duplication, but each significant piece of functionality should be implemented in just one place in the source code)
Hollywood Principle: “Don’t call us, we’ll call you.” (another name for Inversion-of-Control)
Minimise Coupling, Maximize Cohesion, Be Orthogonal (independent)
Encapsulate what varies:
if (pet.type() == dog) {
pet.bark();
} else if (pet.type() == cat) {
pet.meow();
} else if (pet.type() == duck) {
pet.quack()
}
// instead we can just write
pet.speak();
Program against abstractions: program by keeping interfaces and their relations and interactions in mind. Don’t take concrete classes into consideration while designing.
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.
Composition over Inheritance: prefer composition rather than inheritance; because it is much less rigid and can be changed later, composition also allows multiple inheritance like relation which Java doesn’t allow with concrete classes (diamond problem).
// an Employee is a Person, and a Manager is both
// with inheritance
class Person { }
class Employee extends Person { }
class Manager extends Person, Employee { } // can't do this; multiple-inheritance
class Manager extends Employee { } // so we do this
// with composition
class Person { }
class Employee {
Person p; // Employee has Person object
Employee(Person p){
this.p = p;
}
}
Person p = new Person();
Employee e = new Employee(p);
class Manager {
Person p;
Employee e; // Manager has both Employee and Person objects
Manager(Person p, Employee e) {
this.p = p;
this.e = e;
}
}
Manager m = new Manager(p, e);