From Class-based Hero to Functional Ninja

The ugly truth about Class-based Object Oriented Programming and Encapsulation.

by Gion Kunz

Gion Kunz

  • Angular GDE since 2018
  • Front-end developer, web standards advocate
  • Specialized in spa development using Angular
  • Onsite ramp-ups, coaching and implementation support in projects
  • Author of the book "Mastering Angular Components"
  • Frequent speaker at conferences and meetups
  • Teacher for web development and UX engineering at the SAE Institute in Zurich
  • Tutor at O'Reilly Safari Online Training
  • Contributor on the Angular project, author of Chartist.js library

Founder, UI Developer, UX Enthusiast at syncrea GmbH

Typescript is pretty strong without Classes...

Union Literal Types

type FibonacciNumber = 1 | 2 | 3 | 5 | 8 | 13;
// Error!
const n: FibonacciNumber = 4;

String Literal Union Types as Enumeration Type

type OrderStatus = 'Ordered' | 'Paid' | 'Shipped' | 'Delivered';

interface Order {
  id: number;
  status: OrderStatus;
}

const order: Order = {
  id: 1,
  status: 'Ordered'
};

Intersection Types

interface Cat {
  name: string;
  meow: () => void;
}

interface Dog {
  name: string;
  bark: () => void;
}

export type CatDog = Cat & Dog;

Discriminated Union Types

export interface Apple {
  readonly kind: 'Apple';
  readonly color: string;
}

export interface Banana {
  readonly kind: 'Banana';
  readonly bendRadius: number;
}

export type Fruit = Apple | Banana;

Classes make sense, until you meet the Platypus...

What's Wrong with OOP and Encapsulation?

  • What intuitively made sense, to combine data and behaviour into Objects, is actually very impractical for composition.
  • Mostly the types with behaviors metaphor is not taking you that far.
  • We have learned that Inheritance is a bad thing to begin with and we should avoid it.
  • Implementing behavior classes and using composition is suggested but it's just a work-around.
  • Classes are rigid constructs and Mixins / Traits are recommended to add more flexibility, but this is also just a workaround :-)
    https://scg.unibe.ch/archive/papers/Scha03aTraits.pdf

Interfaces with Object Literals

export interface Person {
  readonly firstName: string;
  readonly lastName: string;
}

const person: Person = {
  firstName: 'Peter',
  lastName: 'Griffin'
};

Separate Model from Behavior

  • Use interfaces to define your model / data.
  • If too much boilerplate for construction, use object factory functions.
  • Implement any logic outside your model using pure helper functions.

Problems with class based inheritance

  • Rigidity: Changes in the base class can force unwanted changes in derived classes, making the system inflexible.
  • Tight Coupling: Derived classes are tightly coupled to their base classes, leading to a higher risk of code changes propagating through the hierarchy.
  • Code Duplication: Similar functionality may need to be duplicated across different branches of the hierarchy if it doesn’t fit neatly into the existing structure.
  • Misplaced Responsibilities: Methods and properties can end up in inappropriate classes due to the need to share functionality across the hierarchy.
  • Limited Extensibility: Adding new features that cross-cut existing hierarchies can be difficult without major restructuring.
  • Violation of SOLID Principles: Inheritance can lead to violations of the Single Responsibility Principle and Liskov Substitution Principle.
  • Testing Challenges: Mocking, stubbing, and dealing with shared state in an inheritance hierarchy can complicate testing.

The Multiple Inheritance Dilemma

export class DesigningDeveloper {
  design(): void {
    // Can design
  }

  develop(): void {
    // Can develop
  }
}

You don't need to know Currying, functors, Monads and Monoids to do FP style....

Ok, nice, but what about those perfect OOP Problem domains?

Some problems seem to scream O.O.P.!

Example: Create a graphics application with Shapes that can be rendered, manipulated, grouped etc.

How to achieve similar Extensibility like with OOP.

  • Higher-Order Functions
  • Polymorphic Functions
  • Type Classes (mimicking behavior of Haskell / Scala)

Higher-Order Functions

// Factory function that creates a logger function with a specified prefix
const createLogger = (prefix: string): ((message: string) => void) => {
  return (message: string) => {
    console.log(`${prefix}: ${message}`);
  };
};

// Usage
const infoLogger = createLogger('INFO');
const errorLogger = createLogger('ERROR');

infoLogger('This is an info message.');
// Output: INFO: This is an info message.

errorLogger('This is an error message.');
// Output: ERROR: This is an error message.

Polymorphic Functions

const getMax = <T>(
  arr: T[], 
  compare: (a: T, b: T) => number): T | undefined => {

  if (arr.length === 0) return undefined;
  return arr.reduce((max, item) => (compare(max, item) > 0 ? max : item));
};

const numberCompare = (a: number, b: number): number => a - b;

// Usage
const numbers = [3, 1, 4, 1, 5, 9];
const maxNumber = getMax(numbers, numberCompare);
console.log(maxNumber); // Output: 9

Type Classes Mimicking with Registry

type Show<T> = { show: (item: T) => string; };
const showRegistry: Record<string, Show<any>> = {};

// Register Show implementations
showRegistry['string'] = { show: (item: string) => `String: ${item}` };
showRegistry['number'] = { show: (item: number) => `Number: ${item}` };

const showItem = (type: string, item: any): string =>
  showRegistry[type]?.show?.(item);

console.log(showItem('string', 'Hello')); // Output: String: Hello
console.log(showItem('number', 42));      // Output: Number: 42

Graphics Application Example

Let's create together.

Thanks!