Pull to refresh

API for Any (thing)

Reading time5 min
Views2.1K

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

Мой опыт разработки под iOS с 2008 года 🙂🤖🖐
Примеры на Swift 📲

Идея

Представьте, что любой предмет можно получить. Достаточно точно его описать.
План:

  1. Описать предмет.

  2. Создать запрос.

  3. Получить.

Идеальный вариант

Например, гео

В мире объектно-ориентированного программирования все предметы- это объекты. У каждого объекта есть тип.

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

Functions are self-contained chunks of code that perform a specific task. You give a function a name that identifies what it does, and this name is used to “call” the function to perform its task when needed.

//Функция с 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

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

Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments5

Articles