Принцип замены Лискова (LSP) гласит, что объекты суперкласса должны иметь возможность заменяться объектами подкласса, не влияя на корректность программы. Другими словами, подкласс должен иметь возможность заменять свой суперкласс, не нарушая при этом код.
Я пытаюсь понять, как это может быть хорошей реализацией в реальном примере, где мы используем подкласс повсюду в нашем коде и используем суперкласс только при создании экземпляра. Это мой подход:
Первый выстрел будет примерно таким:
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}');
}
Но для меня это некрасиво (необходимо использовать as UserWithName), и в случае, если нам нужна новая функциональность, а не параметр, как это было некрасиво решено в предыдущем примере, можно было бы сделать пример ниже, чтобы сохранить в LSP, но здесь мы нарушаем принцип открытия-закрытия:
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()}');
}
Возврат:
hello null
hello User Name
Во втором примере мы добавили getName в суперкласс.
О, кажется, теперь я понимаю, что вы имеете в виду: второй пример нарушает OCP, потому что для добавления нового именованного класса/функциональности вы изменили родительский класс.
Да, это то, что я имею в виду.
В реальном мире этот принцип необходим в различных случаях, когда суперкласс является великим по умолчанию, но подклассы имеют одинаковые виды способностей, но природа их способностей отличается от великого по умолчанию.
Думайте о шахматах как об игре и механизме, который проверяет, как могут выглядеть возможные допустимые ходы в игре. У вас вполне могут быть такие методы, как
и все это реализовано для шахмат. Теперь вам нужно реализовать аналогичные классы для таких вариантов шахмат, как случайные шахматы Фишера, Чатуранга, где у вас также есть таблица 8x8, белые и черные фигуры, примерно с одинаковыми стартовыми позициями (первая строка и восьмерка рандомизированы в случай случайных шахмат Фишера), но правила другие, и некоторые ходы необходимо изменить. Итак, вы создаете подклассы, по одному для каждого варианта, и переопределяете методы.
Теперь, если ваш проект хорошо спланирован, движок запрашивает допустимые ходы, метод, который проходит через все части и для каждой части получает допустимые ходы. Результаты будут разными, но логика, которая их запрашивает и анализирует, будет одинаковой.
Спасибо за ответ :-), вопрос, который я хочу обсудить, следуя вашему сценарию: допустим, в Чатуранге король имеет право убить все фигуры вокруг при выполнении условия X, эта функциональность применима только к подклассу Чатуранга, как продолжим ли мы следовать LSP, не нарушая OCP?
@Daniel Тогда это будет просто реализация getValidKingMoves
в классе Чатуранга.
Я имею в виду новую функцию, выходящую за рамки суперкласса.
@Daniel, ты говоришь о новом методе в подклассе, но не в базовом классе. Если вы хотите сохранить этот принцип, то вы реализуете его фиктивным образом для базового класса, все подклассы унаследуют его как поведение по умолчанию, и вы переопределите это для Чатуранги.
@LajosArpad да, именно об этом я и говорил в своем примере: если через некоторое время мне понадобится реализовать новый метод только для подкласса, для поддержки LSP нам нужно сломать OCP. Очевидно, я пытался найти элегантный способ избежать законов Вселенной, большое спасибо за ваши ответы, они очень помогают мне лучше понять.
@Daniel, когда вам нужно создать newMethod() для подкласса, вы создаете его и для базового класса, поэтому принцип не нарушается. Этот принцип не обязательно должен соблюдаться, он не высечен на камне, но в примерах, когда методы базового класса и подкласса очень похожи, имеет смысл его сохранить.
Наконец, правильная реализация, учитывающая LSP и OCP, будет такой:
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;
String getName() => this.name;
}
void main() {
User user = new User(id: '0', avatar: 'avatar');
UserWithName userWithName =
new UserWithName(id: '0', avatar: 'avatar', name: 'User Name');
_printUserWithName(UserWithName userWithName) {
print('hello ${userWithName.getName()}');
}
if (user.runtimeType == UserWithName) {
_printUserWithName(userWithName);
}
if (userWithName.runtimeType == UserWithName) {
_printUserWithName(userWithName);
}
}
Ответ:
hello User Name
Почему второй пример нарушает принцип открытости-закрытости?