Search code examples
dartliskov-substitution-principle

How to implement LSP (Liskov Substitution Principle) using Dart, in a real world example


The Liskov Substitution Principle (LSP) states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. In other words, a subclass should be able to replace its superclass without breaking the code.

I'm trying to figure out how it could be a good implementation in a real world example, where we use the subclass everywhere in our code and only use the superclass on instantiation. This is my approach:

The first shot would be something like:

class User {
  const User({
    required this.id,
    required this.avatar,
  });

  final String id;
  final String avatar;
}

class UserWithName extends User {
  const UserWithName(
      {required super.id, required super.avatar, required this.name});

  final String name;

}

void main() {
  User userWithName = new UserWithName(id: '0', avatar: 'avatar', name: 'User Name');
  
  print('hello ${(userWithName as UserWithName).name}');
}

But for me it is ugly (nedding to use the as UserWithName), and in case we want a new functionality, not parameter, as it is been ugly resolved in previous example, one could do the example below to keep in LSP, but here we are breaking the open close principle:

class User {
  const User({
    required this.id,
    required this.avatar,
  });

  final String id;
  final String avatar;

  String? getName() => null;
}

class UserWithName extends User {
  const UserWithName(
      {required super.id, required super.avatar, required this.name});

  final String name;

  String getName() => this.name;
}

void main() {
  User user = new User(id: '0', avatar: 'avatar');
  User userWithName = new UserWithName(id: '0', avatar: 'avatar', name: 'User Name');

  print('hello ${user.getName()}');
  print('hello ${userWithName.getName()}');
}

Returns:

hello null
hello User Name

Solution

  • In the real world this principle is needed in various cases, when the superclass is a grand default, yet the subclasses have the same kinds of abilities, yet, the nature of their abilities varies from the grand default.

    Think of chess as a game and an engine that checks what the possible legal moves of a game might look alike. You may well have methods like

    • getValidMoves(...)
    • getValidKingMoves(...)
    • getValidQueenMoves(...)
    • getValidRookMoves(...)
    • getValidBishopMoves(...)
    • getValidKnightMoves(...)
    • getValidPawnMoves(...)

    and all these are implemented for chess. Now, you need to implement similar classes for variants of chess, like Fischer Random Chess, Chaturanga, where you also have a table of 8x8, White and Black pieces, roughly the same starting positions (the first row and the eight's row is randomized in case of Fischer Random Chess), but the rules are different and some of the moves need to be overriden. So you create subclasses, one for each variant and override the methods.

    Now, if your project is well-planned, then the engine asks for valid moves, a method that goes through all the pieces and for each piece it gets its valid moves. The results will be different, but the logic that asks for them and parses them will be the same.