Переведено с английского языка. Оригинал задания лежит в проекте. Coding exercise.pdf
Предоставленный код является неэффективным с точки зрения тестируемости, современности и API-дизайна. Он также некорректен при неправильном использовании. Ваша задача - переписать код так, чтобы он стал корректным, полностью тестируемым, потокобезопасным и легким при повторном использовании.
Современные концепции, такие как async/await
, actor
'ы и structured concurrency
, должны использоваться вместо устаревших концепций, таких как NotificationCenter
.
Переписанный код должен:
- Гарантировать, что переданная замыкание не будет вызвана, если сеть недоступна.
- Вызвать переданную замыкание, если сеть изначально доступна.
- Вызвать переданную замыкание, если сеть станет доступной в течение заданного времени ожидания.
- Не вызывать замыкание, если сеть станет доступной только после истечения времени ожидания.
import Foundation
import Network
public class NetworkOperationPerformer {
private let networkMonitor: NetworkMonitor
private var timer: Timer?
private var closure: (() -> Void)?
public init() {
self.networkMonitor = NetworkMonitor()
}
/// Пытается выполнить сетевую операцию, используя переданное замыкание, в течение заданного времени ожидания.
/// Если сеть недоступна в течение заданного времени ожидания, операция не выполняется.
public func performNetworkOperation(
using closure: @escaping () -> Void,
withinSeconds timeoutDuration: TimeInterval
) {
self.closure = closure
if self.networkMonitor.hasInternetConnection() {
closure()
} else {
tryPerformingNetworkOperation(withinSeconds: timeoutDuration)
}
}
private func tryPerformingNetworkOperation(withinSeconds timeoutDuration: TimeInterval) {
NotificationCenter.default.addObserver(
self,
selector: #selector(networkStatusDidChange(_:)),
name: .networkStatusDidChange,
object: nil
)
self.timer = Timer.scheduledTimer(withTimeInterval: timeoutDuration, repeats: false) { _ in
self.closure = nil
self.timer = nil
NotificationCenter.default.removeObserver(self)
}
}
@objc func networkStatusDidChange(_ notification: Notification) {
guard
let connected = notification.userInfo?["connected"] as? Bool,
connected,
let closure
else {
return
}
closure()
}
}
private class NetworkMonitor {
private let monitor = NWPathMonitor()
init() {
startMonitoring()
}
private func startMonitoring() {
monitor.pathUpdateHandler = { _ in
NotificationCenter.default.post(
name: .networkStatusDidChange,
object: nil,
userInfo: ["connected": self.hasInternetConnection()]
)
}
monitor.start(queue: DispatchQueue(label: "NetworkMonitor"))
}
func hasInternetConnection() -> Bool {
return monitor.currentPath.status == .satisfied
}
}
private extension Notification.Name {
static let networkStatusDidChange = Notification.Name("NetworkStatusDidChange")
}
let networkOperationClosure: () async -> SomeType = { // Long-lasting network operation.
return result
}
let result = await NetworkOperationPerformer().perform(withinSeconds: 3) {
return await networkOperationClosure()
}
- Опишите существующие в оригинальном коде проблемы
- Улучши свой код, таким образом чтобы была возможность отмены задач на выполнение. Подумай, как лучше это организовать
- Добавь документацию на публичный метод в твоем коде
Создайте простое приложение на основе SwiftUI
с двумя экранами:
- Экран загрузки и экран отображения изображения
- Используйте
NetworkOperationPerformer
из Части 1 для выполнения операции загрузки изображения.
- Правильно управляйте состоянием приложения.
- Разделите логику и интерфейс, чтобы упростить тестирование.
- При запуске приложения отображается экран загрузки со спиннингом.
- Если интернет недоступен в течение 0,5 секунд, отображается дополнительный текст, указывающий на отсутствие сети.
- Выполните операцию загрузки изображения, используя метод perform из
NetworkOperationPerformer
с временем ожидания 2 секунды. - После загрузки изображения или по истечении времени ожидания отображается второй экран:
- Если изображение было загружено, оно отображается.
- Если загрузка не удалась, отображается текст, указывающий на ошибку.
- Исходный код полагался на
NotificationCenter
для отслеживания изменений состояния сети, что может быть ненадежным и не является потокобезопасным. Кроме того, он устарел. Современный код наSwift
должен использоватьasync/await
,actor
ы иstructured concurrency
. - Использование
NotificationCenter
иTimer
делает исходный код трудным для тестирования в контролируемых условиях. Для получения дополнительной информации, как тестировать такой код прочитайте статью , она поможет глубже понять проблему. - Отсутствие внедрения зависимостей (dependency injection) в проекте может привести к ряду проблем, связанных с поддерживаемостью, тестируемостью и масштабируемостью.
- Исходный код может приводить к потенциальным состояниям гонок (race condition) и неопределенному поведению.
- Таймеры часто захватывают
self
сильно в своих замыканиях, что приводит к retain cycle. Чтобы этого избежать, используйте[weak self]
. - Использование протоколов для сервисов в swift настоятельно рекомендуется для внедрения зависимостей и тестирования. Протоколы помогают определить четкие контракты для ваших сервисов, что позволяет разъединять компоненты, легко заменять реализации и упрощать модульное тестирование с использованием моков и стабов.
protocol NetworkOperationPerformer {
/// Выполняет заданную асинхронную операцию и гарантирует, что она завершится в пределах указанного времени.
/// Этот метод запускает предоставленную сетевую операцию в новой задаче, позволяя при необходимости отменить ее.
/// Если операция не успевает выполниться за отведенный интервал времени, то метод бросает ошибку timeout
///
/// - Параметры:
/// - closure: Асинхронное замыкание, представляющее сетевую операцию, которую необходимо выполнить.
/// - withinSeconds: `TimeInterval`, указывающий лимит времени в секундах, в течение которого операция должна завершиться.
func performNetworkOperation(
using closure: @escaping @Sendable () async -> Void,
withinSeconds timeoutDuration: TimeInterval
) async throws
}
Полную реализацию можно посмотреть в проекте.
P.S. Пулл реквесты с замечаниями, исправлениями, добавлениями красивого UI слоя - приветствуются!