Home  >  Article  >  Java  >  Applying the \"Liskov Substitution Principle\" with Typescript and Java

Applying the \"Liskov Substitution Principle\" with Typescript and Java

PHPz
PHPzOriginal
2024-08-30 06:01:32798browse

Aplicando o

Concepts

Interfaces

An interface defines a contract or a set of methods and properties that a class must implement. Interfaces are used to ensure that a class follows a certain format, but they do not provide implementation of methods, only their signatures.

Whenever a class implements an interface, it signs all contracts (methods and attributes) for the interface. Each attribute and method is mandatorily implemented.

SOLID

SOLID is an acronym that represents five fundamental principles of object-oriented programming, proposed by Robert C. Martin - Uncle Bob. Here you can read more about his article.
These principles aim to improve the structure and maintenance of code, making it more flexible, scalable, and easier to understand. Such principles help the programmer to create more organized codes, dividing responsibilities, reducing dependencies, simplifying the refactoring process and promoting code reuse.

About LSP

The "L" in the acronym stands for "Liskov Substitution Principle". The phrase that uncle bob used to define this principle was:

"Derived classes must be able to completely replace base classes"

It is therefore suggested that the derived class should be as close as possible to the base class, so that the derived class can replace its base class without any modification in the code.

This principle was introduced by Barbara Liskov in 1988, based on the theory of data abstraction and types. Derived from the concept of Design by Contracts (DBC), popularized by Bertrand Meyer in 1986.

Another specification of this principle is:

The subtype should be used as your base type without any surprises.

In programming, changes and surprises can cause problems. If a system feature needs to be replaced, the new one must provide the same type of information, or the system may fail. To ensure that class S has the same behavior as base class T, it is essential to use a contract (interface or abstract class) that defines the mandatory methods for implementing the new functionality, in order to guarantee the integrity of the similarity between class S and class T.

Practical application

Consider a Bird base class with a fly() method that will be used in two child classes: Sparrow and Ostrich.

File: bird.java

class Bird {
    void fly() {
        System.out.println("I can fly!");
    }
}

class Sparrow extends Bird {
    // Herda o comportamento de 'fly' da classe 'Bird'
}

class Ostrich extends Bird {
    @Override
    void fly() {
        throw new UnsupportedOperationException("I cannot fly");
    }
}

File: bird.ts

class Bird {
  fly() {
    console.log("I can fly!");
  }
}

class Sparrow extends Bird {}

class Ostrich extends Bird {
  fly() {
    throw new Error("I cannot fly");
  }
}

Problems encountered

Here, the Sparrow class adheres to LSP because sparrows can indeed fly. However, the Ostrich class violates the LSP because it overrides the voo() method in a way that fundamentally alters its behavior, breaking the expectations set by the Ave class.

How to fix?

We will need to apply LSP by dividing each specificity of the Sparrow and Ostrich classes into contracts (interfaces or abstract classes, here I will use interfaces) that they must sign to modulate the behaviors of each:

File: bird.java

interface Bird {
    String getName();
    void makeSound();
}

interface FlyingBird extends Bird {
    void fly();
}

class Sparrow implements FlyingBird {
    private String name;

    public Sparrow(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void makeSound() {
        System.out.println("Chirp chirp!");
    }

    @Override
    public void fly() {
        System.out.println(this.name + " is flying!");
    }
}

class Ostrich implements Bird {
    private String name;

    public Ostrich(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void makeSound() {
        System.out.println("Boom boom!");
    }
}

public class Main {
    public static void main(String[] args) {
        Sparrow sparrow = new Sparrow("Little Sparrow");
        sparrow.makeSound(); // Chirp chirp!
        sparrow.fly(); // Little Sparrow is flying!

        Ostrich ostrich = new Ostrich("Ostrich");
        ostrich.makeSound(); // Boom boom!
        ostrich.fly(); // Error: Method 'fly' does not exist on 'Ostrich'
    }
}

File: bird.ts

interface Bird {
  name: string;
  makeSound(): void;
}

interface FlyingBird extends Bird {
  fly(): void;
}

class Sparrow implements FlyingBird {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  makeSound() {
    console.log("Chirp chirp!");
  }

  fly() {
    console.log(`${this.name} is flying!`);
  }
}

class Ostrich implements Bird {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  makeSound() {
    console.log("Boom boom!");
  }
}

const sparrow = new Sparrow("Little Sparrow");
sparrow.makeSound(); // Chirp chirp!
sparrow.fly(); // Little Sparrow is flying!

const ostrich = new Ostrich("Ostrich");
ostrich.makeSound(); // Boom boom!
ostrich.fly(); // Error: Method 'fly' does not exist on 'Ostrich'

Analysis

Correction Explanation
Bird Interface: Defines behaviors common to all birds, such as makeSound(). All birds must implement this interface.

FlyingBird Interface: Inherits from Ave and adds the fly() behavior, which is specific to birds that can fly.

Sparrow Class: Implements the FlyingBird interface, since sparrows can fly. This class defines the behavior of both emitting sound and flying.

Ostrich Class: Only implements the Bird interface, as ostriches cannot fly. This class does not have the fly() method and therefore does not violate LSP.

Conclusion

LSP is crucial to ensuring that code is modular, reusable, and easy to maintain. LSP violations can lead to fragile code that breaks when new subclasses are introduced or when existing subclasses are modified, as this can lead to unexpected behavior in parts of the code that depend on the superclass.

Subtype substitution allows a module to be extended without modification, essential for the flexibility provided by the Open/Closed Principle (OCP), made possible by the Liskov Substitution Principle. Contracts (implemented through interfaces or abstract classes) are crucial for secure design, but they must be well understood by programmers, helping to avoid common errors in legacy software. They also provide valuable guidance on how to implement and use the code, simply by observing the contract in question.

Practical Implications

  1. When designing subclasses, make sure they can be used wherever their superclass is used, without introducing bugs or requiring special handling.
  2. Avoid creating subclasses that violate the expected behavior of the superclass, as this can lead to maintenance issues and unexpected bugs.

Understanding and applying the Liskov Substitution Principle helps developers create more predictable and stable object-oriented systems.

The above is the detailed content of Applying the \"Liskov Substitution Principle\" with Typescript and Java. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn