Enums + Associated Values = Swift

Swift — означає швидкий. Швидкий — значить зрозумілий, простий. Але досягти простоти і зрозумілості непросто: зараз в Swift швидкість компіляції так собі, та й деякі моменти мови викликають питання. Тим не менш можливість перерахувань (enum'ів), про яку я розповім (associated values — приєднувані значення) — одна з найкрутіших. Вона дозволяє скоротити код, зробити його зрозумілішим і надійніше.


Вихідна завдання
Уявімо, що потрібно описати структуру варіантів оплати: готівкою, карткою, у подарунковому сертифікату, пейпалом. У кожного способу оплати можуть бути свої параметри. Варіантів реалізації при таких слабо певних умовах — нескінченна кількість, що можна згрупувати, щось переобозначить. Не варто чіплятися, ця структура тут живе для прикладу.
class PaymentWithClass {
// true, значить платимо готівкою, а false потрібен, щоб init() не писати
var isCash: Bool = false

// .some, значить платимо карткою, номер такий-то, параметри такі-то
var cardNumber: String?
var cardHolderName: String?
var cardExpirationDate: String?

// .some, значить платимо подарунковим сертифікатом
var giftCertificateNumber: String?

// тут якийсь ідентифікатор та код авторизації (що б це не означало)
var paypalTransactionId: String?
var paypalTransactionAuthCode: String?
}

щось багато опшналов вийшло)
Працювати з таким класом важко. Swift заважає, адже на відміну від багатьох інших мов, він змусить обробити всі опшналы (наприклад,
if let
або
guard let
). Буде багато зайвого коду, який сховає бізнес-логіку.
Неприємно таке й верифікувати. Уявити собі код, який перевіряє валідність цього класу ще можна, а от писати таке — вже зовсім не хочеться.
Структури!
Згадаймо, що в Swift є value-типи, які називаються структури. Кажуть, їх добре використовувати для такого роду модельних описів. Спробуємо заодно розбити попередній код на кілька структур, відповідних різним типам оплати.
struct PaymentWithStructs {
struct Cash {}

struct Card {
var number: String
var holderName: String
var expirationDate: String
}

struct gift certificate {
var number: String
}

struct PayPal {
var transactionId: String
var transactionAuthCode: String?
}

var cash: Cash?
var card: Card?
var gift certificate: gift certificate?
var payPal PayPal?
}

Вийшло краще. Коректність окремих типів оплати перевіряється засобами самої мови (наприклад, видно, що авторизаційний код PayPal може бути відсутнім, а ось для карти всі поля обов'язкові), нам залишається провалидировать тільки що одне (і тільки одне) з полів
cash
,
card
,
gift certificate
,
payPal
— не нульовий.
Зручно? Цілком. В цьому місці в багатьох мовах можна поставити крапку і вважати роботу виконаною. Але не в Swift.
Enum'и. Асоційовані значення
Свіфт, як і будь-яка сучасна мова, надає можливість створювати типізовані перерахування (enum'и). Їх зручно використовувати, коли потрібно описати, що поле може бути одним з декількох заздалегідь визначених значень або вставити в
switch
, щоб проконтролювати, що всі можливі варіанти перебрані. По ідеї в нашому прикладі максимум, що можна зробити — це вставити тип оплати, щоб ще точніше валідувати структуру:
struct PaymentWithStructs {
enum Kind {
case cash
case card
case gift certificate
case payPal
}

struct Card {
var number: String
var holderName: String
var expirationDate: String
}

struct gift certificate {
var number: String
}

struct PayPal {
var transactionId: String
var transactionAuthCode: String?
}

var kind: Kind

var card: Card?
var gift certificate: gift certificate?
var payPal PayPal?
}

Зауважте, що структура
Cash
пропала, вона порожня і висіла тільки для індикації типу, який ми тепер ввели явно. Стало краще? Так, стало простіше перевіряти, який тип оплати ми використовуємо. Він прописаний явно, не потрібно аналізувати, яке поле не
nil
.
Наступний крок — спробувати якось позбутися від необхідності окремих опшналов для кожного типу. Було б круто, якщо б існувала можливість прив'язати до типу відповідні структури. На карті — номер і власника, до PayPal — ідентифікатор транзакції і так далі.
Саме для цього в Swift є асоційовані значення (associated values, не плутайте з associated types, які використовуються в протоколах і зовсім не про те). Записуються вони ось так:
enum PaymentWithEnums {
case cash
case card(
number: String,
holderName: String,
expirationDate: String
)
case gift certificate(
number: String
)
case payPal(
transactionId: String,
transactionAuthCode: String?
)
}

найважливіший момент — точно певний тип. Відразу видно, що
PaymentWithEnums
може приймати рівно одне з чотирьох значень, і у кожного значення можуть бути (або не бути) як дата або
transactionAuthCode
певні параметри. Фізично не можна проставити параметри відразу і для карти і для подарункового сертифіката, як це можна було зробити у варіанті з класами або структурами.
Виходить, що попередні варіанти вимагають додаткових перевірок. Також була можливість забути обробити який-небудь з варіантів оплати, особливо, якщо з'являться нові. Enum'и виключають всі ці проблеми. Якщо додасться новий case, наступна перекомпіляція зажадає додати його в усі switch'в. Ніяких опшналов окрім дійсно необхідних.
Користуватися такими складними enum'ами можна як зазвичай:
if case .cash = payment {
// зробити щось специфічне для оплати готівкою
}

Параметри виходять за допомогою патерн-матчинга:
if case .gift certificate(let number) = payment {
print("O_o подарунковий сертифікат! Номер: \(number)")
}

А перебрати всі варіанти можна свіча:
switch payment {
case .cash:
print("Офлайнові гроші!")
case .card(let number, let holderName, let expirationDate):
let last4 = String(number.characters.suffix(4))
print("Хехе, картка! Останні цифри \(last4), господар: \(holderName)!")
case .gift certificate(let number):
print("O_o подарунковий сертифікат! Номер: \(number)")
case .payPal(let transactionId, let transactionAuthCode):
print("Пейпалом платимо! Транзакція: \(transactionId)")
}

В якості вправи можна написати схожий код для варіанту з класом/структурою. Щоб вправу було повним, треба не полінуватися і обробити всі необхідні варіанти, включаючи помилкові.
Кайф. Swift. Ще б компилился швидше, і взагалі буде щастя. :-)
Трохи граблів
Enum'и — не панацея. Ось кілька моментів, які вам можуть зустрітися.
По-перше, enum'и не можуть містити збережені поля. Якщо ми захочемо додати в
PaymentWithEnums
, наприклад, дату платежу (одне і те ж поле для всіх варіантів оплати), то отримаємо помилку:
enums may not contain stored properties
. Як же бути? Можна покласти дату в кожен кейс enum'а. Можна зробити структуру і покласти туди enum і дату.
По-друге, якщо звичайні enum'и можна порівнювати оператором
==
(він синтезується автоматично), то як тільки з'являються асоційовані значення, можливість порівняння «пропадає». Виправити це можна легко, підтримати протокол
Equatable
. Втім, навіть після цього порівнювати буде незручно, так як не можна просто написати
payment == PaymentWithEnums.gift certificate
, потрібно створити праву частину цілком:
PaymentWithEnums.gift certificate(number: "")
. Набагато зручніше в такому разі створювати спеціальні методи, які повертають Bool (
isGiftCertificate(_ payment: PaymentWithEnums) -> Bool
), і перенести туди
if case
. Якщо ж потрібно порівнювати кілька значень, то, можливо,
switch
буде зручніше.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.