Заметки о Dependency Injection

 
     
 

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

Преамбула

Для того, чтобы не превращать эту статью в рассказ “от сотворения мира”, очень рекомендую посмотреть это видео (с указанного таймкода, лекция там в целом про архитектуру, но нас в рамках этой статьи будут интересовать выкладки про DI).

Постановка проблемы

Базовая задача

Итак, мы стараемся придумать решение следующей проблемы

  • protocol SomeBookService { /* ... */ }
    
    class BookProcessor {
        let service: SomeBookService = SomeBookServiceImpl()
    }
    
  • interface SomeBookService { /* ... */ }
    
    class BookProcessor(
        private val service: SomeBookService = SomeBookServiceImpl()
    )
    

Что мы хотим от хорошего решения?

  • Через протокол/интерфейс. Использующий зависимость класс не должен знать о конкретной реализации и должен иметь работать с любой реализацией правильно реализующей контракт (объявленный в протоколе/интерфейсе).
  • Узкий контракт. Реализация, которую мы получаем в виде зависимости должена иметь минимально необходимый контракт. Наша сущность не должна видеть лишнего
  • Необходимость и достаточность. Поставляемые зафисимости поставляются в необходимом и достаточном объеме, не требуется каких-то специальных приседаний для того, чтобы получить что-то еще (это как правило относится к запрету использовать внутри синглтоны).
  • Реализация DI для логического кода “невидима”. Мы не должны видеть фрагменты порождающего кода где-либо (кроме некоторых исключений).

Платформенные особенности

Мы рассматриваем мобильные платформы (в целом можно рассматривать и серверные, но там как правило все проще в этом плане).

iOS

Как правило, все сущности iOS позволяют нормально реализовать constructor injection паттерн без необходимости построения специальных решений. Иногда требуются специальные действия для создания циклических зависимостей, но они легко решаются способами, описанными ниже.

Единственный объект, который система создает сама - это UIApplication (и applicationDidFinishLaunchingWithOptions) или SwiftUI объект обозначенный как @main. Но эти объекты будем считать точкой входа и растить графы от них.

Android

У Android также как и в iOS есть главный объект создаваемый системой - Application, но есть и важное отличие - в Android есть объекты, которые фреймворком могут быть убиты и пересозданы через тривиальный конструктор, такие как Activity, Fragment, View и тд. И есть общепринятая практика использовать для иньекции зависимостей в такие объекты через паттерн Service Locator. Service Locator считаем антипаттерном (как минимум он противоречит принципам хорошего решения, упомянутым выше).

Поэтому для решение проблемы спец сущностей (которые могут быть пересозданы), в Android надо предусмотреть дополнительные средства.

O Service Locator’е

Service Locator это такой паттерн проектирования, который, упрощенно говоря, предоставляет единую точку, через которую можно запросить разные зависимости. В рамках создания библиотек для DI он обычно соседствует с паттерном Singleton предоставляя публично известный разделяемый объект, из которого можно запросить практически все что угодно.

Я считаю его антипаттерном в применении к задачам DI (у него есть другая применимость, которая норм).

Но чтобы не быть голословным, давайте рассмотрим поставленные требования к хорошему решению:

  • Через протокол/интерфейс - эту задачу в целом можно решить, отдавая из публичного singleton’а сервис локатора интерфейсы
  • Узкий контракт - так как точка эта общеизвестная, то надо отдавать зависимость целиком, что нарушает принцип сужения контракта. Можно из публичного singleton’а отдавать одну реализацию под набором интерфейсов, но тогда у нас будет очень сложно выглядеть общий контракт такого объекта, сложно будет что-то в нем найти.
  • Необходимость и достаточность - мы показываем всевозмоюжные интерфейсы или даже реализации, что любой клиент может видеть все приложение, что явно нарушает это требование
  • “Невидимость” - требование явно нарушается, так как из любой точки приложения можно публичный singleton локатора

Таким образом Service Locator использовать не стоит, поэтому будем строить решение на базе концепции контейнера.

О библиотечных решениях

В мире написано огромное количество библиотек, заявляющих что так или иначе решают проблему внедрения зависимостей. Однако большая часть из них основана на паттерне Service Locator. Справедливости ради стоит сказать, что наиболее популярная библиотека Dagger 2 для Android пытается использовать концепцию контейнера, и в Sevice Locator его чаще всего превращают при использовании.

Идея решения проблемы

Как уже было сказано выше - будем использовать концепцию контейнера.

Контейнер - это такая сущность, которыя

  • Создает нужные объекты и управляет их временем жизни
  • Настраивает связи между ними
  • Невидима для самих объектов

Каждая конкретная логическая сущность (кроме самих контейнеров) заявляет необходимость зависимостей (через constructor или property injection) и все. Никоим образом логическая сущность не получает доступа не к типу контейнера, ни к его инстансу (кроме как через Factory интерфейсы там где нужна prototype зависимость).

Отступление про нейминг. Однажды я увидел, как в коде контейнеры называют графами (Graph) и мне эта идея так понравилась, что далее буду называть типы контейнеров графами, можно в рамках этой заметки считать эти термины эквивалентными для задач DI.

Таким образом простейший контейнер может выглядеть так

  • final class SomeGraph {
    
        // 'singleton' entities
        private let dep1: Dep1
        private let dep2: Dep2
        private let dep3: Dep3
    
        // other graphs
        private let subGraph: SomeSubGraph
    
        init(
            someGraphDependencies: SomeGraphDependencies,
            someSpecificDependency: SomeSpecificDependency,
            configuration: SomeGraphConfiguration,
            ...
        ) {
            self.dep1 = Dep1(
                /* ... */,
                makeSome: {
                    return self.makeSomePrototypeDep(val: configuration.val)
                }
            )
            self.dep2 = Dep2(/* ... */, useVal: configuration.useVal)
            self.dep3 = Dep3(dep1: Dep1, dep2: Dep2)
            // ...
            self.subGraph = SomeSubGraph(
                /* ... */
            )
        }
    
        deinit {
            // тут некоторая логика очистки сущностей, которые того требуют
        }
    
        // 'prototype' entities
        private func makeSomePrototypeDep(val: Val1) -> Dep4 {
            // ...
            return Dep4(
                val: val
            )
        }
    }
    
  • class SomeGraph(
        someGraphDependencies: SomeGraphDependencies,
        someSpecificDependency: SomeSpecificDependency,
        private val configuration: SomeGraphConfiguration,
        ...
    ) {
    
        // 'singleton' entities
        private val dep1: Dep1
        private val dep2: Dep2
        private val dep3: Dep3
    
        // other graphs
        private val subGraph: SomeSubGraph
    
        init {
            this.dep1 = Dep1(
                /* ... */,
                makeSome = {
                    return makeSomePrototypeDep(val = configuration.val)
                }
            )
            this.dep2 = Dep2(/* ... */, useVal = configuration.useVal)
            this.dep3 = Dep3(dep1 = Dep1, dep2 = Dep2)
            // ...
            this.subGraph = SomeSubGraph(
                /* ... */
            )
        }
    
        fun cleanup() {
            // в Java/Kotlin нет полноценных деструкторов, поэтому cleanup методы
            // необходимо будет вызывать руками
            // тут некоторая логика очистки сущностей, которые того требуют
        }
    
        // 'prototype' entities
        private fun makeSomePrototypeDep(val: Val1): Dep4 {
            // ...
            return Dep4(
                val = val
            )
        }
    }
    

И все - собирайте нужную конструкцию из иерархических графов. Когда какой-то набор сущностей станет ненужным - зануляете граф и все очищается (в Android не забываем звать cleanup).

PROFIT? не совсем, есть тонкости

iOS

В iOS как правило все проходит без проблем. Мы создаем корневой граф-контейнер в applicationWill/DidLaunchingWithOptions и передаем его целиком или частями в дальнейшие сущности, или создаем его в корневом объекте помеченном @main и также передаем целиком или частями дальше. Главное не забывать о сужении интерфейсов и принципе достаточной необходимости.

Android

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

Заведем пару контрактов

// любая сущность, в которую наш DI сможет что-то инжектить
interface Injectable

// контракт сущности контейнера, который сможет что-то куда-то инжектить
interface Injector {
    fun inject(into: Injectable) // можно опционально return Result
}

Чуть выше в разделе про Service Locator я уже говорил, что основная проблема в нем в том, что

  • его shared instance известен публично, и может быть использован скрыто. С этим увы ничего не поделать, андроидные компоненты устроены так, что нам придется пойти на открытие какого-то shared instance. Для activity/fragment есть лазейка, о ней чуть позже, но в общем итоге не поделать ничего.
  • из него можно достать что угодно, эту проблему будем решать.

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

Теперь нам нужно предусмотреть сущность, которую мы будем видеть через shared instance, но не моч из нее ничего достать

class SomeGraph private constructor(
    private val dependency: SomeDependnecy,
    // other deps
): Injector {
    override fun inject(target: Injectable) = when (target) {
        is SomeKnownFragment -> {
            target.dependency = dependency
        }
        else -> {
            // report error in some kind
        }
    }

    companion object {
        var sharedInstance: SomeGraph? = null
            private set

        fun setup(
            dependency: SomeDependency
        ) {
            synchronized(this) {
                if (sharedInstance != null) {
                    return
                }
                sharedInstance = SomeGraph(
                    dependency = dependency
                )
            }
        }
    }
}

И теперь в условном фрагменте мы можем написать так

class SomeKnownFragment: Fragment(), Injectable {

    lateinit var dependency: Dependency? = null

    override fun onAttach(context: Context) {
        super.onAttach(context)
        SomeGraph.sharedInstance?.inject(this)
    }

Как я уже сказал, есть некоторая проблема в том, что связывание будет проверено только в runtime, а правильнее было бы сделать в compile time, но архитектура компонентов Android’а тут этому препятствует.

К слову такой подход хорошо подходит для библиотек, этот SomeGraph может быть объектов библиотеки, которую надо проинициализировать и потом сама библиотека будет ее использовать. Для Activity/Fragment и тд можем сделать лучше, для этого нам потребуется еще немножко сахара (покажу на примере Activity, для фрагментов можно сделать аналогично)

fun Activity.inject() {
    val thisAsInjectable = this as? Injectable ?: throw InjectException("activity not injectable")
    val application = application ?: throw InjectException("application isn't set to activity")
    val injector = application as? Injector ?: throw InjectException("application is not an injector")
    injector.inject(thisAsInjectable)
}

теперь надо класс приложения разметить соответствующим образом

class YourApp: Application(), Injector {

    private lateinit var rootGraph: RootGraph

    override fun onCreate() {
        super.onCreate()
        rootGraph = RootGraph(this)
    }

    override fun inject(target: Injectable) {
        if (!this::graph.isInitialized) {
            error("Application isn't properly initialized yet")
        }
        graph.inject(target)
    }
}

корневой граф будет выглядеть примерно так

class RootGraph(
    applicationContext: Context
): Injector {

    // private val yourdeps = ...

    init {
        // тут инициализация нужных компонентов
    }

    override fun inject(target: Injectable) {
        when (target) {
            is MainActivity -> {
                target.depepdency = ...
            }
            // можно делегировать что-то субграфам
        }
    }
}

И в самой активити будет просто

class MainActivity : Activity(), Injectable {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        inject()

        // остальная инициализация
    }
}

Никаких синглетонов и никакого раскрытия лишних зависимостей - PROFIT.

P.S. Если придумаете как решить эту проблему с аналогичными гарантиями да еще и в compile time буду благодарен

Внедрение зависимостей (dependency injection) в Swift 5.1

 
 
   

Внедрение зависимостей очень горячая тема в любой области разработки, где мы пишем что-то более сложное чем Hello, World. Однако несмотря на казалось бы изученный вдоль и поперек вопрос, вариантов его решения вы можете на просторах интернета найти великое множество. И в каждом месте оно подается как единственно правильное. И как же выбрать? Предлагаю в этой статье немножко рассмотреть подходы, их плюсы и минусы, немножко поиграться со Swift’ом вообще и попробовать его новые фичи в виде @PropertyWrapper’s.

Итак, постановка задачи у нас будет такая - у нас есть два класса BooksRenderer, который просто каким-то образом рисует книжки, и BooksProvider, который ему их поставляет. На Swift это будет выглядеть примерно так:

final class BooksRenderer {
    let provider: BooksProvider = ... /* за это троеточие и будет вестись основная борьба */

    func draw() {
        let books = provider.books
        /* тут каким-то образом рисуются книги из массива books */
    }
}

protocol BooksProvider {
    var books: [Book] { get }
}

Будем также считать что есть некая реализация протокола BooksProvider, например такая наивная

final class NaiveBooksProvider: BooksProvider {
    var books: [Book] {
        return [
            Book(title: "Dune", author: "Frank Herbert"),
            Book(title: "Lord of the Rings", author: "John R.R. Tolkien")
        ]
    }
}

Теперь наша задача каким-то образом доставить экземпляр класса NaiveBooksProvider в BooksRenderer. Самый наивный подход такой, создать экземлляр класса прямо на месте:

final class BooksRenderer {
    let provider: BooksProvider = NaiveBooksProvider()
}

Несмотря на то, что этот подход, каким бы наивным он не был, много где применяется, у него есть очевидные недостатки:

  1. Мы можем захотеть как-то менять конкретный класс реализации, а он тут “прибит гвоздями”
  2. Мы можем захотеть unit-протестировать класс BooksRenderer, и тогда вместо провайдера захотим вставить какой-нибудь мок и т.д.

Нам надо что-то лучше. И много где предлагают хорошо известный паттер ServiceLocator. Если его применить, то выглядеть это будет примерно так:

final class ServiceLocator {
    static let booksProvider: BooksProvider = NaiveBooksProvider()
}

final class BooksRenderer {
    let provider: BooksProvider = ServiceLocator.booksProvider
}

Уже лучше, ответственность за выбор конкретного класса мы достали из BooksRenderer и наделили этой почетной обязанностью класс ServiceLocator. И мы даже можем сделать разные ServiceLocator’ы для основного приложения и для тестов, которые будут создавать разные BooksProvider’ы, однако:

  1. Теперь 90% кода будет знать про класс ServiceLocator
  2. Класс ServiceLocator будет огромным (кто там что говорил про Massive View Controller?, у нас тут Massive Service Locator)

Прежде чем пойти дальше, давайте сделаем некоторое лирическое отступление, разберемся в терминологии зависимостей. Вообще внедряемых зависимостей может быть два типа: прости хоспади singleton (но это не то что вы подумали) и prototype. “singleton” зависимости - это такие зависимости, которые сколько бы вы не внедряли в рамках одного конкретного модуля, это всегда будет один экземпляр. “prototype” же - даст на каждую точку внедрения новый экземпляр.

Поэтому если говорить про наш пример с ServiceLocator’ом, то например booksProvider - это singleton зависимость, а bookUpdateOperation - prototype:

final class ServiceLocator {
    static let booksProvider: BooksProvider = NaiveBooksProvider()

    static func bookUpdateOperation() -> Operation & BookUpdate {
        return NaiveBookUpdateOperation(...)
    }
}

Теперь давайте сделаем еще одно лирическое отступление, подчерпнутое мной когда я еще занимался “кровавым” enterprise и работал с Srping Framework. Хороший DI контейнер это такой контейнер, который не видно. Тут можно еще пофилософствовать и вспомнить ТРИЗ с ее идеальным конечным результатом, который на наш DI’ный контекст перефразируется так: “хороший DI контейнер - это такой, которого нет, а зависимости внедряются”.

Таким образом, можно сделать такой DI на базе initializer injection (оно лучше property injection, потому что компилятор в этом случае не даст вам озорничать, а с property injection легко забыть что-нибудь присвоить и грохнуться в рантайме):

final class AppContainer {
    let booksProvider: BooksProvider

    init(with appDelegate: AppDelegate) {
        booksProvider = NaiveBooksProvider(...)
        appDelegate.booksRenderer = BooksRenderer(provider: booksProvider)
    }
}

@UIApplicationMain
final class AppDelegate: UIResponder {
    let container: AppContainer
    var booksRenderer: BooksRenderer!

    func applicationDidFinishLauncherWithOptions(...) {
        container = AppContainer(with: self)
    }
}

final class BooksRenderer {
    let provider: BooksProvider

    init(provider: BooksProvider) {
        self.provider = provider
	}
}

Причем такой подход будет гарантировать вам проверку компилятором. И при этом про AppContainer будет знать только AppDelegate. Да, корневые зависимости в самом AppDelegate’е будут force unwrapped (что исть не хорошо, но лучше я не придумал), но эта вольность доступна только тут.

prootype зависимости в таком подходе можно оформить либо в виде фабрики

final class SomeFactory {
    let superDep: SuperDep

    init(superDep: SuperDep) {
        self.superDep = superDep
    }

    func makeSomeDep(...) -> SomeDep {
        return SomeDep(superDep, ...)
    }
}

и потом внедрять это фабрику как singleton зависимость туда где нужно генерить prototype’ные, или в виде замыкания

typealias SomeFactory = (_ superDep: SuperDep, ...) -> SomeDep { SomeDep(superDep, ...) }

и также ее внедрять как singleton зависимость.

Но я обещал немножко Swift 5.1 и @PropertyWrapper, их легко сделать так (пусть будет наш пример с ServiceLocator’ом, хотя его можно легко модифицировать):

@propertyWrapper
public class Inject<Dep> {
    private let name: String?
    private var kept: Dep?

    public var wrappedValue: Dep {
        kept ?? {
            let dependency: Dep = ServiceLocator.resolve(for: name)
            kept = dependency
            return dependency
        }()
    }

    public convenience init() {
        self.init(nil)
    }
    
    public init(_ name: String?) {
        self.name = name
    }
}

final class ServiceLocator {
    static func register<T>(for name: String, resolver: @escaping () -> T) {
        // register
    }

    static func resolve<T>(for name: String?) -> T {
        // do some magic
    }
}

final class BooksRenderer {
    @Inject private var provider: BooksProvider
}

И вуаля! Однако несмотря на всю прелесть такой магии, есть проблема в месте где творится магия (“do some magic”). Если вы вдруг забыли сделать register, то упс, вы получаете рантайм крэш. И сделать это красиво с compile time check непонятно как, так как регистрация динамическая.

Предлагаю подискутировать, оформляйте issues тут

P.S.

Разрабатываем и отлаживаем С++ в Docker с помощью VSCode (2019 edition - clang8 lldb)

     
     

Какое-то время назад я написал статью Разрабатываем и отлаживаем С++ в Docker с помощью VSCode в котором для сборки использовался gcc и для отладки gdbserver. В этом обновленном посте хочу затронуть такой же вопрос, но уже на базе clang 8 и lldb-server.

Также в отличии от предыдущей статьи мы не будем каждый раз пересоздавать контейнер, а запустим его один раз и будет с ним работать через docker exec.

Итак, начнем, Dockerfile

FROM ubuntu:18.04

RUN apt-get update 
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential xz-utils curl cmake
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y lldb

RUN curl -SL http://releases.llvm.org/8.0.0/clang+llvm-8.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz | tar -xJC . && \
    mv clang+llvm-8.0.0-x86_64-linux-gnu-ubuntu-18.04 clang_8.0.0 && \
    mv clang_8.0.0 /usr/local

ENV PATH="/usr/local/clang_8.0.0/bin:${PATH}"
ENV LD_LIBRARY_PATH="/usr/local/clang_8.0.0/lib:${LD_LIBRARY_PATH}"

WORKDIR /opt/build
VOLUME ["/opt"]

lldb (для lldb-server) используем поставляемый в пакетах (на момент написания статьи для 18.04 это был LLDB 6). Также из пакетов ставим cmake (вроде ничего специфичного пока не надо). clang ставим руками, но уже собранный.

Дальше нам нужен скрипт сборки контейнера (/scripts/docker_build.sh)

#!/bin/bash

cwd=$(pwd)

docker build \
    -t something/cmaker \
    .

И таска для vscode

{
    "label": "build docker container",
    "command": "${workspaceFolder}/scripts/docker_build.sh"
}

после этого нам понадобится запустить контейнер для последующего использования, для этого нам нужен скрипт (/scripts/docker_run.sh). Тут попутно запускается lldb-server, о нем позже:

#!/bin/bash

cwd=$(pwd)

docker stop сmaker
docker rm сmaker
docker run \
    -dt \
    --name сmaker \
    -p 7000:7000 \
    -p 7001:7001 \
    -p 7002:7002 \
    -p 7003:7003 \
    -v ${cwd}:/opt \
    --privileged \
    something/cmaker \
    "${@}"

и таска для vscode

{
    "label": "run container for future builds",
    "command": "${workspaceFolder}/scripts/docker_run.sh",
    "args": [
        "lldb-server", "platform", "--server", "--listen=0.0.0.0:7000", "-m", "7001", "-M", "7003"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    }
}

После этого можем приступать к сборке проекта. Структуру проекта сразу предусмотрел для многомодульного проекта

include
scripts
  docker_build.sh
  docker_run.sh
hword
  include
  CMakeLists.txt
  main.cpp
CMakeLists.txt
Dockerfile

Корневой CMakeLists.txt выглядит так

cmake_minimum_required(VERSION 3.0)
project(something)

set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/build)

set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR})

set(PROJECT_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)

include_directories("${PROJECT_INCLUDE_DIR}")
include_directories("${PROJECT_SOURCE_DIR}")

add_subdirectory(hword)

CMakeLists.txt для hword выглядит так

cmake_minimum_required(VERSION 3.0)
project(hword)

set(PROJECT_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)
set(HWORD_VERSION_MAJOR 1)
set(HWORD_VERSION_MINOR 0)

set(PROJECT_HWORD_SRCS
${PROJECT_SOURCE_DIR}/main.cpp
)

include_directories("${PROJECT_BINARY_DIR}")
add_executable(${PROJECT_NAME}_r ${PROJECT_HWORD_SRCS})
include_directories("${PROJECT_INCLUDE_DIR}")

Обратите внимание что к имени проекта добавлен постфикс _r

Для примера содержимое main.cpp

#include <unistd.h>
#include <limits.h>

#include <iostream>

using namespace std;

static const int HNAME_MAX = 512;
static const int LNAME_MAX = 512;

int main()
{
    char hostname[HNAME_MAX];
    char username[LNAME_MAX];
    gethostname(hostname, HNAME_MAX);
    getlogin_r(username, LNAME_MAX);

    cout << "hostname: " << hostname << ", username: " << username << endl;
    return 0;
}

Теперь соберем все это дело, запустим и отладим

Нам понадобится скрипт для docker exec

#!/bin/bash

cwd=$(pwd)

docker exec \
    -it \
    cmaker \
    "${@}"

Таска для cmake

{
    "label": "cmake initialize scripts (Makefile, Debug)",
    "command": "${workspaceFolder}/scripts/docker_exec.sh",
    "args": [                
        "cmake", "-G", "Unix Makefiles", 
        "-DCMAKE_BUILD_TYPE=Debug", 
        "-DCMAKE_C_COMPILER=/usr/local/clang_8.0.0/bin/clang",
        "-DCMAKE_CXX_COMPILER=/usr/local/clang_8.0.0/bin/clang++",
        "/opt"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    }
}

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

Далее таска для сборки всего проекта и для сборки конкретного

{
    "label": "make full project",
    "command": "${workspaceFolder}/scripts/docker_exec.sh",
    "args": [
        "make", "-j", "8"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    }
}
{
    "label": "make (hword) project",
    "command": "${workspaceFolder}/scripts/docker_exec.sh",
    "args": [
        "make", "-j", "8", "hword_r"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    }
}

И для простого запуска

{
    "label": "run (hword) project",
    "command": "${workspaceFolder}/scripts/docker_exec.sh",
    "args": [
        "/opt/build/hword_r"
    ]
}

С отладкой есть несколько проблем. Во-первых, кроме порта на котором слушает lldb-server надо пробросить еще несколько, на которых будет работать собственно соединение (для этого при запуске контейнера несколько опций -p). Во-вторых, lldb-server’у этот диапазон портов надо явно задать. И в третьих, надо правильно написать launch.json, выглядит он так:

{
    "name": "debug: hword",
    "type": "lldb",
    "request": "launch",
    "program": "/opt/build/hword_r",
    "initCommands": [
        "platform select remote-linux",
        "platform connect connect://127.0.0.1:7000"
    ],
    "cwd": "/opt",
    "sourceMap": { 
        "/opt" : "${workspaceFolder}" 
    }
}

Жмем F5 и выуля - мы в отладке. Надеюсь это поможет :)

Сравнение Swift vs Kotlin

 
   

Небольшая табличка сравнения кода на Swift и кода на Kotlin для справки. Этакий cheet sheet.

Переменные и константы

Swift

let a: Int = 10
var b: Int = 20

Kotlin

val a: Int = 10
var b: Int = 20

Optionals

Swift

let some: Something? = nil
var some: Something? = Something(text: "Hello")

// optional chaining
let greeting = some?.greet()

// elvis
let value = some?.value ?? 30

// optional unwraping
if let some = some {
    print(some) // some is not optional
}

// force unwraping
let forced = some!

Kotlin

val some: Something? = nil
var some: Something? = Something("Hello")

// optional chaining
val greeting = some?.greet()

// elvis
val value = some?.value ?: 30

// optional unwraping
some?.let { some -> // it if name is not provided
    print(some) // some is not optional
}
if (some != null) {
    print(some) // some is smart casted and not optional
}

// force unwraping
val forced = some!!

Форматирование кода

Скобки (круглые и фигурные)

Swift

  • Круглые - не обязательны
  • Фигурные - обязательны
let str = "Hello, Swift"

func increse(_ value: Int) -> Int {
    return value + 1
}

func statements() {
    var i = 0
    while i < 10 {
        i = increase(i)
        if str == "something" {
            print(str)
        }
    }
}

Kotlin

  • Круглые - обязательны
  • Фигурные - не обязательны
val str = "Hello, Kotlin"

fun increase(value: Int): Int = value + 1

fun statements() {
    var i = 0
    while (i < 10) i = increase(i)
    if (str == "something") print(str)
}

Управляющие конструкции

Pattern Matching

Swift

let number = 42
switch number {
    case 0...7, 8, 9: print("1 digit")
    case 10: print("2 digits")
    case 11...99: print("2 digits")
    case 100...999: print("3 digits")
    default: print("4 or more digits")
}

Kotlin

val number = 42
when (number) {
    in 0..7, 8, 9 -> println("1 digit")
    10 -> println("2 digits")
    in 11..99 -> println("2 digits")
    in 100..999 -> println("3 digits")
    else -> println("4 or more digits")
}

if, when = expressions, not statements (!)

Swift

  • Не применимо

Kotlin

val test = if (true) "Test" else "False"

val state = State.Off
fun decide() = when(state) {
    State.Off -> "Off"
    State.On -> "On"
}
val decision = decide()

Типы

Любой тип

Swift

let a: Any = "Hello"
let b: AnyObject = SomeClass()

Kotlin

val a: Any = "Hello"
// no AnyObject yet

Строковая интерполяция

Swift

let str = "Hello \(nickname)"
let str2 = "Hello \(user.nickname)"

Kotlin

val str = "Hello $nickname"
val str2 = "Hello ${user.nickname}"

Интервалы (Ranges)

Swift

let rng1 = 0...10
let rng2 = 0..<10

Kotlin

val rng1 = 0..10
// no rng2

Коллекции

Swift

let stringArray = [String]()
// no stringList
let stringFloatMap = [String: Float]()
let stringSet = Set<String>()

var countries: [String] = ["Switzerland", "France", "Germany"]
countries.append("Italy")
countries.remove("France")

var jobs: [String: String] = [
    "Roger": "CEO",
    "Martin": "CTO"
]
jobs["Adrian"] = "Writer"

Kotlin

  • Нельзя использовать [] для инициализации массива, используется arrayOf
  • в arrayOf добавлять и удалять элементы нельзя, можно только их менять
val stringArray = arrayOf<String>()
val stringList = listOf<String>()
val stringFloatMap = mapOf<String, Float>()
val stringSet = setOf<String>()

val countries = mutableListOf<String>("Switzerland", "France", "Germany")
countries.add("Italy")
countries.remove("France")

val jobs = mutableMapOf(
    "Roger" to "CEO",
    "Martin" to "CTO"
)
jobs["Adrian"] = "Writer"

Итерирование коллекций

Swift

for str in stringArray {}
for (index, str) in stringArray.enumerated() {}
// no for str in stringList
for (str, num) in stringFloatMap {}
for str in stringSet {}

Kotlin

for (str in stringArray) {}
for (str in stringList) {}
for ((str, num) in stringFloatMap) {}
for (str in stringSet) {}

Процедурное программирование

Процедуры и функции

Swift

func method1(input: String) -> Int {
    return input.count
}

// no method2

func method3<T>(input: T) -> String {
    return String(describing: input)
}

Kotlin

fun method1(input: String): Int {
    return input.length
}

fun method2(input: String) = input.length

fun <T>method3(input: T) = input.toString()

ООП

Интерфейсы

Swift

protocol Person {
    var name: String { get set }
    
    func greet() -> String
    
    func showMoreInformation()
}

extension Person {
	func greet() -> String {
	    return "Hello! I am \(self)"
	}
}

Kotlin

interface Person {
    
    var name: String
        get set

    
    fun greet() = "Hello! I am $this"

    
    fun showMoreInformation()
}

Конструкторы

Swift

class Manager: Person {
    var backingName: String
    var staff: [Person]
    var state: State
    let isActive: Bool
    init(backingName = "": String, staff: [Person] = [], state: State = .off) {
        self.backingName = backingName
        self.staff = staff
        self.state = state
        self.isActive = true
    }

Kotlin

class Manager(private var backingName: String = "",
              private var staff: MutableList<Person> = mutableListOf<Person>(),
              var state: State = State.Off) : Person {
    private val isActive: Boolean
    init {
        isActive = true
    }
}

Data Classes (PONSO/POSS vs POJO)

Swift

struct Employee {
    let backingName: String
    let age: Int
    init(backingName: String = "", age: Int = 30) {
        self.backingName = backingName
        self.age = age
    }
}

Kotlin

data class Employee(private var backingName: String = "",
                    var age: Int = 30) : Person {

Инстанцирование объектов

Swift

let person = Employee(
    name: "Olivia", 
    age: 45
)

Kotlin

val person1 = Employee("Olivia", 45) 

val person2 = Employee().apply { 
    name = "Thomas"
    age = 56
}

Расширения классов

Swift

extension Double {
    var fahrenheit: Double {
	    (self * 9 / 5) + 32
    }
    var celsius: Double {
       (self - 32) * 5 / 9
    }
}

let temperature = Double(32.0)
let fahrenheit = temperature.fahrenheit
let celsius = fahrenheit.celsius
print("\(temperature) degrees Celsius is \(fahrenheit) degrees Fahrenheit")

Kotlin

val Double.fahrenheit: Double get() = (this * 9 / 5) + 32
val Double.celsius: Double get() = (this - 32) * 5 / 9

val temperature: Double = 32.0
val fahrenheit = temperature.fahrenheit
val celsius = fahrenheit.celsius
println("$temperature degrees Celsius is $fahrenheit degrees Fahrenheit")

Простые объекты и Singleton’ы

Swift

enum Constants {
    static let PI = 3.14
    static let ANSWER = 42
    
    static func name() -> String {
        return "Math constants"
    }
}

Kotlin

object Constants {
    val PI = 3.14
    val ANSWER = 42

    fun name() = "Math contstants"
}

Объекты-компаньоны

Swift

  • Просто используйте static (class) члены

Kotlin

  • В Kotlin нет статических членов
companion object OptionalName {
    val MAXIMUM_EMPLOYEE_COUNT = 10

    fun managerFactory() = Manager("Maria Hill")
}

Перегрузка операторов

Swift

  • Можно делать свои операторы (например квадратный корень)
func + (lhs: Person, rhs: Person) -> Team {
    return Team(lhs, rhs)
}

let team = manager + person

Kotlin

operator fun plus(person: Person): Team {
    return Team(this, person)
}

val team = manager + person

Инфиксные методы

Swift

  • Не применимо

Kotlin

infix fun addPerson(person: Person) {
    if (staff.count() < MAXIMUM_EMPLOYEE_COUNT) {
        staff.add(person)
    }
    else {
        throw Exception("Cannot add more staff members")
    }
}

manager addPerson person

Перечисления

Swift

enum State {
    case on
    case off
    
    var desc: String { ... }
    func notify() { ... }
}

Kotlin

enum class State {
    On, Off
    
    var desc: String { ... }
    func notify() = ...
}

Резюмирующая табличка

  Kotlin 1.2 Objective-C 2.0 Swift 4
Наследование Одинарное, с интерфейсами и расширениями Одинарное, с протоколами Одинарное, с протоколами и расширениями
Точки с запятой ; Не обязательны Обязательны Не обязательны
Объявление класса class @interface & @implementation class
Интерфейсы реализует interface поддерживает @protocol поддерживает protocol
Подключение кода import (символы) #import (файлы) import (символы)
Расширение классов Расширения Категории Расширения
Динамическая типизация Any id Any
Суффикс приватных полей Не используется _ (подчеркивание) _ (подчеркивание)
Управление памятью Сборка мусора (GС) Ручное или автоматическое управление памятью (MRC vs ARC) Автоматическое управление памятью (ARC)
Дженерики да (type erasure) да (type erasure) да
Указатели на метод нет @selector #selector
Обратный вызов (callbacks) Лямбды Делегаты и блоки замыкания
Указатели нет да Через библиотечные классы
Корневые классы Object NSObject / NSProxy / … NSObject / NSProxy / …
Управление областью видимости public / internal / protected / private @public / @protected / @private (только поля) open / public / internal / fileprivate / private
Обработка ошибок try / catch / finally + Exception @try / @catch / @finally + NSException do / try / catch + Error
Пространства имен Пакеты Через префиксы классов Неявные, через модули
Грамматика kotlinlang.org   developer.apple.com

Разрабатываем и отлаживаем С++ в Docker с помощью VSCode

     
 

Продолжаем цикл статей “Как кодить на С++ под OSX для Ubuntu”. После сборки cross-toolchain’ов стояла задача научиться собирать, запускать и отлаживать С++ под OSX для Linux (Ubuntu в данном случае). Как оказалось cross-toolchain был не нужен :(. Но об этом позже.

Для начала нужна была IDE, в которой можно было бы комфортно редактировать код и отлаживаться. Претенденты были такие: Xcode, Eclipse (CDT), CLion, чуть позже к ним добавился VSCode и впоследствии выиграл соревнование.

Постановка задачи была такая: C++, CMake, Linux, при этом редактирование кода и отладка должна работать под OSX.

Xcode поэтому выпал из этой гонки сразу, он есть очень удобная IDE для C++, но в целом в основном для OSX. CMake наверное завести можно, но это будет большое количество сторонних средств.

CLion выпал из этой же гонки примерно по тем же причинам (хотя он вроде умеет в CMake). Плюс я не смог в нем нормально подружиться с Docker.

Остался Eclipse CDT, в нем получилось практически все что требовалось (с cross-toolchain), завелась даже отладка через cross-gdb, однако были странные махинации чтобы нормально работать с CMake и контейнером он управлял относительно сам (не совсем прозрачно), хотя надо отметить что поддержка Docker в Eclipse плюс-минус хорошая.

После этого я пошел посерфить интеренет в поисках каких-нибудь идей, как нормально подружить Eclipse и CMake, и тут я наткнулся на VSCode. Которые не позиционирует себя как IDE, но умеет в дебаг и всякие user-defined task’и. Несмотря на мое некоторое отношение к продуктам Microsoft, я решил дать этому редактору шанс и попробовал его… и надо признать, это очень хороший инструмент.

Теперь же собственно, как завести, чтобы все работало.

Для начала нам понадобится собственно VSCode, настроенный так, как вам наиболее удобно. Можно сразу поставить расширения для работы с C++ (C/C++, Native Debug), CMake (CMake, CMake Tools), Docker.

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

.vscode
  tasks.json
  launch.json
build
CMakeLists.txt
dbuild.sh
ddebugger.sh
dmaker.sh
Dockerfile
main.cpp

Начнем с С++, пример простой, однако чтобы понять что мы действительно работаем для Linux я добавил несколько linux’овых вещей.

#include <unistd.h>
#include <limits.h>

#include <iostream>

using namespace std;

int main() {
	char hostname[HOST_NAME_MAX];
	char username[LOGIN_NAME_MAX];
	gethostname(hostname, HOST_NAME_MAX);
	getlogin_r(username, LOGIN_NAME_MAX);

	cout << "hostname: " << hostname << ", login: " << username << endl; // prints Hello World!
	return 0;
}

Этот код мы будем собирать, запускать и отлаживать.

Далее CMakeLists.txt, который тоже простой и относительно шаблонный:

cmake_minimum_required(VERSION 3.5)

project(HWord)

set(HWORD_VERSION_MAJOR 1)
set(HWORD_VERSION_MINOR 0)

set(SOURCE ${PROJECT_SOURCE_DIR}/main.cpp)

add_executable(${PROJECT_NAME} ${SOURCE})

Собираем

Как я уже выше писал сначала я пытался собрать все это безобразие под OSX с помощью cross-toolchain. Это плюс-минус работало, но часто приходилось пересобирать toolchain с еще каким-нибудь волшебным флагом, а на моем машине это делается примерно 3 часа. В одну из таких пересборок мне пришла мысль, а почему бы не собирать docker’ом, т.е. gcc который в контейнере и в требуемой системе (linux).

Таким образом собираем такой контейнер:

Dockerfile

FROM ubuntu:16.04

RUN apt-get update 
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y gcc g++ gdb cmake

WORKDIR /opt/build
VOLUME ["/opt"]

Для упрощения работы с командной строкой docker’а заведем пару скриптов:

Скрипт сборки образа:

dbuild.sh

#!/bin/bash

cwd=$(pwd)

docker build \
    -t mrdekk/maker \
    .

Ничего сверхординарного, но возможно вам лучше поставить свой тег, вместо mrdekk/maker сделать %username%/maker

Скрипт запуска сборок:

dmaker.sh

#!/bin/bash

cwd=$(pwd)

docker stop maker
docker rm maker
docker run \
	-it \
	--name maker \
	-p 6666:6666 \
	-v ${cwd}:/opt \
	--privileged \
	mrdekk/maker \
    "${@}" 

Скрипт запуска контейнера для отладки:

ddebugger.sh

#!/bin/bash

cwd=$(pwd)

docker stop maker
docker rm maker
docker run \
	-dt \
	--name maker \
	-p 6666:6666 \
	-v ${cwd}:/opt \
	--privileged \
	mrdekk/maker \
    "${@}" 

Скрипты запуска сборки и дебаггера в общем отличаются только ключами -it и -dt.

Теперь сделаем несколько задач (tasks) для VSCode

Задача сборки контейнера:

Ничего особенного, вызываем скрипт dbuild.sh

{
    "label": "Docker: Build Containers",
    "command": "${workspaceFolder}/dbuild.sh",
},

И заодно выполняем ее, после этого можете сделать docker images и проверить, что образ mrdekk/maker появился

Задача для CMake:

Теперь с помощью CMake сгенерим Makefile’ы для сборки проекта. Здесь мы воспользуемся скриптом dmaker.sh, но будем вызывать его с набором ключей:

{
    "label": "CMake: Initialize",
    "command": "${workspaceFolder}/dmaker.sh",
    "args": [
        "cmake", "-G", "'Unix Makefiles'", "-DCMAKE_BUILD_TYPE=Debug", "/opt"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    }
},

/opt это путь до Volume’а в контейнере в который мы будем проецировать текущую папку с исходниками проекта - это в общем часть магии.

После этого в подпапке build вы должны увидеть сгенерированные cmake’ом файлы.

Задача для сборки бинарника:

Здесь снова используем скрипт dmaker.sh но с другими ключами

{
    "label": "Make: Build Project",
    "command": "${workspaceFolder}/dmaker.sh",
    "args": [
        "make", "-j", "8"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    },
    "group": {
        "kind": "build",
        "isDefault": true
    }
},

После того, как собереться можете попробовать сделать так ./build/HWord на OSX, и вы должны получить примерно такое:

exec format error: ./build/HWord

Это правильно, наш бинарник собран для linux и на OSX запускаться не должен. Если же вы увидите вывод программы, это значит что что-то пошло не так (вы собрали OSX’овым компилятором).

Запускаем без отладки

Теперь у нас есть бинарник, надо проверить что он запускается. Для этого снова воспользуемся скриптом dmaker.sh и такой задачей:

{
    "label": "Docker: Run Containers",
    "command": "${workspaceFolder}/dmaker.sh",
    "args": [
        "/opt/build/HWord"
    ]
},

После этого вы должны увидеть в docker logs maker и терминале VSCode примерно такое

hostname: 03a3a4dee13f, login:

Это хорошо, наш бинарник на linux работает хорошо.

Отлаживаемся

С помощью VSCode поставьте breakpoint куда-нибудь в коде. Нам будет необходим запущенный в фоне контейнер, внутри которого мы через docker exec будем запускать gdb. Увы, завести VSCode так, чтобы он смог нормально в интерактивном режиме (dmaker.sh и -it) запускать debugger у меня не получилось (видимо механизм запуска дебаггера в VSCode как-то так хитро устроен), поэтому я воспользовался pipeTransport и все заработало.

Таким образом задача для предварительного запуска контейнера:

{
    "label": "Docker: Run Debugging Container",
    "command": "${workspaceFolder}/ddebugger.sh",
    "args": [
        "bash"
    ]
}

а launch.json выглядит так

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Container Debug",
            "type": "cppdbg",
            "request": "launch",
            "program": "/opt/build/HWord",
            "cwd": "/opt",
            "linux": {
                "MIMode": "gdb",
                "setupCommands": [
                    {
                        "description": "Enable pretty-printing for gdb",
                        "text": "-enable-pretty-printing",
                        "ignoreFailures": true
                    }
                ]
            },
            "osx": {
                "MIMode": "gdb",
                "setupCommands": [
                    {
                        "description": "Enable pretty-printing for gdb",
                        "text": "-enable-pretty-printing",
                        "ignoreFailures": true
                    }
                ]
            },
            // "preLaunchTask": "Docker: Run Debugging Container",
            "pipeTransport": {
                "pipeProgram": "docker",
                "pipeCwd": "${workspaceRoot}",
                "pipeArgs": [
                    "exec", "-i", "maker", "sh", "-c"
                ],
                "quoteArgs": false,
                "debuggerPath": "/usr/bin/gdb"
            },
            "sourceFileMap": {
                "/opt":"${workspaceFolder}"
            },
        }
    ]
}

Внимательно ко всем параметрам, они все имеют значение.

После этого жмем F5 (или в меню выбираем Start Debugging) и… отлаживаемся. Дебаггер должен остановить выполнение в установленном вами breakpoint’е.

Примеры кода и исходники к статье лежат тут mrdekk/viscose-cpp-docker.

P.S. Да, я тоже ненавижу автокомплит, когда он что-то исправляет. В названии репозитория должно было быть vscode-cpp-docker, а получилось viscose-cpp-docker. Но, ладно! Так тому и быть.