Изучение компромиссов между безопасностью типов и возможностью независимого развертывания в разработке программного обеспечения.

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

Мы это уже знаем, но вызывает ли система типов, на которую мы полагаемся и которую так любим, именно эти проблемы?

Введение

Дебаты между безопасными типами и динамическими языками ведутся уже давно. Это кажется «просто предпочтением», а не чем-то, что позволит или помешает вам что-то сделать. Вы должны быть в состоянии написать почти такой же код с типобезопасным и динамическим языком, причем типобезопасный код будет… безопаснее, потому что компилятор защищает вас от сбоев во время выполнения. Но это неправильно. Типы вводят связь, о которой вы, возможно, никогда не думали.

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

Теперь мы рассмотрим, как эти двое не могут и не могут быть вместе.

Пример

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

Пример написан таким образом, чтобы было легко обнаружить и понять проблему; дело не в заводах.

Пример будет на Котлине

Легенда диаграммы архитектуры:
открытая стрелка = использование
закрытая стрелка = реализует

Для этого мы рассмотрим пример Factory Pattern.

Автомобильный завод

sealed interface Car
object Porsche911 : Car
object BMWM8Competition: Car
object MercedesSL: Car

interface CarFactory {
    fun makePorsche911(): Car
    fun makeBMWM8Competition(): Car
    fun makeMercedesSL(): Car
}

class CarFactoryImpl : CarFactory {
    override fun makePorsche911() = Porsche911
    override fun makeBMWM8Competition() = BMWM8Competition
    override fun makeMercedesSL() = MercedesSL
}

Почти стандартный способ, которым вы видите это в Интернете.

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

Не так быстро…

Проблема

Что произойдет, если мы добавим классную новую машину (производную), например, DeLorean Alpha5? Затем нам нужно внести изменения выше и ниже линии.

Таким образом, зависимости не указывают только в одном направлении, что означает, что изменения ниже линии вызывают изменения выше линии. Добавление к реализации разрушает абстракцию. Они не могут быть развернуты независимо.

Какое это имеет отношение к безопасности типов?

Решение 1

Мы можем легко исправить это, используя один метод makeCar, который принимает тип автомобиля, который мы хотим сделать:

interface CarFactory {
    fun makeCar(type: CarType): Car
}

class CarFactoryImpl : CarFactory {
    fun makeCar(type: CarType): Car { ... }
}

Теперь в игру вступают типы и связанные с ними проблемы.

Что будет CarType? sealed class, sealed interface, enum? Неважно, что вы выберете; проблема есть.

Проблема легко увидеть, как только мы нарисуем диаграмму.

А если мы добавим новую машину:

Код над строкой знает и зависит от кода под строкой. Всякий раз, когда мы добавляем новую машину в наши sealed class, sealed interface или enum, мы нарушаем абстракцию. Оба должны быть развернуты вместе, поэтому они не могут быть развернуты независимо друг от друга.

Решение 2

interface CarFactory {
    val carNames: List<String>
    fun makeCar(type: String): Car
}

class CarFactoryImpl : CarFactory {
    val carNames = listOf("Porsche 911", "BMW M8 Competition", "Mercedes SL")
    fun makeCar(type: String): Car { ... }
}

Использование String в качестве аргумента позволяет нам иметь независимую возможность развертывания именно потому, что это небезопасно для типов.

А если мы добавим новую машину:

Изменения ниже черты остаются ниже черты!

Мы пожертвовали безопасностью типов в пользу возможности независимого развертывания, но есть ли способ иметь и то, и другое? Не совсем. Не так, как мы думаем о безопасности типов.

Оптимальный ответ

Модульные тесты. Модульные тесты предоставляют нам механизмы безопасности для наших String аргументов. Хороший набор тестов обеспечивает такую ​​же безопасность, как и проверка типов. Конечно, эти два механизма разные, но результат один и тот же.

Субоптимальный ответ

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

Заключение

Безопасность типа муфта. Мы привязаны к типам, которые используем, и если случается так, что мы используем тип, находящийся ниже линии, то независимая возможность развертывания нарушается.

Несмотря на то, что безопасность типов — это то, на что мы стали полагаться за эти годы, я не думаю, что это важнее, чем возможность независимого развертывания. Безопасность типов — это костыль для нас, программистов; это позволяет нам уйти с большим, чем мы должны.

Модульные тесты — это механизмы безопасности, которые работают глубже, чем простая проверка типов, поэтому я целыми днями беру их за безопасность типов. На данный момент я практикую строгий TDD уже более трех лет, и это лучшее, что я сделал в своей карьере программиста. Я люблю это. Чем больше я использую TDD, тем больше я люблю языки с динамической типизацией. Я бы хотел, чтобы за пределами мира JS был динамичный Kotlin. Если есть, пожалуйста, дайте мне знать.

Дайте мне знать, что вы думаете об этой теме. Что для вас важнее — типобезопасность или возможность независимого развертывания?