Введение в важные принципы, которые вы должны знать — DRY, KISS, SOLID

Исходя из моего опыта первокурсника бакалавриата и опыта старшеклассников, школы и колледжи учат вас программированию, а также необходимой математике, такой как дискретная математика и исчисление. Но когда вы покидаете университет и начинаете работать в отрасли, вам необходимо знать определенные концепции и принципы, чтобы переход был легким. Мы обсудим принципы KISS, DRY и SOLID.

Принцип ПОЦЕЛУЯ

Держать его просто глупо!

Часто вы обнаружите, что работаете вместе в команде. Группа разработчиков, работающих над разными аспектами проекта. Если бы вам пришлось переключиться и работать над чужим кодом, хотели бы вы видеть беспорядочный код без каких-либо комментариев или переменных? Или хотели бы увидеть хорошо задокументированный код, который хорошо объясняет эту часть проекта? Очевидно последнее.

Если вы пишете очень сложную программу и говорите, что заболели. Вы возвращаетесь к своему проекту через неделю после выздоровления. Конечно, вы потеряете поток программирования, но хотели бы вы прийти и посмотреть код, который вы больше не понимаете? Или увидеть что-то, что вы можете немного понять для начала? Опять же последнее.

Теперь представьте, что вы работаете над проектом в одиночку и работаете над ним каждый день. Вы столкнетесь с ошибками, и вам нужно их отладить. Насколько легко отлаживать код, если он сложнее, чем должен быть?

Все вышеперечисленные сценарии указывают на одно — будь проще, глупец!

«Любой дурак может написать код, понятный компьютеру. Хорошие программисты пишут код, понятный людям». — Мартин Фаулер

В конце концов, машине все равно, написали ли вы простой код или сложный код для выполнения определенной задачи. Напротив, это определенно важно для людей (включая вас), которые будут читать и пытаться понять код.

Но как я ПОЦЕЛУЮ? (звучит странно, я в курсе)

Рассмотрим примерный класс, Студент. Это будет хранить 2 элемента и карту, ключ и значение которой являются парами. Первая пара состоит из 2 строк: имя модуля и идентификатор. Вторая пара состоит из 2 дублей, полученных оценок и максимальной оценки за этот модуль.

data class Student(
  val name: String,
  val age: Int,
  val moduleMarks: Map<Pair<String, String>, Pair<Int, Int>>
)

Сделав такой выбор дизайна, теперь вам необходимо записать имена студентов и модули, по которым они набрали более 80%.

fun scholars(students: List<Student>): Map<String, List<String>> {
  val scholars = mutableMapOf<String, List<String>>()
  students.forEach { student ->
    scholars[student.name] = student.moduleMarks
      .filter { (_, (a, m)) -> a / m > 0.8}
      .map { ((n, _), _) -> n }
  }
  return scholars
}

Возвращение даже через несколько дней к такому коду было бы катастрофой. Хотя вы можете утверждать, что это был довольно простой пример, есть способы упростить его. Вводите как можно больше абстракций и переменных.

data class Student(
  val name: String,
  val age: Int,
  val moduleMarks: Map<Module, Mark>
)

data class Module(
  val name: String,
  val id: String
)

data class Mark(
  val achieved: Double,
  val maximum: Double
) {
  fun isAbove(percentage: Double): Boolean {
    return achieved / maximum * 100 > percentage
}

fun scholars(students: List<Student>): Map<String, List<String>> {
  val scholars = mutableMapOf<String, List<String>>()
  students.forEach { student ->
    val modulesAbove80 = student.moduleMarks
      .filter { (_, mark) -> mark.isAbove(80.0)}
      .map { (module, _) -> module.name }

    scholars[student.name] = modulesAbove80
  }
  return scholars
}

Это добавляет много кода. Но что более важно, код выглядит чище и читается как английский.

СУХОЙ принцип

Не повторяйтесь

Если вы обнаружите, что выполняете один и тот же код снова и снова, создайте функцию и используйте ее повторно. В моем университетском задании я работал с набором объектов (ячеек), и большинство (если не все) определенных функций требовали от меня поиска и извлечения определенного объекта из набора и работы с ним.

public class Spreadsheet implements BasicSpreadsheet {
  private final Set<Cell> cells;

  @Override
  public double getCellValue(CellLocation location) {
    Cell cell = cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);
    
    return cell == null ? 0d : cell.getValue();
  }

  @Override
  public String getCellExpression(CellLocation location) {
    Cell cell = cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);
    
    return cell == null ? "" : cell.getExpression();
  }

  @Override
  public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
    Cell cell = cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);

    // ...
  }

  // ...
}

Это большой код выше. Но пока я набирал этот код, я обнаружил, что копирую один и тот же блок кода несколько раз в разных частях. Поэтому я абстрагировал их в функции, которые повторно использую везде.

public class Spreadsheet implements BasicSpreadsheet {
  private final Set<Cell> cells;

  @Override
  public double getCellValue(CellLocation location) {
    return getFromCell(location, Cell::getValue, 0d);
  }

  @Override
  public String getCellExpression(CellLocation location) {
    return getFromCell(location, Cell::getExpression, "");
  }

  @Override
  public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
    Cell cell = findCell(location);

    // ...
  }

  // ...

  private Cell findCell(CellLocation location) {
    return cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);
  }

  private <T> T getFromCell(CellLocation location,
                            Function<Cell, T> function,
                            T defaultValue) {
    Cell cell = findCell(location);
    return cell == null ? defaultValue : function.apply(cell);
  }
}

Таким образом, если я пойму, что в моем коде есть ошибка, мне не придется изменять код в nразных местах. Изменив его один раз, внутри функции, достаточно исправить ошибку везде.

SOLID Принципы

Это не один единственный принцип, а 5 принципов, которые крайне важно знать для разработки программного обеспечения.

S — единая ответственность

У класса должна быть одна и только одна причина для изменения.

Возможно, самый простой для понимания принцип. Каждый определяемый вами класс/функция должен выполнять только одну задачу. Учтите, что вы создаете сетевое приложение.

class Repository(
  private val api: MyRemoteDatabase,
  private val local: MyLocalDatabase
) {
  fun fetchRemoteData() = flow {
    // Fetching API data
    val response = api.getData()

    // Saving data in the cache
    var model = Model.parse(response.payload)
    val success = local.addModel(model)
    if (!success) {
      emit(Error("Error caching the remote data"))
      return@flow
    }

    // Returning data from a single source of truth
    model = local.find(model.key)
    emit(Success(model))
  }
}

Приведенный выше код нарушает принцип единой ответственности. Эта функция не только извлекает удаленные данные, но также отвечает за их локальное хранение. Это должно быть извлечено в другой класс.

class Repository(
  private val api: MyRemoteDatabase,
  private val cache: MyCachingService /* Notice I changed the dependency */
) {
  fun fetchRemoteData() = flow {
    // Fetching API data
    val response = api.getData()

    val model = cache.save(response.payload)

    // Sending back the data
    model?.let {
      emit(Success(it))
    } ?: emit(Error("Error caching the remote data"))
  }
}

// Shifted all caching logic to another class
class MyCachingService(
  private val local: MyLocalDatabase
) {
  suspend fun save(payload: Payload): Model? {
    var model = Model.parse(payload)
    val success = local.addModel(model)
    return if (success)
      local.find(model.key)
    else
      null
  }
}

Обратите внимание, что MyCachingService отвечает только за сохранение входящей полезной нагрузки в локальную базу данных, в то время как репозиторий отвечает только за получение данных при отправке модели выше. Это рекомендуется делать из-за так называемого разделения ответственности, которое улучшает отладку и тестируемость.

О — Открыто/Закрыто

Программные сущности (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации.

Этот принцип в основном означает, что не следует писать программный код, который при будущих изменениях нарушит работу кода на стороне клиента. Учтите, что вы создаете API веб-разработки на Kotlin. Вы разработали ParagraphTag, AnchorTag и ImageTag. В вашем коде вас просят сравнить высоты двух элементов.

class ParagraphTag(
  val width: Int,
  val height: Int
)

class AnchorTag(
  val width: Int,
  val height: Int
)

class ImageTag(
  val width: Int,
  val height: Int
)

// Client-code
infix fun ParagraphTag.tallerThan(anchor: AnchorTag): Boolean {
  return this.height > anchor.height
}

infix fun AnchorTag.tallerThan(anchor: ParagraphTag): Boolean {
  return this.height > anchor.height
}

infix fun ParagraphTag.tallerThan(anchor: ImageTag): Boolean {
  return this.height > anchor.height
}

// ... more functions

Вздох! Это было много работы. Теперь у вас есть новые требования, требующие включения тега заголовка. Вам придется добавить еще шесть функций на стороне клиента. Это не только утомительно, вы также модифицируете код на стороне клиента, чтобы он соответствовал потребностям вашей программы.

Вместо этого объявите интерфейс — PageTag

interface PageTag {
  val width: Int
  val height: Int
}

class ParagraphTag(
  override val width: Int,
  override val height: Int
) : PageTag

class AnchorTag(
  override val width: Int,
  override val height: Int
) : PageTag

class ImageTag(
  override val width: Int,
  override val height: Int
) : PageTag


// Client Code
infix fun PageTag.tallerThan(other: PageTag): Boolean {
  return this.height > other.height
}

Теперь вы закрыли клиентский код для дальнейшего его изменения. Чтобы расширить вашу функциональность, открыто создать новый класс и реализовать PageTag, и все будет работать отлично.

L — Замена Лискова

Если S является подтипом T, то любые свойства, доказуемые T, также должны быть доказуемы S.

Ой. Математика? Что ж, это нехорошо. Напротив, это простой для понимания принцип. Рассмотрим новый пример.

open class Bird {
  open fun fly() {
    // ... performs code to fly
  }

  open fun eat() {
    // ...
  }
}

class Penguin : Bird() {
  override fun fly() {
    throw UnsupportedOperationException("Penguins cannot fly")
  }
}

Обратите внимание, что приведенный выше класс Bird не выдает никаких исключений, в то время как класс Penguin делает это. Вы не можете заменить Penguin на Bird в клиентском коде, не сломав или не изменив его. Это нарушает принцип подстановки Лисков. Penguin расширение Bird ломает клиентский код, тем самым также нарушая принцип открытости/закрытости.

Способ исправить это — изменить реализацию проекта.

open class FlightlessBird {
  open fun eat() {
    // ...
  }
}

open class Bird : FlightlessBird() {
  open fun fly() {
    // ...
  }
}

class Penguin : FlightlessBird() {
   // ...
}

class Eagle : Bird() {
  // ...
}

Этот код выше объясняет, если FlightlessBird может есть, то все подклассы FlightlessBird тоже могут есть. Точно так же, если Bird может летать, то все подклассы Bird также должны летать.

I — Разделение интерфейса

Интерфейсы не должны заставлять своих клиентов зависеть от методов, которые они не используют.

Это определение не выглядит пугающим. На самом деле это не страшно. Считайте, что вы строите автомобиль, самолет и велосипед. Поскольку все они являются транспортными средствами, вы реализуете интерфейс Vehicle.

interface Vehicle {
  fun turnOn()
  fun turnOff()
  fun drive()
  fun fly()
  fun pedal()
}

class Car : Vehicle {
  override fun turnOn() { /* Implementation */ }
  override fun turnOff() { /* Implementation */ }
  override fun drive() { /* Implementation */ }
  override fun fly() = Unit
  override fun pedal() = Unit
}

class Aeroplane : Vehicle {
  override fun turnOn() { /* Implementation */ }
  override fun turnOff() { /* Implementation */ }
  override fun drive() = Unit
  override fun fly() { /* Implementation */ }
  override fun pedal() = Unit
}

class Bicycle : Vehicle {
  override fun turnOn() = Unit
  override fun turnOff() = Unit
  override fun drive() = Unit
  override fun fly() = Unit
  override fun pedal() { /* Implementation */ }
}

Фу! Видите, как классы вынуждены реализовывать ненужные им методы? Я также не могу объявить классы абстрактными. В соответствии с принципом разделения интерфейса вместо этого у нас должен быть такой дизайн.

interface SystemRunnable {
  fun turnOn()
  fun turnOff()
}

interface Drivable() {
  fun drive()
}

interface Flyable() {
  fun fly()
}

interface Pedalable() {
  fun pedal()
}

class Car : SystemRunnable, Drivable {
  override fun turnOn() { /* Implementation */ }
  override fun turnOff() { /* Implementation */ }
  override fun drive() { /* Implementation */ }
}

class Aeroplane : SystemRunnable, Flyable {
  override fun turnOn() { /* Implementation */ }
  override fun turnOff() { /* Implementation */ }
  override fun fly() { /* Implementation */ }
}

class Bicycle : Pedalable {
  override fun pedal() { /* Implementation */ }
}

Теперь это выглядит намного чище, а также проще ссылаться на различные возможности их интерфейсов.

D — Инверсия зависимости

1. Модули высокого уровня не должны зависеть от модулей низкого уровня; оба должны зависеть от абстракций.

2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Что это вообще значит? Модули высокого уровня — это те модули, которые видит бизнес или пользовательский интерфейс. Модули низкого уровня — это те, которые обрабатывают сложности приложения. Вспомните мой пример из Solid Responsibility Principle:

class Repository(
  private val api: MyRemoteDatabase,
  private val cache: MyCachingService
) {
  fun fetchRemoteData() = flow {
    // Fetching API data
    val response = api.getData()

    val model = cache.save(response.payload)

    // Sending back the data
    model?.let {
      emit(Success(it))
    } ?: emit(Error("Error caching the remote data"))
  }
}

class MyRemoteDatabase {
  suspend fun getData(): Response { /* ... */ }
}

class MyCachingService(
  private val local: MyLocalDatabase
) {
  suspend fun save(): Model? { /* ... */ }
}

class MyLocalDatabase {
  suspend fun add(model: Model): Boolean { /* ... */ }
  suspend fun find(key: Model.Key): Model { /* ... */ }
}

Это выглядит хорошо, и это будет работать отлично. Однако в будущем, если я решу изменить свою локальную базу данных с PostgreSql на MongoDB; или, если я решу полностью изменить свой механизм кэширования, мне придется изменить все детали реализации, а также клиентский код. Модули высокого уровня зависят от конкретных модулей низкого уровня.

Это неправильно. Вместо этого вы должны абстрагировать функциональность в интерфейс и иметь конкретные реализации, расширяющие ее.

interface CachingService {
  suspend fun save(): Model?
}

interface SomeLocalDb() {
  suspend fun add(model: Model): Boolean
  suspend fun find(key: Model.Key): Model
}

class Repository(
  private val api: SomeRemoteDb,
  private val cache: CachingService
) { /* Implementation */ }

class MyCachingService(
  private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }

class MyAltCachingService(
  private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }

class PostgreSQLLocalDb : SomeLocalDb { /* Implement methods */ }
class MongoLocalDb : SomeLocalDb { /* Implement methods */ }

Вы можете легко изменить различные реализации вашего репозитория во всем приложении, изменив одно слово. Это вызывает у меня мурашки каждый раз, когда я слушаю это.

Я потратил довольно много времени, формулируя каждую информацию в этой статье. Надеюсь, вам понравилось читать и вы узнали что-то новое. Спасибо!

Использованная литература:

If you wish to read every article from me, consider joining the Medium 
program with this referral link.

Want to connect?

My GitHub profile.
My Portfolio website.