2017-12-24

О типизации

Да начнётся срач!
Так получилось, что пару месяцев подряд я интенсивно кодил на Котлине. И учил студентов кодить на Яве.
Один разок я даже устроил воркшоп в стиле TDD, где к своему удивлению обнаружил, что добрая половина ява-кода в IntelliJ IDEA пишется по нажатию Alt+Enter. Это автоисправление ошибок: класс новый создать, который тут упоминается, но ещё отсутствует, метод новый добавить, и в нужные интерфейсы, и в нужные классы, когда этот метод впервые упоминается в тестах.
А другое автодополнение, по Alt+Space, но чаще просто выскакивающее само, после точки после имени переменной, позволяет особо не нагружать свою человеческую память тем, что умеют данные классы. Таким образом тоже «пишется» уйма кода.
А вот неделю назад пришлось расчехлить Питон. Надо было чуток подкрутить-поковырять проектик, который чуть ли не год пылился.
Ну и где моя магия автодополнений? Если что, у меня IDEA Ultimate, и все нужные плугины стоят.
Почему IDEA ничего не знает о существовании функции requests.compat.urljoin, пока я не напишу соответствующий импорт? Почему я, когда смотрю на __init__.py этого самого requests, тоже не вижу никаких compat? Откуда он вообще берётся? Почему в официальной документации requests ничего не сказано про compat? Почему таки использование этого urljoin нахваливают на StackOverflow? Его вообще можно использовать, или это секретное API, которое в любой момент может превратиться в тыкву?
Это лишь один, последний попавшийся, пример. Почему-то в Питоне всегда так. IDE лишь смутно предполагает, что тут сымпортировали, что тут передали, что с этим можно делать. Разработчику нужно либо действовать методом научного тыка, либо писать подробные тесты, либо очень хорошо знать ту систему, которую пишешь, и те библиотеки, которые используешь. Чтобы предполагать несколько больше, чем IDE. А если нужно быстро написать долбаный плугин к совершенно незнакомому фреймворку, вообще чувствуешь себя слепым котёнком. И остаётся только научный тык.
Сладывается у меня подозрение, что это всё из-за динамической типизации. Никто не знает, что тут пришло, лишь бы крякало. Если не крякает, это ваши проблемы, надо было выявить тестами.
Untyped Duck
Тут сразу возражают: ведь весь окружающий мир динамичен. Ведь нам в запросе приходит бог его знает какой JSON, и нам надо его обработать. И в языках с динамической типизацией это делается просто и естественно. А в языках со статической типизацией приходится сначала натягивать этот JSON на какой-то класс. Да и этот класс нужно сначала где-то объявить.
На самом деле, хочется видеть «линзу». Не важно, что там приходит. Но здесь и сейчас я хочу видеть объект с определёнными свойствами. Если нужных мне обязательных свойств нужного типа не пришло — это ошибка. Если пришло, натягиваем на объект, и поехали дальше уже со строгой типизацией.
Собственно, все приличные веб фреймворки так и поступают с входящим JSON. Вопрос только в том, как описать те свойства, которые нам интересны. Можно ява-бобами, можно котлиновыми дата-классами (обожаю их). В нашем секретном фреймворке мы вообще (почти) автоматически натягиваем динамический объект сообщения (а-ля Map) на (почти любой) интерфейс, описывающий ожидаемые свойства этого сообщения.
Написать интерфейс из нескольких геттеров, или написать дата-класс на три строчки — имхо, не большая плата за то, что во всех дальнейших операциях гарантируется наличие этих нужных свойств. Причём гарантия даётся ещё при написании кода. В рантайме сломаться может только маппинг динамического объекта на статическое описание. Ну мы же и не ожидали, что любой JSON на входе нам подойдёт?
Говорят, интерфейсы в Go как раз и делают то, что нужно. С одной стороны есть произвольные структуры. С другой стороны есть интерфейсы, описывающие ожидаемые методы. Если есть функции над структурой, реализующие эти методы, считается, что эта структура реализует этот интерфейс. Поэтому мы пишем интерфейс, указывая, что мы хотим. И пишем функции, указывающие, как из имеющихся данных получить то, что мы хотим.
Static Typing
Вообще вырисовывается три способа передачи развесистых данных в функцию. Передача динамического объекта, вроде мапы или словаря. Передача статического объекта, с заранее (на этапе компиляции) определённым набором полей. Ну и можно выделить именованные параметры, достаточно динамичный способ, тем не менее, задаваемый при написании кода, которым почему-то брезгуют многие языки. А ведь удобно, не зря же в современных API на JavaScript взялись передавать объекты с именованными полями, как единственный аргумент функции.
В языках с динамической типизацией легче передавать динамические объекты. Словари в Питоне, ассоциативные массивы в PHP, объекты (они там все динамические) в ЯваСкрипте. Создавать новые классы тут муторно и лень.
В языках со статической типизацией легче передавать статические объекты. Проще описать класс с нужными свойствами и сконструировать его экземпляр, чем собрать и разобрать какой-нибудь Map.
Наверное, это какой-то заговор. И вот эта лёгкость динамичного в динамичных языках и лёгкость статичного в статических языках как раз и приводит к тому, что языки остаются каждый в своей нише, и не идут навстречу друг к другу.
Duck Typing
А если нам нужны не данные, а поведение?
В Питоне есть такое понятие: file-like object. Попробуйте найти в официальной документации, какие именно методы должны быть у таких объектов. Конечно будет read(). Но ведь не только он?
Утиная типизация. Известно (по крайней мере ожидается), что это утка. Утка должна крякать. Но что такое «крякать»? Документация Питона не всегда даёт ответ на этот вопрос. Интерфейсы же, что Явы, что Гоу, дают чёткий ответ на этот вопрос.
И всё же, я предпочитаю статическую типизацию даже не из-за проверок компилятора, в конце концов тесты в любом случае нужно писать, а из-за того, что в случае, когда IDE знает точный тип, она может дать актуальные подсказки, которые действительно ускоряют работу. Если мерять эту работу не количеством набранных символов (что можно подсчитать по истории коммитов), а количеством нажатых клавиш (что можно подсчитать только локально в IDE).
Впрочем, тут есть и обратная сторона. Видел кучу разработчиков, которые вообще ни разу не открывали документацию того API, который они вовсю используют. Они просто ставят точку, смотрят список методов, который выдаёт IDE, выбирают наболее подходящий (по имени), и используют его. Конечно же они напарываются на неудачные названия методов, всяческие нюансы, вроде ограничений использования данных классов и методов, выбирают не оптимальные решения. Всё равно документацию читать надо. RTFM, как говорится.
Duck Typing
З.Ы. Не уверен, относится ли это к данной дискуссии, но утверждают, что с типами в Питоне вообще всё плохо.

2017-12-09

О Spring

Spring — это весна. Spring — это пружина. Spring — это родник. Springfield — это городок, где живут Симпсоны. Плюс ещё стопицот одноимённых городков в Соединённом Королевстве, Австралии и Соединённых Штатах.
А ещё есть Spring Framework. Фреймворк, который знают все явисты. Возникший когда-то как легковесная альтернатива Ынтырпрайзным ЯваБобам (EJB).
Spring Logo
Помню, как лет десять назад мы перелезали на Спринг с таких, ныне экзотичных вещей, как Apache Struts, или даже просто месива из сервлетов и JSP. Тогда надо было писать большущие простыни XMLя с описанием всех бинов. Уже тогда надо было подглядывать в исходники Спринга, чтобы понять, как туда воткнуть что-то нестандартное с точки зрения разработчиков фреймворка.
Сейчас в Спринге есть аннотации. И конфигурацию можно описывать Ява кодом. Или вообще не описывать, а просто помечать нужные классы как @Component.
Сейчас есть Spring Boot. Вообще крайне странная штуковина.
С одной стороны, он сильно упрощает начальное создание Спринг приложения. Это — POM артефакты, содержащие целые группы спринговых и внешних зависимостей, с тщательно подобранными (хочется в это верить) версиями, гарантированно работающими друг с другом. Это — автоматические настройщики, которые создают бины, исходя из набора свойств в application.yml или application.properties, и сканируют пакеты в поисках классов и методов помеченных хитрыми аннотациями, чтобы и их настроить. Это — плугины к Maven и Gradle, которые умеют собирать красивые суперджары, с внедрённым Jetty или Netty для веб приложений.
С другой стороны, Spring Boot весьма усложняет попытки уйти в сторону и сделать что-то непредусмотренное. Притащить другую версию библиотеки почти наверняка не выйдет, потому что у артефакта Spring Boot уже есть своя версия. Причём какая это версия, простого способа узнать нет, ибо там очень много транзитивных зависимостей. Чтобы воткнуть нестандартную конфигурацию, банально несколько подключений к разным MongoDB, придётся отключать автоконфигураторы. Какие именно отключать, придётся находить методом научного тыка, ибо нет простого списка этих конфигураторов с описанием того, за что они отвечают и что делают.
Spring Boot Logo
В тех случаях, когда наш любимый, акторный, но пока не очень человеколюбивый фреймворк использовать нельзя, мы берём Spring, Spring Boot и Kotlin. И нормально.
Точка входа в приложение, Application.kt, выглядит забавно.
@SpringBootApplication
@EnableAutoConfiguration(exclude=arrayOf(MongoAutoConfiguration::class, MongoDataAutoConfiguration::class))
@EnableScheduling
@EnableCaching
open class Application

fun main(args: Array<String>) {
    SpringApplication.run(Application::class.java, *args)
}
main() тут получается в классе ApplicationKt. Но Спрингу и его Буту нужен ещё один класс, открытый, ибо станет бином. И вот этот класс и становится @SpringBootApplication.
Современный Спринг делает страшные вещи с вашими классами. То, что будет бином, т.е. любые классы, помеченные @SpringBootApplication, @Configuration, @Component, @Repository, @Service, @Controller, а также любые методы, помеченные @Bean, в Котлине должны быть open. Потому что вы (почти) никогда не получите на выходе (в IOC) именно ваш класс. Вы получите его наследника. Куда будут аккуратно засунуты все объявленные @Autowired и @Value.
Любой класс, куда вы навешаете эти аннотации, станет частью Спринга. Вам понадобятся спринговые зависимости, чтобы эти аннотации объявить. И если этот класс случайно окажется в пакете, где его найдёт автоконфигурация, он сразу окажется в IOC. А если не получится найти все нужные @Autowired и @Value, то приложение рухнет на старте.
Поэтому мы так не делаем. Мы стараемся делать сервисы и компоненты, которые вообще не зависят от Спринга. Все другие сервисы и компоненты, а также конфигурационные значения, которые нужны для работы этого сервиса, передаются в конструкторе. Как это и положено с нормальными объектами. Куча аргументов конструктора в Котлине — не проблема. Мы просто используем именованные аргументы.
Сами внешние сервисы и компоненты в аргументах конструктора представлены интерфейсами. Интерфейс легко замокать. И можно и нужно протестировать этот компонент как положено, юнит тестами. А ещё типы бинов удобнее представлять интерфейсами, тогда можно тихо и незаметно подменить реализацию. Кстати, Спринг вполне корректно различает генерик интерфейсы, с разными типами переменных типа.
class EntityInsertService(
    private val name: String = "",
    private val mongoOps: MongoOperations,
    private val collectionName: String,
    private val executor: Executor
) : IInsertService<Entity> {

    override fun insert(data: Entity) {
        //...
    }

    override fun flush() {
        //...
    }

}
Такие компоненты без аннотаций легко выносятся в (почти) независимые от Спринга библиотеки и переиспользуются между разными приложениями.
А в самом приложении они уже подключаются через объявление конфигурации.
@Configuration
open class InsertServiceConfiguration {

    @Value("\${insert.concurrency:2}")
    private var concurrency: Int = 2

    @Value("\${insert.collection:data}")
    private lateinit var collection: String

    @Autowired
    private lateinit var mongo: MongoOperations

    @Bean
    open fun insertService(): IInsertService<Entity> {
        val executor = Executors.newWorkStealingPool(concurrency)
        return EntityInsertService(
            name = "entityInsert",
            mongoOps = mongo,
            collectionName = collection,
            executor = executor
        )
    }

}
Kotlin Logo
Вот эти вот insert.concurrency — это проперти приложения. Как известно, их можно объявлять в application.properties. Но мы, конечно же, предпочитаем более сложновложенный application.yml. Из этих пропертей тоже можно конструировать бины, списки и даже мапы.
Вот вам нестандартная конфигурация с кучей Монг:
mongodb:
  connections:
    source:
      host: 172.31.22.180
      port: 27017
      database: project
      serverSelectionTimeout: 10000     # https://scalegrid.io/blog/understanding-mongodb-client-timeout-options/
      connectTimeout: 5000
      socketTimeout: 1000
    target:
      host: localhost
      port: 27017
      database: test
      serverSelectionTimeout: 10000
      connectTimeout: 5000
      socketTimeout: 1000
      writeConcern: acknowledged        # http://mongodb.github.io/mongo-java-driver/3
Можно, конечно, наваять @Configuration класс, куда внедрить все эти значения через @Value. Но мне понадобилось иметь произвольное количество таких подключений. Их все можно прочитать в мапу с помощью @ConfigurationProperties.
data class MongoConnectionProperties(
    var host: String = "localhost",
    var port: Int = 27017,
    var database: String = "test",
    var serverSelectionTimeout: Int = 0,
    var connectTimeout: Int = 5000,
    var socketTimeout: Int = 1000,
    var writeConcern: String = "acknowledged",
    var connectionsPerHost: Int = 10
) {
    val writeConcernValue: WriteConcern
        get() = WriteConcern.valueOf(writeConcern.toUpperCase())
}

data class MongoConnections(
    val connections: MutableMap<String, MongoConnectionProperties> = mutableMapOf()
) {
    operator fun get(name: String): MongoConnectionProperties
        = connections[name] ?: throw NoSuchElementException("No MongoDB connection mongodb.connections.$name")
}

@Configuration
open class MongoConnectionsConfiguration {

    @Bean
    @ConfigurationProperties(prefix="mongodb")
    open fun allMongoProperties(): MongoConnections {
        return MongoConnections()
    }

}
Эта мапа должна быть мутабельной, а объекты должны быть настоящими ява бинами, с полным набором сеттеров, чтобы Спринг смог эту мапу заполнить. Поэтому тут в котлиновых датаклассах сплошные var и дефолтные значения. Для итогового набора свойств тоже нужно придумывать свой класс, нельзя просто вернуть коллекцию, потому что списки и мапы в качестве типа бина обрабатываются Спрингом по-особому: он пытается туда впихнуть все бины из контекста, подходящие по типу, а нам это совсем не нужно.
Из бина описания можно сделать настоящий объект MongoTemplate, который уже можно использовать.
@Configuration
open class MongoConnectionsConfiguration {

    @Bean
    @Scope("prototype")
    open fun mongoOperations(name: String): MongoOperations {
        val properties = allMongoProperties()[name]
        val options = MongoClientOptions.builder()
            .writeConcern(properties.writeConcernValue)
            .serverSelectionTimeout(properties.serverSelectionTimeout)
            .connectTimeout(properties.connectTimeout)
            .socketTimeout(properties.socketTimeout)
            .connectionsPerHost(properties.connectionsPerHost)
            .build()
        val dbFactory = SimpleMongoDbFactory(
            MongoClient(ServerAddress(properties.host, properties.port), options),
            properties.database)
        return MongoTemplate(dbFactory)
    }

}
Правда здесь у нас получается бин, который прототип, да ещё принимающий строку параметром. Получить такие бины из ApplicationContext труда не составляет. А вот @Autowired или @Inject для них уже не работают.
Поэтому приходится, если нужно, все эти прототипы создавать явно, и засовывать в синглетон коллекцию. Опять своего отдельного типа. Славься Котлин дата классами.
data class MongoOperationsMap(
    val mongos: Map<String, MongoOperations>
)

@Configuration
open class MongoConnectionsConfiguration {

    @Bean
    open fun allMongoOperations() : MongoOperationsMap {
        val map: MutableMap<String, MongoOperations> = mutableMapOf()
        for ((name, _) in allMongoProperties().connections) {
            map.put(name, mongoOperations(name))
        }
        return MongoOperationsMap(map)
    }

}
Ну а дальше этот наш дата класс можно автовайрить куда угодно.
Springfield
Иногда приходится бороться со Спрингом вообще, и со Спринг Бутом в частности. И хочется выкинуть этот Спринг нафиг. Со всеми его странными и несовместимыми обёртками вокруг обычного Монго драйвера.
Но как представишь, сколько вопросов возникнет без Спринга: Где создавать компоненты? Как их связывать друг с другом? Как автоматически и многопоточно подключаться к какой-нибудь очереди? Какой шедулер взять? Какой кэш взять и как его прикрутить?
И решаешь: пусть Спринг остаётся. Всё же он берёт на себя громадную кучу инфраструктурных проблем. А иногда с ним пободаться даже полезно.