Property Wrapper’ı Trendyol Kodunda nasıl kullandık? Decoding Hatalarına nasıl çözüm Bulduk?

Anıl taşkıran
5 min readMar 9, 2021

--

Production’da Property Wrapper’ı kullanarak sürekli yaptığımız bir yapıdan nasıl mı kurtulduk? Decoding hatası almamak için optional değer tanımlamak yerine default value nasıl atayabiliriz diye düşünmeye başladık ve property wrapper bizim işimizi gördü. Bu yazının sonunda ihtiyacımızı nasıl çözdüğümüzün cevabını bulacaksınız.

Property Wrapper, swift 5.1 ile backward compatible (alt sürümlere uyumlu) bir özellik olarak hayatımıza girdi. Bir özelliğin yaratılma süreci ve setter/getter aşamaları dahil olmak üzere yeni özellikler, filtreler veya hesaplamalar eklenmesini kolaylaştıran otomatik olarak yeni işlemlerin yapılmasını sağlayan bir yapıdır. En basit bir örnekle zihnimizde canlandıracaksak bir string tutan değerin içine ne setlenirse setlensin büyük karakterlerle tutulmasını istiyorsanız, bunu property wrapper ile kolayca yapabilirsiniz.

İlk olarak 2015–2016 yıllarında ele alınan bu konuya alternatif bi yaklaşım sunan Doug Gregor ve Joe Groff 16 Eylül 2020'de Swift Evolution 0258 Proposal olarak tanıttı. 2015'te sunulan teklife kıyasla Doug and Joe’nun yaklaşımı daha basit, derleyiciyi daha verimli kullanan ve yazılımcılar için anlaşılması daha kolay bir yaklaşım sundu.

Property wrapper, oluşturmak istediğiniz enum, struct veya class’ın başına “@propertyWrapper” yazarak aktifleştirebilirsiniz ve zorunlu olarak eklememiz gereken wrappedValue propertysi üzerinde logic’lerinizi yönetebilirsiniz.

Mesela Yukarıda bahsettiğim örnekteki gibi içine setlenen değer ne olursa olsun tamamını büyük harfe çevirelim.

@propertyWrapper
struct Uppercased {
var wrappedValue: String {
didSet { wrappedValue = wrappedValue.uppercased() }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue.uppercased()
}
}

Yukarıdaki şekilde Uppercased adında bir struct oluşturduk. Bunun property Wrapper olduğunu @propertyWrapper yazarak göstermiş olduk. Bu yüzden bize zorunlu olarak wrappedValue’yu ekletti. Siz burada kullanacağınız property’nin tipine göre type geçebilirsiniz. Ben şimdi string değerlerin her setlendiğinde büyük harfe geçirilmesi için bu örneği verdim.

struct Article {
@Uppercased var title: String
}

Ardından bu wrapper’ı hangi property’de kullanmak isterseniz başına yazmanız yeterli. Ben bu örnekte yarattığım her article’ın title’ı büyük oluşsun istiyorum. O yüzden title propertysine ekledim.

let article = Article(title: "property Wrapper")
print(article.title) // PROPERTY WRAPPER

ve artık her çağırdığınızda article’ın title’ı büyük harf’e dönmüş olacak.

Peki iyi hoş da nasıl bir sorunla karşılaştık da bizim problemimizi property wrapper çözmüş oldu? Tek derdimiz bi property’i büyük yapmak değildi. Gelin şimdi bu konuya bi göz atalım.

DTO’nuzda diyelim ki userType’ını tutacaksınız. Ve şu an 3 tipi var. Int veya string karşılıkları olarak tutabilirsiniz veya gelecek bütün karşılıklarını biliyorsanız enum bile yazabilirsiniz. Ama enum kullandığınız zaman yeni bir type eklenirse projeniz decoding error almaya başlayacak tabii eğer o enum’a custom init yazıp decoding alınca default value ataması yapmadıysanız. Biz decoding almamak için projenin her yerinde enum karşılığı olabilecek değerler için custom initlerimizi yazdık.

enum UserType: String, Codable {
case admin, user, none

public init(from decoder: Decoder) throws {
self = try UserType(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .none
}
}

Mesela yukarıdaki örneğe baktığınızda eğer yeni bir UserType’ı eklenirse eski client bunu decode edemeyecek. Ama sizin karşıladığınız none değerine setlenecek ve decoding error almadan hayatınıza devam edeceksiniz.

Peki decoding aldığı zaman default value setleyebilecek bir wrapper kullanmaya ne dersiniz?

public protocol DefaultCodableInterface {
associatedtype RawValue: Codable

static var defaultValue: RawValue { get }
}
@propertyWrapper
public struct DefaultCodable<T: DefaultCodableInterface>: Codable {
public var wrappedValue: T.RawValue

public init(wrappedValue: T.RawValue) {
self.wrappedValue = wrappedValue
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.wrappedValue = (try? container.decode(T.RawValue.self)) ?? T.defaultValue
}

public func encode(to encoder: Encoder) throws {
try wrappedValue.encode(to: encoder)
}
}
extension DefaultCodable: Equatable where T.RawValue: Equatable { }
extension DefaultCodable: Hashable where T.RawValue: Hashable { }

Öncelikle Property Wrapper’ımızı yaratalım. Bunun bir interface’i olsun ve bu sayede kolaylıkla işlemlerimizi generic bir yapıda yapalım. İsterseniz Default value olarak enum’ın son case’ini atayacak bir yapı yapın isterseniz de default olarak bir bool değeri null gelince false/true setleyecek bir yapı oluşturun.
Üstteki örnekte wrappedValue’muzun type’ı interface’den geliyor. bu sayede bunu decoder’a veriyoruz ve eğer bir değer döndüremezse elimizdeki default value’yu paslıyoruz.

Yukarıdaki UserType örneğine baktığımızda aslında default olarak last case’i vermek için bir geliştirme yapabiliriz dedik ve kolları sıvadık. Öncelikle EnumDefaultValueSelectable diye bi protocol yaratarak başladık. Bu protocol yapılacak aksiyonu belirleyecek. Mesela decoding aldığımızda Enum case’indeki last value’yu seçecek bir yapı yaratalım. Bunun için; Codable & CaseIterable & RawRepresentable’ı conform eden bir protocol oluşturduk. Ve elimizdeki tüm case’leri gezerek lastCase’i return ettik.

public protocol EnumDefaultValueSelectable: Codable & CaseIterable & RawRepresentable where RawValue: Decodable, AllCases: BidirectionalCollection { }public struct LastCase<T>: DefaultCodableInterface where T: EnumDefaultValueSelectable {
public static var defaultValue: T { T.allCases.last! }
}

Yukarıdaki verdiğim UserType örneğine bir daha bakarsak eğer ne kadar temiz olduğunu fark ettiniz mi? Bi de bunu projenin her yerinden temizleyip property wrapper ile süslediğinizi düşünün. Tadından yenmez.

enum UserType: String, EnumDefaultValueSelectable {
case admin, user, none
}
struct UserResponse: Codable {
@DefaultCodable<LastCase> var user: UserType
}

Yarattığınız response’larda decoding hatası almamak için property’lerinizi optional yapıyor olabilirsiniz ama null gelen bir bool değerinin sizin için bi anlamı olmalı.

Yukarıdaki örneğe ek olarak bir de isAdmin propertymiz olsun mesela. isAdmin değeri null gelirse aslında biz buna default olarak false değerini atayabiliriz. O zaman DefaultFalse adında bir struct oluşturalım. Ve tek yapmamız gereken Interface’imizi conform eden bi struct oluşturup default value’muzu döndürmek.

public struct DefaultFalse: DefaultCodableInterface {
public static var defaultValue: Bool { return false }
}

Ardından Bool tipindeki property’mizi null olduğu durumda false olacak şekilde kurgulayabiliriz.

struct UserResponse: Codable {
@DefaultCodable<DefaultFalse> var isAdmin: Bool
}

Eğer bu Property Wrapper’ı kullanmak isterseniz kolayca aşağıdaki linkten erişebilirsiniz.

Property Wrapper’ı Production kodunda bu şekilde kullanarak bayağı bir yerde yaptığımız custom init’lerden kurtulmuş olduk. Eğer sizin de kullandığınız yapılar varsa yorum kısmında belirtebilirsiniz.

Beğendiyseniz, alkışlarınızla daha fazla kişiye ulaşmasını sağlayabilirsiniz. 👏🏼

--

--