SOLID - это набор принципов объектно-ориентированного программирования, которые помогают создавать гибкие, расширяемые и поддерживаемые системы. Рассмотрим каждый из этих принципов и приведем примеры их применения на языке Kotlin:
Принцип единственной ответственности (Single Responsibility Principle)
1. Принцип единственной ответственности (S) гласит, что каждый класс должен иметь только одну причину для изменения. То есть класс должен отвечать только за одну часть функциональности системы.
Пример:
class Car {
fun start() { /* ... */ }
fun stop() { /* ... */ }
}
class CarController {
fun startCar(car: Car) { car.start() }
fun stopCar(car: Car) { car.stop() }
}
В этом примере класс Car имеет только одну ответственность, а именно - запуск и остановка автомобиля. Класс CarController отвечает
за управление автомобилем, а не за его внутреннюю логику.
Принцип открытости/закрытости (Open/Closed Principle)
2. Принцип открытости/закрытости (O) гласит, что классы должны быть открыты для расширения, но закрыты для модификации. То есть, если требуется добавить новую функциональность, то необходимо расширять существующий класс, а не изменять его.
Пример:
open class Shape {
open fun draw() { /* ... */ }
}
class Rectangle : Shape() {
override fun draw() { /* ... */ }
}
class Circle : Shape() {
override fun draw() { /* ... */ }
}
В этом примере класс Shape является абстрактным классом, который определяет метод draw(). Классы Rectangle и Circle наследуют класс Shape и реализуют метод draw() в соответствии со своей логикой.
3. Принцип подстановки Барбары Лисков (L)
Принцип подстановки Барбары Лисков гласит, что объекты в программе должны быть заменяемы на экземпляры их подтипов без изменения корректности программы.
Пример:
open class Shape {
open fun draw() {
println("Drawing a shape")
}
}
class Circle : Shape() {
override fun draw() {
println("Drawing a circle")
}
}
class Rectangle : Shape() {
override fun draw() {
println("Drawing a rectangle")
}
}
fun drawAllShapes(shapes: List<Shape>) {
for (shape in shapes) {
shape.draw()
}
}
fun main() {
val shapes = listOf(Circle(), Rectangle())
drawAllShapes(shapes)
}
Здесь класс Shape является базовым классом для классов Circle и Rectangle. Метод drawAllShapes принимает список объектов класса Shape и вызывает для каждого из них метод draw.
4. Принцип разделения интерфейсов (I) гласит, что клиенты не должны зависеть от методов, которые они не используют. Вместо этого следует разбить интерфейсы на более мелкие, чтобы клиенты могли реализовывать только те методы, которые им нужны.
Пример:
Допустим, у нас есть интерфейс Shape для фигур, который содержит методы для вычисления площади и периметра:
interface Shape {
fun calculateArea(): Double
fun calculatePerimeter(): Double
}
Теперь допустим, что у нас есть класс Circle, который реализует интерфейс Shape и имеет дополнительный метод draw для рисования круга:
class Circle(val radius: Double): Shape {
override fun calculateArea() = Math.PI * radius * radius
override fun calculatePerimeter() = 2 * Math.PI * radius
fun draw() {
// рисование круга
}
}
Это нарушает принцип разделения интерфейсов, поскольку клиенты, которым нужно только вычисление площади и периметра, должны знать о методе draw, который для них не нужен.
Чтобы исправить это, мы можем создать отдельный интерфейс Drawable, который будет содержать метод draw:
interface Drawable {
fun draw()
}
Затем мы можем сделать класс Circle реализующим оба интерфейса:
class Circle(val radius: Double): Shape, Drawable {
override fun calculateArea() = Math.PI * radius * radius
override fun calculatePerimeter() = 2 * Math.PI * radius
override fun draw() {
// рисование круга
}
}
Теперь клиенты, которым нужно только вычисление площади и периметра, могут работать с объектами типа Shape, а клиенты, которым нужно рисование круга, могут работать с объектами типа Drawable. Таким образом, мы разделили интерфейсы на более мелкие, чтобы клиенты могли реализовывать только те методы, которые им нужны, и не зависеть от методов, которые они не используют.
5. Принцип инверсии зависимостей (D) гласит что высокоуровневые модули не должны зависеть от низкоуровневых модулей. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Пример:
Предположим, что у нас есть интерфейс для работы с базой данных, который определяет методы для выполнения операций чтения и записи в базу данных:
interface Database {
fun readData(): String
fun writeData(data: String)
}
И у нас есть класс DataProcessor, который зависит от этого интерфейса:
class DataProcessor(private val database: Database) {
fun processData() {
val data = database.readData()
// process data
database.writeData(data)
}
}
Затем мы создаем класс, который реализует интерфейс Database для работы с SQLite базой данных:
class SQLiteDatabase : Database {
override fun readData(): String {
// read data from SQLite database
}
override fun writeData(data: String) {
// write data to SQLite database
}
}
Это нарушает принцип инверсии зависимостей, так как класс DataProcessor зависит от реализации Database через конструктор. Вместо этого мы можем воспользоваться концепцией внедрения зависимостей (Dependency Injection, DI) и передать интерфейс Database через конструктор:
class DataProcessor(private val database: Database) {
fun processData() {
val data = database.readData()
// process data
database.writeData(data)
}
}
И теперь мы можем использовать класс DataProcessor с любой реализацией Database, которая реализует необходимые методы интерфейса. Например, мы можем создать класс, который реализует интерфейс Database для работы с InMemory базой данных:
class InMemoryDatabase : Database {
override fun readData(): String {
// read data from InMemory database
}
override fun writeData(data: String) {
// write data to InMemory database
}
}
И использовать его вместо класса SQLiteDatabase:
val database = InMemoryDatabase()
val dataProcessor = DataProcessor(database)
dataProcessor.processData()
Таким образом, мы соблюдаем принцип инверсии зависимостей, потому что класс DataProcessor зависит от абстракции (интерфейса Database), а не от конкретной реализации (класса SQLiteDatabase или InMemoryDatabase).