Приведи пример использования принципа Барбары Лисков
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Принцип Барбары Лисков (Liskov Substitution Principle)
Определение
Принцип подстановки Барбары Лисков — одна из основ SOLID. Он гласит: объекты подклассов должны корректно заменять объекты базовых классов без нарушения корректности программы. Если класс S является подтипом класса T, то объекты типа T в программе могут быть заменены на объекты типа S без каких-либо нежелательных побочных эффектов.
Сиными словами: если вы создали иерархию наследования, подкласс должен работать везде, где используется родительский класс.
Плохой пример (нарушение LSP)
// Базовый класс
abstract class Bird {
void fly();
}
// Реализация 1
class Eagle extends Bird {
@override
void fly() {
print("Eagle летит высоко");
}
}
// Реализация 2 — ПРОБЛЕМА!
class Penguin extends Bird {
@override
void fly() {
throw UnsupportedError("Пингвины не летают!");
}
}
// Использование
void makeBirdFly(Bird bird) {
bird.fly(); // Работает для Eagle, но ПАДАЕТ для Penguin!
}
// Вызов
Bird myBird = Penguin();
makeBirdFly(myBird); // Exception: Пингвины не летают!
Проблема: Penguin нарушает контракт Bird. Код, работающий с Bird, ломается при использовании Penguin.
Правильный пример (соблюдение LSP)
// Базовый класс для всех птиц
abstract class Bird {
void move();
String getSpecies();
}
// Летающие птицы
abstract class FlyingBird extends Bird {
void fly();
@override
void move() => fly();
}
// Нелетающие птицы
abstract class FlightlessBird extends Bird {
void walk();
@override
void move() => walk();
}
// Конкретные реализации
class Eagle extends FlyingBird {
@override
void fly() {
print("🦅 Eagle летит высоко в небо");
}
@override
String getSpecies() => "Eagle";
}
class Penguin extends FlightlessBird {
@override
void walk() {
print("🐧 Penguin вразвалку ходит по льду");
}
@override
String getSpecies() => "Penguin";
}
class Parrot extends FlyingBird {
@override
void fly() {
print("🦜 Parrot летит между ветками");
}
@override
String getSpecies() => "Parrot";
}
// Корректное использование
void makeBirdMove(Bird bird) {
bird.move(); // Работает ВСЕГДА, независимо от подкласса!
print("Это ${bird.getSpecies()}");
}
void flyBird(FlyingBird bird) {
bird.fly(); // Безопасно вызывать fly только для FlyingBird
}
void walkBird(FlightlessBird bird) {
bird.walk(); // Безопасно вызывать walk только для FlightlessBird
}
// Использование
List<Bird> birds = [
Eagle(),
Penguin(),
Parrot(),
];
for (var bird in birds) {
makeBirdMove(bird); // Работает корректно для всех!
}
Практический пример из Flutter
Widget иерархия
// Неправильно: Widget может быть StatefulWidget или StatelessWidget
// но они имеют разное поведение
// Правильно:
abstract class BaseWidget extends Widget {
final Color backgroundColor;
const BaseWidget({required this.backgroundColor});
@override
Element createElement();
}
class SimpleCard extends StatelessWidget implements BaseWidget {
@override
final Color backgroundColor;
final String title;
const SimpleCard({
required this.title,
this.backgroundColor = Colors.white,
});
@override
Widget build(BuildContext context) {
return Container(
color: backgroundColor,
child: Text(title),
);
}
@override
Element createElement() => StatelessElement(this);
}
class AnimatedCard extends StatefulWidget implements BaseWidget {
@override
final Color backgroundColor;
final String title;
const AnimatedCard({
required this.title,
this.backgroundColor = Colors.blue,
});
@override
State<AnimatedCard> createState() => _AnimatedCardState();
@override
Element createElement() => StatefulElement(this);
}
class _AnimatedCardState extends State<AnimatedCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: Duration(seconds: 1), vsync: this);
_controller.forward();
}
@override
Widget build(BuildContext context) {
return Container(
color: widget.backgroundColor,
child: ScaleTransition(
scale: _controller,
child: Text(widget.title),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Пример с Repository pattern
// Общий интерфейс
abstract class UserRepository {
Future<User> getUser(String id);
Future<void> saveUser(User user);
}
// Реализация 1: из сети
class NetworkUserRepository implements UserRepository {
final HttpClient _httpClient;
NetworkUserRepository(this._httpClient);
@override
Future<User> getUser(String id) async {
final response = await _httpClient.get('/users/$id');
return User.fromJson(response);
}
@override
Future<void> saveUser(User user) async {
await _httpClient.post('/users', body: user.toJson());
}
}
// Реализация 2: из локальной БД
class LocalUserRepository implements UserRepository {
final Database _db;
LocalUserRepository(this._db);
@override
Future<User> getUser(String id) async {
final result = await _db.query('users', where: 'id = ?', whereArgs: [id]);
if (result.isEmpty) throw Exception('User not found');
return User.fromMap(result.first);
}
@override
Future<void> saveUser(User user) async {
await _db.insert('users', user.toMap());
}
}
// Реализация 3: с кешем
class CachedUserRepository implements UserRepository {
final UserRepository _primary;
final Map<String, User> _cache = {};
CachedUserRepository(this._primary);
@override
Future<User> getUser(String id) async {
if (_cache.containsKey(id)) {
return _cache[id]!;
}
final user = await _primary.getUser(id);
_cache[id] = user;
return user;
}
@override
Future<void> saveUser(User user) async {
_cache[user.id] = user;
await _primary.saveUser(user);
}
}
// Использование: код работает со ВСЕМИ реализациями!
class UserService {
final UserRepository _repository;
UserService(this._repository);
Future<User> fetchUser(String id) {
return _repository.getUser(id);
}
}
// Инъекция зависимостей
void main() {
// Можем использовать любую реализацию!
final repo1 = NetworkUserRepository(httpClient);
final repo2 = LocalUserRepository(database);
final repo3 = CachedUserRepository(repo1);
final service = UserService(repo3); // Работает одинаково!
}
Ключевые принципы LSP
- Контракт базового класса должен быть выполнен подклассом
- Исключения не должны быть неожиданными
- Производительность не должна деградировать в подклассе
- Побочные эффекты должны быть предсказуемы
Как проверить LSP в своем коде?
// ✅ Хороший знак: вы можете заменить тип везде
UserRepository repo = CachedUserRepository(NetworkUserRepository(...));
UserRepository repo2 = LocalUserRepository(...);
// Оба работают одинаково!
// ❌ Плохой знак: нужны instanceof проверки
if (repo is CachedUserRepository) {
// Что-то специальное для кеша
} else if (repo is LocalUserRepository) {
// Что-то специальное для локала
}
Итоги
Принцип Барбары Лисков гарантирует, что код, работающий с базовым типом, будет работать корректно с любым подтипом. Это основа полиморфизма и позволяет писать гибкий, расширяемый код. В Flutter это особенно важно при работе с Repository pattern'ом, Widget'ами и Service слоями.