Возможно ли создать интерфейс для получения любого объекта одинаковым способом?
Это исследование данных: Как в коде программы мы получаем, создаем, трансформируем и передаем объекты.
Мой опыт разработки под iOS с 2008 года 🙂🤖🖐
Примеры на Swift 📲
Идея
Представьте, что любой предмет можно получить. Достаточно точно его описать.
План:
Описать предмет.
Создать запрос.
Получить.
Идеальный вариант
![](https://habrastorage.org/getpro/habr/upload_files/bf9/8a7/67d/bf98a767ddfb16481b3e8ca8afd23896.png)
Например, гео
В мире объектно-ориентированного программирования все предметы- это объекты. У каждого объекта есть тип.
no Swift, please
Например, такие объекты из стандартного комплекта разработки:
CLLocation - геопозиция пользователя;
CMPedometerData - данные системного шагомера;
VNHumanHandPoseObservation - данные о положении человеческих рук (на фото, видео и др.) с использованием компьютерного зрения.
Когда в приложении нужно использовать геопозицию пользователя, программист реализует логику в коде вручную или использует библиотеки.
Любым способом получая объект CLLocation
.
Самая популярная библиотеĸа для получения гео на GitHub- это SwiftLocation.
Больше 1000 строĸ тольĸо ĸода без тестов. И для ĸаждого типа объеĸта мы должны находить решение или писать свое, используя опыт других.
У всех разработчиков со временем складывается личный набор инструментов для разных нужд (не пропустите холивар). Но как в этом разобраться новому человеку? Человеку не из мобильной разработки.
Эти инструменты абсолютно не стандартизированы.
Единый интерфейс для черных ĸоробоĸ?
Асинхронно
Обычно сейчас используют блоки (блок = лямбда, замыкание) для обработки данных полученных асинхронно:
{ (location: CLLocation) in
}
Первый пункт готов:
✅ Описать предмет
Тип объекта- это и есть точное описание данных, которые ожидаются в рамках блока.
Что там на входе?
Часть объектов может быть создана или запрошена с настройками по умолчанию. Другие зависят от объектов в контексте или должны быть созданы с опциями.
Перед запросом необходимо создать зависимости и добавить в контекст:
//Объект от данных которого зависит предмет запроса
Any
//Не упорядоченная куча объектов разного типа
Set<Any>
//Объекты с ключами не по умолчанию
Dictionary<String, Any>
Например, гео
При запросе геопозиции пользователя, программист обычно настраивает точность:
//Например, геопозиция пользователя с точностью до ± сотен метров
accuracy: CLLocationAccuracy = kCLLocationAccuracyHundredMeters
И дистанцию обновления геопозиции в метрах:
//Обновлять каждые 250 метров
filter: CLLocationDistance = 250
Порядок не имеет значения:
["CLLocationDistance": 250,
"CLLocationAccuracy": kCLLocationAccuracyHundredMeters]
Все данные для создания объектов должны быть в контексте до начала запроса.
Запрос
Вход- это данные в контексте, выход- это ожидаемый объект. Создаем запрос.
Функции в Swift
//Функция с 2 аргументами типа String, возвращает String
func greet(person: String, title: String) -> String {
}
static func | (date: Date, format: String) -> String {
}
Примерная хронология размышлений
— Придумать архитеĸтуру, новые абстраĸции?
— Коротĸое и емĸое имя?
— В один символ?
— Мы используем всю ĸлавиатуру?
— Каĸ передают данные между программами с теĸстовым интерфейсом?
Примерно два года размышлений и переписывания с нуля превратились в Пайп.
Pipe
англ. pipeline в терминологии операционных систем семейства Unix — некоторое множество процессов, для которых выполнено следующее перенаправление ввода-вывода: то, что выводит на поток стандартного вывода предыдущий процесс, попадает в поток стандартного ввода следующего процесса. Запуск конвейера реализован с помощью системного вызова
pipe()
.
Я представляю этот знак как трубу которая связывает объекты на разных концах:
command1 | command2 | command3
ls -l | grep key | less
Добавьте один знак | к блоку и получайте данные:
|{ (location: CLLocation) in
}
Положите зависимости в пайп, когда они нужны:
["CLLocationDistance": 250,
"CLLocationAccuracy": kCLLocationAccuracyHundredMeters] | { (l: CLLocation) in
}
Второй пункт:
✅ Создать запрос
Остается только ждать вызова блока.
✅ Profit
Покрытие тестами в процессе:
https://github.com/El-Machine/Pipe/tree/main/Tests/PipeTests
Примеры
Представьте трубу, из которой можно получить что угодно.
Достаточно описать объект и ждать получения.
Не имеет значения что на другом конце трубы.
Любой объект можно положить в трубу и получить без изменений в любой другом конце этой трубы.
//Async
//Anything from Nothing
|{ (anything: Anything) in
🧙🏼
}
//Determinate user's location
|{ (location: CLLocation) in
}
//Receive pedometer data
|{ (data: CMPedometerData) in
}
//Find bluetooth peripheral near
|{ (peripheral: CBPeripheral) in
}
//Subscribe to notification
UIWindow.keyboardWillShowNotification | { (n: Notification) in
}
//Look for a contact
CNContact.predicateForContacts(matchingName: "John Appleseed") | .every { (contact: CNContact) in
}
//Read NFC tag
|{ (tag: NFCNDEFTag) in
}
//Recognize bodies in imageURL
URL(string: "http://example.com/image.jpg") | { (faces: [VNFaceObservation]) in
}
//Recognize bodies in data
data | .while { (bodies: [VNHumanBodyPoseObservation]) in
bodies < 2
}
//Recognize 4 hands on input
let pipe = |{ (hands: [VNHumanHandPoseObservation]) in
}
let request: VNDetectHumanHandPoseRequest = pipe.get()
request.maximumHandCount = 4
let preview: AVCaptureVideoPreviewLayer? = pipe.get()
view.layer.addSublayer(preview!)
//Sync
//Anything from Something
let anything: Anything = something|
let i: Int = float|
let string: String = data|
let front: UIColor = 0x554292|
let back: UIColor? = "#554292"|
let message: NFCNDEFMessage? = URL(string: "https://exmaple.com/handle")|
Под капотом
Логика начинается в классе Pipe
- это контекст для запросов:
//Положить объект
func put<T>(_ object: T) -> T
//Достать объект
func get<T>() -> T?
Объекты хранятся по ключу своего типа, опционально использовать любой строковой ключ.
Для ожидания объекта создается экземпляр Expect<T>
c условием (every по умолчанию):
//Ожидание
Expect.every { T in
}
Expect.while { T in
true
}
let expect = Expect.one { T in
}
//Создаем запрос
|expect
//Сахар
|.every { T in
}
|.one { T in
}
|.while { T in
true
}
Asking
Объекты реализуют Asking
для настройки окружения и запроса экземпляров:
//Например, запрос прав на получение геопозиции у пользователя.
//Запрос формируется на основании объекта with
extension CLAuthorizationStatus: Asking {
static func ask<E>(with: Any?, in pipe: Pipe, expect: Expect<E>) {
let source = with as? CLLocationManager ?? pipe.get()
switch with as? CLAuthorizationStatus {
case .authorizedAlways:
source.requestAlwaysAuthorization()
case .authorizedWhenInUse, .none:
source.requestWhenInUseAuthorization()
default:
break
}
}
}
Вызовы могут быть связаны в цепочку для использования одного контекста:
| .every { T in
} | .one { U in
} | .while { E in
true
}
Плюс уведомления о прогрессе:
| .every { T in
} | .one { U in
} | .any { some in
} | .all { last in
}
Текущая реализация не эталон, это попытка найти решение: возможно ли создать универсальный интерфейс для любых объектов?
Исходный код на Github
Приглашаю всех к обсуждению идеи, интерфейса, реализации вне зависимости от платформы.