Что такое принципы SOLID?
SOLID — это аббревиатура, обозначающая пять принципов дизайна, которые позволяют создавать сопровождаемое, масштабируемое и надежное ПО. Роберт К. Мартин ввел эти принципы, чтобы помочь программистам писать высококачественный код. Изначально предназначенные для объектно-ориентированного программирования, принципы SOLID применимы и к другим языкам, например Kotlin. Они направлены на обеспечение чистоты кода и улучшение дизайна программного обеспечения.
Вот 5 принципов SOLID:
- Single Responsibility Principle (SRP) — принцип единственной ответственности.
- Open-Closed Principle (OCP) — принцип открытости/закрытости.
- Liskov Substitution Principle (LSP) — принцип подстановки Лисков.
- Interface Segregation Principle (ISP) — принцип разделения интерфейса.
- Dependency Inversion Principle (DIP) — принцип инверсии зависимостей.
Теперь рассмотрим подробнее каждый из этих принципов с примерами выполнения и нарушения.
Принцип единственной ответственности
Принцип единственной ответственности является одним из принципов SOLID в объектно-ориентированном программировании. Смысл его заключается в том, что для каждого конкретного класса должно быть определено только одно назначение. Другими словами, каждый класс должен иметь только одну обязанность или одно задание.
SRP полезен для поддержания классов и функций, поскольку позволяет сохранить их организованность и легкость их восприятия. Определяя классу несколько обязанностей, вы непреднамеренно можете повлиять на решение этим классом других задач, что приведет к неожиданному поведению, ошибкам и увеличению затрат на сопровождение.
Теперь рассмотрим ситуации нарушения и выполнения этого принципа.
Нарушение:
// Нарушение принципа единственной ответственности
// В данном примере класс System пытается одновременно обрабатывать множество различных ситуаций.
// Такой подход может привести к серьезным проблемам в будущем.
class SystemManager {
fun addUser(user: User) { }
fun deleteUser(user: User) { }
fun sendNotification(notification:String) {}
fun sendEmail(user: User, email: String) {}
}
В данном примере класс System пытается обработать множество различных ситуаций в одном и том же месте. Такой подход может привести к серьезным проблемам в будущем.
Выполнение:
// Выполнение принципа единственной отвественности.
// Как видно из этого примера, мы разделили класс System на определенные части
// и поместили функции в соответствующие классы.
class MailManager() {
fun sendEmail(user: User, email: String) {}
}
class NotificationManager() {
fun sendNotification(notification: String) {}
}
class UserManager {
fun addUser(user: User) {}
fun deleteUser(user: User) {}
}
Как видно из этого примера, класс System разделен на определенные части и функции помещены в соответствующие классы.
Принцип открытости/закрытости
Принцип “открытости/закрытости” — это правило объектно-ориентированного программирования, согласно которому классы, модули, функции и другие программные объекты должны быть открыты для расширения, но закрыты для изменений. Это означает, что вы можете добавлять новые элементы в класс, не модифицируя его исходный код. Таким образом, вместо изменения самого класса для добавления новых возможностей можно написать новый код, использующий существующий класс. Вследствие этого, код становится более удобным для сопровождения и повторного использования.
Теперь рассмотрим ситуации нарушения и корректного выполнения этого принципа.
Нарушение:
// Нарушение приницпа открытости/закрытости
// В данном примере, когда мы пытаемся добавить что-то новое в класс,
// приходится переписывать существующий код, что в дальнейшем может привести к проблемам.
class Shape(val type: String, val width: Double, val height: Double)
fun calculateArea(shape: Shape): Double {
if (shape.type == "rectangle") {
return shape.width * shape.height
} else if (shape.type == "circle") {
return Math.PI * shape.width * shape.width
}
return 0.0
}
В данном примере, для добавления чего-то нового в класс существующий код переписывается, что в дальнейшем может привести к проблемам.
Выполнение:
// Выполнение принципа открытости/закрытости.
// Вместо изменения самого класса этот принцип
// предусматривает написание новых классов, используя существующий класс,
// и реализацию функций в новых классах.
interface Shape {
fun area(): Double
}
class Rectangle(val width: Double, val height: Double) : Shape {
override fun area() = width * height
}
class Circle(val radius: Double) : Shape {
override fun area() = Math.PI * radius * radius
}
fun calculateArea(shape: Shape) = shape.area()
Здесь, вместо того чтобы менять существующий класс, были написаны новые классы с его использованием и в этих новых классах реализованы функции.
Принцип подстановки Лисков
Принцип подстановки Лисков — важное правило объектно-ориентированного программирования. Согласно ему, если у вас есть программа, работающая с объектом определенного типа, вы должны иметь возможность использовать любой подтип этого объекта. Таким образом, все методы и свойства базового класса должны работать и для всех подклассов, не требуя никаких изменений.
Теперь рассмотрим ситуации нарушения и выполнения этого принципа.
Нарушение:
// Нарушение принципа подстановки Лисков.
// В этом примере метод, который мы написали в главном классе, должен правильно работать в его подклассах в соответствии с принципом подстановки Лисков,
// но когда подкласс наследовался от суперкласса, метод fly не работал так, как ожидалось.
open class Bird {
open fun fly() {}
}
class Penguin : Bird() {
override fun fly() {
print("Penguins can't fly!")
}
}
Согласно принципу подстановки Лисков, метод, написанный в главном классе, должен корректно работать в его подклассах. Но как показано в этом примере, при наследовании подкласса от суперкласса метод fly стал работать не так, как ожидалось.
Выполнение:
// Выполнение приципа подстановки Лисков.
// Как видно из этого примера, все, что мы пишем в суперклассе, будет работать и для подклассов,
// потому что мы реализовали метод, который не действителен для подклассов, создав интерфейс и используя его там, где нам это нужно.
open class Bird {
// общие методы и свойства Bird (птица)
}
interface IFlyingBird {
fun fly(): Boolean
}
class Penguin : Bird() {
// Методы и свойства, специфические для пингвинов
}
class Eagle : Bird(), IFlyingBird {
override fun fly(): Boolean {
return true
}
}
Как видно из этого примера, все, что написано в суперклассе, будет работать и в подклассах, поскольку реализован метод, не действительный для подклассов, путем создания и использования интерфейса там, где это необходимо.
Принцип разделения интерфейса
Принцип разделения интерфейса — это правило создания компьютерных программ. Оно гласит: при создании различных частей программы не следует делать их все одинаковыми. Вместо этого нужно создавать их менее масштабными и более конкретными, чтобы другие части программы не зависели от того, что им не нужно. В результате получаем код, который легче изменять и обслуживать, поскольку каждая часть делает только то, что ей нужно.
Теперь рассмотрим ситуации нарушения и выполнения этого принципа.
Нарушение:
// Нарушение принципа разделения интефрейса.
// В этом примере мы видим, что созданный нами интерфейс содержит множество методов.
// Если мы делаем все внутри общего интерфейса, то рискуем навязать лишние функции в тех местах, которые реализуют интерфейс.
// Но можно разделить систему на более мелкие интерфейсные части.
interface Animal {
fun swim()
fun fly()
}
class Duck : Animal {
override fun swim() {
println("Duck swimming")
}
override fun fly() {
println("Duck flying")
}
}
class Penguin : Animal {
override fun swim() {
println("Penguin swimming")
}
override fun fly() {
throw UnsupportedOperationException("Penguin cannot fly")
}
}
Как видно в этом примере, созданный интерфейс содержит множество методов. Если все делать внутри общего интерфейса, то можно навязать лишние функции в местах, которые реализуют интерфейс. Поэтому стоит разделить систему на более мелкие интерфейсные части.
Выполнение:
// Выполнение принципа разделения интерфейса.
// Как видим на примере правильного использования этого принципа, разделение системы на более мелкие интерфейсы и использование их там, где это необходимо, значительно упрощает внесение изменений в систему в будущем.
interface CanSwim {
fun swim()
}
interface CanFly {
fun fly()
}
class Duck : CanSwim, CanFly {
override fun swim() {
println("Duck swimming")
}
override fun fly() {
println("Duck flying")
}
}
class Penguin : CanSwim {
override fun swim() {
println("Penguin swimming")
}
}
Как видите, разделение системы на более мелкие интерфейсы и использование их там, где это необходимо, значительно упрощает дальнейшее внесение изменений в систему.
Принцип инверсии зависимостей
Принцип инверсии зависимостей гласит: модули высокого уровня не должны зависеть от модулей низкого уровня, но оба типа модулей должны зависеть от абстракций. Это означает, что классы должны зависеть от абстракций, а не от конкретных реализаций. Идея данного принципа состоит в том, чтобы отделить компоненты друг от друга, что делает код более модульным, более легким для тестирования и более удобным для сопровождения.
Теперь рассмотрим ситуации с нарушением и выполнением этого принципа.
Нарушение:
// Нарушение принципа инверсии зависимостей.
// Как видно из приведенного примера, каждый из способов оплаты обрабатывается отдельно в классе Service посредством хардкодинга.
// Вместо реализации хардкодинга нужно было сделать так, чтобы система ЗАВИСЕЛА от абстрактной структуры.
class PaymentService {
private val paymentProcessorPaypal = PaypalPaymentProcessor()
private val paymentProcessorStripe = StripePaymentProcessor()
fun processPaymentWithPaypal(amount: Double): Boolean {
return paymentProcessorPaypal.processPayment(amount)
}
fun processPaymentWithStripe(amount: Double): Boolean {
return paymentProcessorStripe.processPayment(amount)
}
}
class PaypalPaymentProcessor {
fun processPayment(amount: Double): Boolean {
// Process payment via Paypal API
return true
}
}
class StripePaymentProcessor {
fun processPayment(amount: Double): Boolean {
// Process payment via Stripe API
return true
}
}
fun main() {
val paymentService = PaymentService()
println(paymentService.processPaymentWithPaypal(50.0)) // Обработка платежей через API Paypal
println(paymentService.processPaymentWithStripe(50.0)) // Обработка платежей через API Stripe
}
Как видно из этого примера, каждый из методов оплаты обрабатывается отдельно в классе Service с помощью хардкодинга. Вместо задействования хардкодинга, нужно было позаботиться о том, чтобы система зависела от абстрактной структуры.
Выполнение:
// Выполнение принципа инверсии зависимостей.
// В случае выполнения принципа нам не нужно прибегать к хардкодингу для обработки методов оплаты в классе Service,
// потому что мы задали абстрактную структуру с помощью созданного нами интерфейса.
interface PaymentProcessor {
fun processPayment(amount: Double): Boolean
}
class PaypalPaymentProcessor : PaymentProcessor {
override fun processPayment(amount: Double): Boolean {
// Обработка платежей через API Paypal
return true
}
}
class StripePaymentProcessor : PaymentProcessor {
override fun processPayment(amount: Double): Boolean {
//Обработка платежей через API Stripe
return true
}
}
class PaymentService(private val paymentProcessor: PaymentProcessor) {
fun processPayment(amount: Double): Boolean {
return paymentProcessor.processPayment(amount)
}
}
fun main() {
val paymentProcessor = PaypalPaymentProcessor()
val paymentService = PaymentService(paymentProcessor)
println(paymentService.processPayment(50.0)) // Обработка платежей через API Paypal
}
В этом примере не пришлось реализовывать хардкодинг при работе с методами оплаты в классе Service, поскольку была установлена абстрактная структура с помощью созданного интерфейса.
Заключение
Таким образом, принципы SOLID необходимы для создания сопровождаемого, масштабируемого и эффективного программного обеспечения. Используя уникальные возможности и конструкции Kotlin, разработчики могут создавать модульные, слабосвязанные системы, которые соответствуют этим принципам.
Соблюдение принципов SOLID не только повышает тестируемость кода, но и способствует развитию культуры непрерывного совершенствования и использования лучших практик. В конечном итоге применение этих принципов при разработке на языке Kotlin приводит к созданию более качественного ПО, которое можно эффективно поддерживать и адаптировать к изменяющимся требованиям.
Читайте также:
- Как уменьшить объем шаблонного кода в тестах Kotlin
- Модификатор Kotlin, которого не должно было быть
- Хитрости и приемы эффективного программирования на Kotlin
Читайте нас в Telegram, VK и Дзен
Перевод статьи Hüseyin Özkoç: Kotlin SOLID Principles | Huawei Developers