Is it required that all functions calling a method on an object exhibit identical behavior in terms of the properties they modify as per Liskov's substitution principle?

I'm grappling with understanding the Liskov's Substitution Principle, particularly when it comes to the classic rectangle and square example. The concept of having a base rectangle class and a square class that inherits from it just isn't clicking for me. In this scenario, the setWidth/setHeight methods in the square class are implemented such that setting the width also changes the height to match it.

However, I can't see why this would be a problem. Wouldn't it make sense for a square to have uniform width and height? Moving on, my confusion extends to whether this principle applies to a more complex vehicle/car/plane structure that I've outlined below.

TLDR: I have a car and a plane inheriting the abstract move() method from the Vehicle class. The car increases its x and y location, while the plane adds movement along the z-axis as well. Does this violate the Liskov's Substitution Principle if a function taking a Vehicle class is called to execute the move action? And if so, what makes this a flawed design choice?

I start with an abstract Vehicle class

abstract class Vehicle {
    private wheels;
    private make;
    private seats;
    abstract honk(){};
    abstract move(){};
}

Followed by subclasses for Car and Plane

Car

   class Car extends Vehicle {
    private location: Location2d;

    public honk() {
      console.log(this.getMake() + " IS HONKING");
    } 
    public move(){
        this.location.x++;
        this.location.y++;  
    }
  }
  export default Car;

Plane

class Plane extends Vehicle implements FlyingVehicle {

    private maxAltitude;
    private location: Location3d;
   

    public honk() {
      console.log(this.getMake() + " is HONKING")
    }
    
    public move(){
       this.location.x++;
       this.location.y++;
       this.location.z++;
    }

  }

If there's a function iterating through an array of vehicles calling move on each one, does this go against the Liskov's Substitution Principle?

Answer №1

In my opinion, Square, Triangle, Car, and Bicycle are not the best examples for object-oriented programming (OOP). I have never come across them in actual code, making it challenging to discuss this topic using these metaphors.

It's rare to see code that controls both planes and cars at the same time ;)

The substitution principle primarily pertains to types. Any subclass method should be callable by something that can call the main class, and the type returned should align with what was returned from the parent.

The principle does not concern itself with side effects. It is possible to implement a new subclass that doesn't move the vehicle at all, opting for a completely different action instead. As long as the types are logical, it is considered valid.

Personally, I believe that a move method implemented by either subclass should align with the original method's intent, focusing more on sound design principles rather than strictly adhering to Liskov's guidelines.

Answer №2

Suppose I have a function that loops through an array of vehicles and then invokes the move function on each one. Would this violate Liskov's substitution principle?

No, this scenario would not break the Liskov substitution principle. (However, it doesn't necessarily mean it's good code either.)

The important point to note is that LSP focuses on the interface of a method - its parameters and return values. Subclasses must accept any parameters accepted by the parent class and may accept additional values rejected by the parent. Similarly, subclasses must return values that the parent could return but are not restricted to returning all possible values that the parent might have returned.

The internal logic of the method can change, leading to different behaviors or side effects.

Let me illustrate why adhering to these principles matters with an example:

Consider a HR software system with an employee class containing a method calculatePaycheque(month: int), which returns the monthly salary amount as an integer. This method throws an error for months less than 1 or greater than 12, calculating the pay by dividing the annual salary by 12. It is used in various scenarios to predict payroll expenses and actually process payments at month end.

Now, imagine some employees are seasonal workers who don't work or get paid during winter but receive performance-based bonuses for working months. To handle this effectively, we create a new 'seasonalEmployee' class that overrides the calculatePaycheque() method. In this subclass, from March to September, the payment consists of a portion of the yearly salary plus a bonus.

If we fail to follow the LSP, we might be tempted to modify the return type to an object like { fixedAmount: int, bonus: int }. Since seasonal workers operate for 7 months, we could conditionally throw errors outside the valid range. However, this deviation could cause issues when integrating seasonal employees into existing payroll processes, requiring extra validation checks and complicating the overall system.

By maintaining LSP guidelines, we ensure that both input (month) and output (payment) remain consistent across classes. This approach minimizes the need for extensive code modifications when incorporating new subclass behavior.

Despite internal modifications, adherence to LSP allows seamless integration of new functionalities without disrupting existing code compatibility.

Answer №3

The scenario of the Square/Rectangle example showcasing the usage of setWidth/setHeight highlights a potential issue related to inheritance. The assumption is made that

  • The base class Rectangle provides setWidth and setHeight methods specifically intended to modify only their corresponding attributes, and
  • The subclass Square maintains an invariant where the width always equals the height.

With these requirements in mind, one must either breach the Liskov substitution principle (by not following the contract outlined for the setWidth and setHeight methods) or compromise the subclass's invariant (by permitting the width to differ from the height).

Nevertheless, if the contract of the Rectangle class does not explicitly state that setWidth should exclusively alter the width (similarly for setHeight and the height), then the Square class can safely override these methods without violating the base class's contract, ensuring harmony. This aligns with scenarios like the Car and Plane analogy; it is likely that the Vehicle base class has no stipulation regarding the move method impacting the z position, thus Plane.move adheres to all expectations without infringing upon the Liskov substitution principle.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

I require a duplicate of the original data. Direct references are unnecessary

I am trying to extract the value of data.notes. After implementing the code below, I noticed that the detailsOfCurrentNotes value changes based on the data.notes. Can anyone provide guidance on how to resolve this issue? notes :Note[] const detailsOfCur ...

A method for converting variables into various data types within a template

I have developed an Angular app where I have configured the following: "angularCompilerOptions": { "strictInjectionParameters": true, "fullTemplateTypeCheck": true, "strictTemplates": true } As a res ...

Ways to navigate to a different component while passing along the specific chosen information

After clicking on a specific card, it routes to a different component, but I also want to pass the data of that card to the new component. You can check out a working demo on StackBlitz here. ...

Interface with several generic types

In an attempt to create a parser that can parse data fields and convert them into a complete form for display purposes, the fields property plays a crucial role. This property will define each field in a JSON data array that the client receives from the ur ...

Pass an array of objects to an Angular 8 component for rendering

Recently, I started working with Angular 8 and faced an issue while trying to pass an array of objects to my component for displaying it in the UI. parent-component.ts import { Component, OnInit } from '@angular/core'; @Component({ selector: ...

Attempting to create a TypeScript + React component that can accept multiple types of props, but running into the issue where only the common prop is accessible

I am looking to create a component named Foo that can accept two different sets of props: Foo({a: 'a'}) Foo({a: 'a', b: 'b', c:'c'}) The prop {a: 'a'} is mandatory. These scenarios should be considered i ...

Tips for creating more organized express routers while incorporating helper functions

When it comes to organizing controllers in separate files and utilizing OOP, one common question arises: how should helper functions be separated? Specifically, I'm referring to those functions created to enhance code cleanliness that are only used on ...

Is it necessary to reload the page each time to see updates on the navbar in nextjs?

I recently developed a Next.js application with a Navbar component integrated into my layout.tsx file. The challenge arises when a user logs in and is redirected to the home page, which showcases a link in the Navbar for viewing their profile. However, I n ...

Ways to increase the number of responses in an Express app after the initial response

In order to comply with the Facebook messenger API requirements, a 200 response must be sent immediately upon receiving the webhook request on my server, within 20 seconds. However, this process may take longer than the execution time of other middleware f ...

Setting the default theme in Material UI4

I am attempting to apply a default theme to the makeStyles function in material ui 4. Within my component, I have imported my theme from Styled Components and passed it to customMaterialStyles for makeStyles. The main component import { faTimes } from &a ...

Validation with React Hooks does not function properly when used on a controlled component

I've recently started using react hook form and I've created a custom component based on material ui's autocomplete. The issue I'm facing is that react hook form isn't validating the field at all. Let me show you how the setup look ...

Substitute terms in a sentence according to the guidelines

Looking to transform strings based on specific rules? "Hello {{firstName}}, this is {{senderName}}." Consider the following rules: rules = { firstName: "Alex", senderName: "Tracy" } The expected output would be: "Hello Alex, this is Tracy." If yo ...

Using an Object as a Key in Maps in Typescript

I had the intention of creating a Map object in Typescript where an object serves as the key and a number is the value. I attempted to define the map object in the following manner: myMap: Map<MyObj,number>; myObj: MyObj; However, when I tried to a ...

Using the -t or --testNamePattern in Jest will execute all tests

Currently, I have set up my testing framework using jest and ts-jest based on the guidelines provided by the ts-jest documentation. When I execute the command yarn test --listTests, I can identify the specific test file I intend to run: processNewUser.ts ...

Error encountered while installing node modules within an angular workspace

Currently, I am facing an issue with my workspace where the command npm install is giving me a series of errors that I cannot seem to resolve. I have tried running it as an admin, manually deleting the node_modules folder, asking for help from a senior col ...

Using React and TypeScript together with complex types in the UseContext feature may encounter issues

I successfully implemented the ThemeContext as shown in this link, but it only handles one field. I attempted to adapt this concept for a more complex UserContext, where the state is being set but not reflected on the page. You can view the example on Co ...

Error loading personalized SVG on mat-icon

I have been attempting to load a custom SVG using the MatIconRegistry within my component. Here is the code snippet I am trying: constructor(fb: FormBuilder, private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer) { thi ...

The import component functions correctly when it is located in the app folder, but does not work when it is installed as

I have a situation with an angular 2 component. When I place it in app-name/src/app/component-folder/component.ts and import it as import {Component} from './component-folder/component', everything works perfectly fine. However, if I install the ...

A guide on incorporating and utilizing third-party Cordova plugins in Ionic 5

Attempting to implement this plugin in my Ionic 5 application: https://www.npmjs.com/package/cordova-plugin-k-nfc-acr122u I have added the plugin using cordova plugin add cordova-plugin-k-nfc-acr122u but I am unsure of how to use it. The plugin declares: ...

What makes TypeScript's addition functionality so quirky?

I'm puzzled by the strange behavior of TypeScript when adding parameters. const getDir = (lastIndex: number) => { // my other code console.log(lastIndex + 10) // result is 1010 } getDir(10); The output displays 1010 instead of 20. Any suggestion ...