Pull to refresh
392.42
Ozon Tech
Команда разработки ведущего e‑com в России

Императивный UIKit‍ vs Декларативный SwiftUI

Reading time15 min
Views12K

Для разработки iOS-приложений можно использовать два основных фреймворка: UIKit и SwiftUI. Однако при переходе со старого инструмента на новый, многие разработчики сталкиваются с трудностями, ведь парадигмы программирования у них сильно отличаются.

Цель статьи

Помочь разработчикам приложений для iOS понять различия между императивным и декларативным подходами к программированию, а также рассмотреть плюсы и минусы фреймворков UIKit и SwiftUI. Знакомство с ними необходимо для оптимизации процесса разработки и создания продукта высокого качества.

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

Почему я над этим задумался?

Наша команда iOS-разработки приложения для продавцов столкнулась с проблемой отсутствия коммерческого опыта в разработке на SwiftUI и понимания двух подходов программирования: императивного и декларативного. 

Темпы роста команды были очень высокими. Мы нанимали крутых инженеров, но большинство из них не сталкивались в своей работе со SwiftUI. Поэтому им приходилось переучиваться в процессе адаптации, набивать шишки на код-ревью и изучать новую технологию на ходу. База знаний о SwiftUI для погружения новых сотрудников у нас отсутствует. Эта ситуация дала понять, что большинство разработчиков сталкиваются с одной и той же проблемой: сложностью перехода с императивного на декларативный подход. Это подтолкнуло меня сначала подготовить доклад для внутреннего митапа Ozon, а позже — написать эту статью.

Содержание

Императивный и декларативный подходы

Рассмотрим два подхода к программированию: императивный и декларативный.

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

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

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

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

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

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

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

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

С другой стороны, у нас есть декларативный стартап, которым руководит парнишка, он же Mr SwiftUI. У него не очень много опыта, но большие амбиции и стремления, он полностью доверяет своим подчинённым и при распределении задач полагается на их опыт и экспертизу, поэтому просто описывает видение конечной реализации и делегирует решение коллегам.

Примеры кода

 // Императивный подход
func sumOfSquaresI(array: [Int]) -> Int {
    var sum = 0
    for number in array {
        let square = number * number
        sum += square
    }
    return sum
}


// Декларативный подход
func sumOfSquaresD(array: [Int]) -> Int {
    array.map { $0 * $0 }.reduce(0, +)
}

В этих примерах мы решаем одну и ту же задачу — вычисление суммы квадратов чисел в массиве — но делаем это по-разному: с помощью императивного и декларативного подходов.

В первом примере мы используем цикл for, чтобы последовательно перебрать элементы массива, и переменную sum, чтобы хранить сумму. Таким образом, мы описываем последовательность шагов, необходимых для решения задачи.

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

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

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

Устаревающий UIKit ?‍? vs Новый SwiftUI ?

В этом разделе мы рассмотрим два фреймворка для разработки пользовательских интерфейсов для iOS: устаревающий UIKit и новый SwiftUI. Мы разберём их особенности и отличия, а также рассмотрим примеры кода на UIKit и SwiftUI, и какой код писать точно не нужно.

UIKit — это фреймворк, который применяется для создания пользовательского интерфейса в iOS-приложениях с 2007 года. Он использует императивный подход к разработке, на который опираются программисты, описывая на Swift и Objective-C, как создавать и настраивать элементы интерфейса. Вы определяете все свойства каждого элемента: положение на экране, размеры, цвета, шрифты и т. д. UIKit предоставляет великолепный инструментарий, однако требует высокого уровня квалификации для работы с ним.

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

Чтобы лучше понимать особенности SwiftUI, полезно знать о связке с реактивным программированием и библиотекой Combine.

Реактивное программирование — это концепция, в которой данные представляются в виде потоков (streams), автоматически обновляющихся при изменении состояния представления. А библиотека Combine позволяет использовать реактивный подход в SwiftUI, упрощая работу с потоками данных.

Действие -> Состояние -> Отображение

SwiftUI — это новый инструмент для разработки пользовательского интерфейса iOS-приложений, который быстро набирает популярность у разработчиков. Он прост в использовании и гибок, что позволяет писать код значительно быстрее, чем со старым UIKit.

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

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

Давайте рассмотрим процесс создания кнопки в обоих фреймворках.

Императивный подход на UIKit

final class MyViewController: UIViewController {
    let myButton = UIButton(type: .system)


    override func viewDidLoad() {
        super.viewDidLoad()


        myButton.frame = CGRect(x: 100, y: 100, width: 100, height: 50)
        myButton.setTitle("Press me", for: .normal)
        myButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        view.addSubview(myButton)
    }


    @objc func buttonTapped() {
        print("Button tapped!")
    }
}

Декларативный подход на SwiftUI

struct MyView: View {
    var body: some View {
        Button("Press me") {
            print("Button tapped!")
        }
        .frame(width: 100, height: 50)
        .position(x: 100, y: 100)
    }
}

Оба этих примера демонстрируют создание кнопки, нажатие на которую выводит сообщение “Button tapped” на консоль. Однако декларативный подход на SwiftUI гораздо более лаконичный и понятный. Вместо того чтобы настраивать каждый аспект кнопки отдельно, мы описываем кнопку в терминах её желаемых свойств — и SwiftUI самостоятельно обеспечивает реализацию. Это может существенно ускорить процесс разработки и уменьшить количество ошибок.

Кроме того, SwiftUI в связке с Combine позволяет нам использовать многие удобные функции, такие как, например, автоматические обновления интерфейса при изменении состояния, что делает создание пользовательских интерфейсов ещё более простым и эффективным.


Теперь рассмотрим процесс создания таблиц в обоих фреймворках.

TableView на UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    let data: [String] = ["Первый элемент", "Второй элемент", "Третий элемент"]
    var tableView: UITableView = UITableView()
  
    override func viewDidLoad( ) {
        super .viewDidLoad()
        tableView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.delegate = self
        tableView.dataSource = self
        self.view.addSubview(tableView)
    }
  
    func numberOfSections(in tableView: UITableView) -> Int { return 1 }
  
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return data.count }
  
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = data[ indexPath.row]
        return cell
    }
  
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("Выбран элемент \(data[ indexPath.row])" )
    }
}

По коду понятно, что реализовать таблицу на UIKit довольно сложно. 

Таблица на SwiftUI

struct ContentView: View {
    let data: Set<String> = ["Первый элемент", "Второй элемент", "Третий элемент"]
    
    var body: some View {
        List(data, id: \.self) { item in
            Text(item)
        }
    }
}

А вот реализация таблицы на SwiftUI не требует стольких усилий — код выглядит куда проще. 

Хороший пример с плохим кодом

import SwiftUI


struct ContentView: View {
    var label = Text("Hello, World!")
    label = label.font(.system(size: 16))
    label = label.background(.red)
    
    var button = Button {
        print("Button tapped")
    } {
        Text("Tap Me")
            .font(.headline)
            .foregroundColor(.white)
            .padding()
            .background(Color.blue)
            .cornerRadius(5)
    }
    
    button.frame(width: 120, height: 44)
}

Данный код на SwiftUI нарушает структуру View, поскольку в нём нет её объявления и функции body, которая должна содержать описание интерфейса. Переменные label и button объявлены вне функции body, что может вызвать ошибки при компиляции. Кроме того, использование модификаторов неоптимально: переменная label переопределяется три раза, что затрудняет чтение кода и может привести к утере значений его свойств. Помимо этого, кнопка не связана с функцией body и не обрабатывает события. Подобный код будет сложно поддерживать, он требует много времени на написание и ревью. Он не будет соответствовать стандартам качества кода и не будет принят инженерами.

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

Итак, SwiftUI — это новый инструмент для разработки пользовательского интерфейса с низким порогом входа и декларативным подходом к программированию, который упрощает создание пользовательского интерфейса. Однако при наличии многолетнего опыта работы с UIKit многие разработчики предпочитают продолжать работать с ним. Но ведь мы должны быть готовы меняться и адаптироваться к новым технологиям, чтобы оставаться на плаву в быстро меняющемся мире iOS-разработки.

Меняйся или сдохни

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

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

Преимущества и недостатки фреймворков

Некоторые инженеры, которые приходят к нам в Ozon Tech, считают, что SwiftUI — полная фигня и им не стоит пользоваться, тем более в коммерческой разработке. Другие же уверены, что UIKit  — это безнадёжно устаревшая технология динозавров. И все они правы! Но не совсем.

Мы переходим к основной части моей работы. Я поговорил с большим количеством экспертов, провёл масштабное исследование и выписал неопровержимые плюсы и минусы SwiftUI и UIKit. Давайте на них посмотрим:

Посмеялись — и хватит ?

Как вы могли догадаться, табличка шуточная. А теперь предлагаю взглянуть на реальную: 

 

SwiftUI ?

UIKit ?‍?

Скорость вёрстки

+

-

Совместимость с легаси-кодом

-

+

Дебаггинг

-

+

Мультиплатформенность

+

-

Поддержка сообщества

-

+

Количество кода

+

-

Совместимость с Modern Concurrency 

+

-

Поддерживаемость

-

+

Быстродействие

+

-

Скорость вёрстки: SwiftUI +, UIKit -

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

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

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

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

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

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

Совместимость с легаси-кодом: SwiftUI -, UIKit +

UIKit появился раньше, чем SwiftUI, он работает с Objective-C и Swift, что обеспечивает совместимость с готовым к использованию легаси-кодом. 

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

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

Дебаггинг: SwiftUI -, UIKit +

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

Одним из выборов разработчиков является выбор между двумя фреймворками: SwiftUI и UIKit. Каждый из них имеет свои преимущества и недостатки при дебаггинге.

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

С другой стороны, SwiftUI является новым фреймворком.. У него есть свои преимущества, такие как более простой синтаксис и быстрота верстки. Однако, это также делает дебаггинг на SwiftUI менее эффективным, так как инструменты для дебаггинга еще не так хорошо развиты как для UIKit.

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

Мультиплатформенность: SwiftUI +, UIKit -

SwiftUI был создан как фреймворк для мультиплатформенной разработки приложений, что позволяет использовать единый код для создания приложений для разных платформ: iOS, macOS, watchOS и tvOS. UIKit ориентирован на создание приложений только для iOS.

Поддержка сообщества: SwiftUI -, UIKit +

UIKit является «взрослым» общепризнанным фреймворком, который объединяет большое сообщество разработчиков и множество инструментов и библиотек для его использования. SwiftUI только появился и его сообщество пока ещё не такое развитое и большое, как у UIKit.

Количество кода: SwiftUI +, UIKit -

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

Совместимость с Modern Concurrency: SwiftUI +, UIKit -

SwiftUI представляет собой фреймворк, который полностью поддерживает Modern Concurrency с помощью новых Swift API, таких как async/await и Combine. UIKit же как старший фреймворк может не всегда соответствовать современным требованиям к асинхронному программированию и требовать дополнительных усилий для написания асинхронного кода.

Поддержка версий: SwiftUI -, UIKit +

SwiftUI был представлен в 2019 году вместе с iOS 13, поэтому поддерживает только устройства на ней и более новых версиях. Это означает, что разработка приложения с использованием SwiftUI может стать проблематичной, если требуется поддержка более старых версий iOS.

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

Быстродействие: SwiftUI +, UIKit -

SwiftUI предоставляет более быстрое и эффективное развитие своего инструментария по сравнению с UIKit. Он ориентирован на использование структур, тогда как UIKit использует классы для создания пользовательского интерфейса. Использование структур делает код на SwiftUI более лёгким, читабельным и удобным в работе. Структуры в Swift хранятся в стеке, а не в куче, как классы, — это обеспечивает более эффективное использование памяти (более быстрое выделение и освобождение).

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

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

Одной из ключевых особенностей, способствующей быстродействию SwiftUI, является его декларативный подход к созданию пользовательского интерфейса. Вместо того чтобы явно обновлять каждый элемент интерфейса вручную, SwiftUI использует концепцию "объявления состояния" (state declaration), где вы определяете желаемое состояние интерфейса, а фреймворк автоматически обрабатывает обновления и перерисовку только тех частей, которые изменились.

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

Благодаря использованию новых технологий и оптимизаций, SwiftUI также может использовать преимущества аппаратного ускорения, такого как Metal и Core Animation, для повышения производительности графики и анимаций.

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

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

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

Кто же победил?

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

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

При работе с версткой пользовательских интерфейсов, мы можем использовать фреймворки UIKit или SwiftUI. Однако, важно понимать, что каждый из них имеет свои особенности и специфику работы.

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

Сейчас мы имеем представление о ключевых парадигмах в iOS-разработке, так как погрузились в академические определения и рассмотрели примеры кода, разобрались в особенностях SwiftUI и UIKit, увидели отличия в количестве кода и код, который лучше не писать, и, конечно же, узнали о плюсах и минусах двух фреймворков, что внесло ясность в общую картину и понимание положения этих инструментов в мире iOS-инженера.

В этой статье мы рассмотрели:

  • особенности императивного и декларативного подходов программирования; 

  • особенности фреймворков UIKit ?‍? и SwiftUI ?;

  • плюсы и минусы фреймворков UIKit ?‍? и SwiftUI ?.

Несмотря на то, что дедуля UIKit не использует декларативный подход, он является очень мощным и гибким инструментом для создания пользовательского интерфейса iOS-приложений, и не стоит списывать его со счетов. Но сейчас у него есть серьёзный конкурент в виде относительно нового фреймворка, который впечатляет своими фичами, взять хотя бы сниженный порог входа в разработку. Может повториться история Objective-С и Swift, ведь реальность такова, что Apple движется в сторону технологических улучшений своих инструментов для разработки, и увеличение популярности SwiftUI и переход на его сторону многочисленного комьюнити — вопрос времени.

Полезные материалы

1. Фреймворк Carbon, который даёт возможность потрогать декларативный подход в UIKit. Это библиотека для создания пользовательских интерфейсов на основе компонентов UITableView и UICollectionView, вдохновлённая SwiftUI и React.

2. Реактивное программирование с Combine: тут и тут

3. Доклад Руслана Кавецкого из Agora «Избавляемся от старья и переходим на SwiftUI».

4. Статья «SwiftUI по полочкам».

*Хотел бы поблагодарить за помощь в написании статьи моих коллег и друзей:

Сергей Фирсов

Василий Усов

Максим Гришутин

Tags:
Hubs:
Total votes 83: ↑80 and ↓3+82
Comments40

Articles

Information

Website
ozon.tech
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия