Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to migrate legacy code efficiently

How to migrate legacy code efficiently

Eleni Papanikolopoulou

September 17, 2022
Tweet

More Decks by Eleni Papanikolopoulou

Other Decks in Programming

Transcript

  1. What is legacy code? It is an organisation’s existing OBSOLETE

    code that is maintained by someone other than the original author.
  2. What is legacy code? It is an organisation’s existing OBSOLETE

    code that is maintained by someone other than the original author. Code that has no tests. Code that can be rewritten using better programming techniques or languages
  3. Network Layer Api Clients Data Models Objective - C 😬

    SignInApiClient ProductsApiClient ShopsApiClient
  4. Domain Model ☑ Free from any form of decoding mechanism

    ( e.g. Codable ) ☑ Decoupled from the API ☑ Datasource Model & Business logic
  5. 4) Replace models returned in Obj-C clients with DMs 7)

    Extract DMs in a more generic way 5) Create the Swift clients and return the DMs 1) Rename the Obj-C models 2) Create the Swift models & Domain Models 3) Create initializers of DMs from Obj-C + Swift models DMs Migration Steps 6) Use DMs throughout the codebase
  6. Migration Steps 4) Replace models returned from Obj-C clients with

    DMs 7) Extract DMs in a more generic way 5) Create the Swift clients and return the DMs 1) Rename the Obj-C models 2) Create the Swift models & Domain Models 3) Create initializers of DMs from Obj-C + Swift models 6) Use DMs throughout the codebase
  7. Obj-C Model @interface @property(nonatomic, copy, readonly, nonnull) NSNumber *productId; @property(nonatomic,

    copy, readonly, nonnull) NSString *name; @property(nonatomic, copy, readonly, nullable) NSString *review; @property(nonatomic, copy, readonly, nonnull) NSNumber *price; @property(nonatomic, copy, readonly, nonnull) NSString *manufacturer; @property(nonatomic, copy, readonly, nullable) NSString *imageUrl; @property(nonatomic, strong, readonly, nullable) NSNumber *reviewsScore; @end : MTLModel <MTLJSONSerializing> Mantle Product
  8. Obj-C Model @interface @property(nonatomic, copy, readonly, nonnull) NSNumber *productId; @property(nonatomic,

    copy, readonly, nonnull) NSString *name; @property(nonatomic, copy, readonly, nullable) NSString *review; @property(nonatomic, copy, readonly, nonnull) NSNumber *price; @property(nonatomic, copy, readonly, nonnull) NSString *manufacturer; @property(nonatomic, copy, readonly, nullable) NSString *imageUrl; @property(nonatomic, strong, readonly, nullable) NSNumber *reviewsScore; @end : MTLModel <MTLJSONSerializing> Mantle Product Old
  9. 4) Replace models returned from old clients with DMs 5)

    Create new Swift clients and return the DMs 1) Rename the Obj-C models 2) Create the Swift models & Domain Models 3) Create initializers of DMs from Obj-C + Swift models Migration Steps 7) Extract DMs in a more generic way 6) Use DMs throughout the codebase
  10. struct Product let productId: Int let name: String let review:

    String? let price: Double let manufacturer: String let imageUrl: String? let reviewsScore: Float } : Decodable { enum SwiftModel { } extension SwiftModel { } SwiftModel.Product Swift Model
  11. Domain Model struct Product { let productId: Int let name:

    String let review: String? let price: Double let manufacturer: String let imageUrl: String? let reviewsScore: Float } enum Domain { } extension SwiftModel { } extension Domain { } SwiftModel.Product struct Product let productId: Int let name: String let review: String? let price: Double let manufacturer: String let imageUrl: String? let reviewsScore: Float } : Decodable Swift Model Domain.Product
  12. extension Domain { struct Product { let productId: Int let

    name: String let review: String? let price: Double let manufacturer: String let imageUrl: String? let reviewsScore: Float // Business logic var priceString: NSAttributedString { let attributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 14.0), .foregroundColor: UIColor.black ] return NSAttributedString(string: "\(price)", attributes: attributes) } var mainImageURL: URL? { guard let imageUrlString = imageUrl, let imageUrl = URL(string: imageUrlString) else { return nil } return imageUrl } } }
  13. 4) Replace models returned from Obj-C clients with DMs 5)

    Create new Swift clients and return the DMs 1) Rename the Obj-C models 2) Create the Swift models & Domain Models 3) Create initializers of DMs from Obj-C + Swift models Migration Steps 7) Extract DMs in a more generic way 6) Use DMs throughout the codebase
  14. extension Domain { struct Product { let productId: Int let

    name: String let review: String? let price: Double let manufacturer: String let imageUrl: String? let reviewsScore: Float } } init(model: SwiftModel.Product) { self.productId = model.productId self.name = model.name self.review = model.review self.price = model.price self.manufacturer = model.manufacturer self.imageUrl = model.imageUrl self.reviewsScore = model.reviewsScore }
  15. extension Domain { struct Product { let productId: Int let

    name: String let review: String? let price: Double let manufacturer: String let imageUrl: String? let reviewsScore: Float } } init(model: SwiftModel.Product) { self.productId = model.productId self.name = model.name self.review = model.review self.price = model.price self.manufacturer = model.manufacturer self.imageUrl = model.imageUrl self.reviewsScore = model.reviewsScore } init(model: OldProduct) { self.productId = model.productId.intValue self.name = model.name self.review = model.review self.price = model.price.doubleValue self.manufacturer = model.manufacturer self.imageUrl = model.imageUrl self.reviewsScore = model.reviewsScore?.floatValue }
  16. 4) Replace models returned from Obj-C clients with DMs 5)

    Create new Swift clients and return the DMs 1) Rename the Obj-C models 2) Create the Swift models & Domain Models 3) Create initializers of DMs from Obj-C + Swift models Migration Steps 7) Extract DMs in a more generic way 6) Use DMs throughout the codebase
  17. typedef void (^RequestHandler)(NSArray<OldProduct *> *products, NSError *error); - (NSURLSessionDataTask *)getProducts:(NSDictionary

    *)parameters completion:(RequestHandler)completion { ClientApiTask *apiTask = [[ClientApiTask alloc] init]; apiTask.httpMethod = enSKZHttpMethodGet; apiTask.urlString = @"home/get_products"; apiTask.parameters = parameters; apiTask.completion = ^(NSInteger responseCode, id responseObject, NSError *error) { if (error) { // handle error } NSArray<OldProduct *> *products = [MTLJSONAdapter modelsOfClass:OldProduct.class fromJSONArray:responseObject[@"products"] error: NULL]; if (completion) { completion(products, nil); } }; return [self urlTaskForApiTask:apiTask]; } The Obj-C ProductsApiClient
  18. typedef void (^RequestHandler)(NSArray<OldProduct *> *products, NSError *error); - (NSURLSessionDataTask *)getProducts:(NSDictionary

    *)parameters completion:(RequestHandler)completion { ClientApiTask *apiTask = [[ClientApiTask alloc] init]; apiTask.httpMethod = enSKZHttpMethodGet; apiTask.urlString = @"home/get_products"; apiTask.parameters = parameters; apiTask.completion = ^(NSInteger responseCode, id responseObject, NSError *error) { if (error) { // handle error } NSArray<OldProduct *> *products = [MTLJSONAdapter modelsOfClass:OldProduct.class fromJSONArray:responseObject[@"products"] error: NULL]; if (completion) { completion(products, nil); } }; return [self urlTaskForApiTask:apiTask]; }
  19. func fetchProducts(completion: @escaping ([ OldProductsApiClient.shared().getProducts(nil) { products, error in if

    let error = error { completion(nil, error) } guard let products = products else { completion(nil, nil) return } } } Request using the Obj-C Model OldProduct]?, Error?) -> Void) { completion(products, nil)
  20. func fetchProducts(completion: @escaping ([ OldProductsApiClient.shared().getProducts(nil) { products, error in if

    let error = error { completion(nil, error) } guard let products = products else { completion(nil, nil) return } } } Domain.Product]?, Error?) -> Void) { completion(products.map { Domain.Product(model: $0) }, nil) Request using the Domain Model Initialiser
  21. 4) Replace models returned from Obj-C clients with DMs 5)

    Create new Swift clients and return the DMs 1) Rename the Obj-C models 2) Create the Swift models & Domain Models 3) Create initializers of DMs from Obj-C + Swift models Migration Steps 7) Extract DMs in a more generic way 6) Use DMs throughout the codebase
  22. The Swift ProductsApiClient func getProducts(baseUrl: String, completion: @escaping (Result<[SwiftModel.Product], NSError>)

    -> Void) { var urlRequest = URLRequest(url: URL(string: "\(baseUrl)/home/get_products")!) urlRequest.httpMethod = "GET" Session .default .request(urlRequest) .validate() .responseDecodable(of: [SwiftModel.Product].self) { response in if let products = response.value { completion?(.success(products)) } else { completion?(.failure(NSError(domain: "Invalid JSON Decoding", code: 0))) } } } Alamo fi re
  23. Request using the Swift Model func fetchProducts(completion: ProductsSwiftClient.shared().getProducts(baseUrl: "a_base_url") {

    result in switch result { case .success(let products): completion(.success(products case .failure(let error): completion(.failure(error)) } } } )) @escaping (Result<[SwiftModel.Product], NSError>) -> Void) {
  24. func fetchProducts(completion: ProductsSwiftClient.shared().getProducts(baseUrl: "a_base_url") { result in switch result {

    case .success(let products): completion(.success(products case .failure(let error): completion(.failure(error)) } } } .map { Domain.Product(model: $0) })) @escaping (Result<[Domain.Product], NSError>) -> Void) { Request using the Domain Model Initialiser
  25. 4) Replace models returned from Obj-C clients with DMs 5)

    Create new Swift clients and return the DMs models 1) Rename the Obj-C models 2) Create the Swift models & Domain Models 3) Create initializers of DMs from Obj-C + Swift models Migration Steps 7) Extract DMs in a more generic way 6) Use DMs throughout the codebase
  26. Swift + Obj-C Clients & Models can co-exist extension Domain

    { struct Product { let productId: Int let name: String let review: String? let price: Double let manufacturer: String let imageUrl: String? let reviewsScore: Float } var shop: Shop? struct Shop { let shopId: Int let shopName: String } var product = products[0] product.shop = shop } SwiftModel.Shop // Old Client var shop: Domain.Shop? OldShopApiClient.shared().getShop(nil) { shop, error in if (error) { // handle error } shop = Domain.Shop(model: shop) } func getProductsAndShop() { // New Client var products: [Domain.Product] = [] ProductsSwiftClient.shared().fetchProducts { response in switch response { case .success(let prods): products = prods.map { Domain.Product(model: $0) } case .failure(let error): print("error") } } OldShop }
  27. ☑ Use objects that have dependencies ☑ Codebase is agnostic

    of implementation details What’s the purpose of this? 🤔 ☑ With a remote feature fl ag we could swap between clients
  28. 4) Replace models returned from Obj-C clients with DMs 7)

    Extract DMs in a more generic way 5) Create new Swift clients and return the DMs 1) Rename the Obj-C models 2) Create the Swift models & Domain Models 3) Create initializers of DMs from Obj-C + Swift models Migration Steps 6) Use DMs throughout the codebase
  29. // Legacy protocol protocol OldModelTransforming { associatedtype TransformType init(from model:

    TransformType) } struct Product self.productId = model.productId.intValue self.name = model.name self.review = model.review self.price = model.price.doubleValue self.manufacturer = model.manufacturer self.imageUrl = model.imageUrl self.reviewsScore = model.reviewsScore?.floatValue } } // Transition protocol protocol NewModelTransforming { associatedtype ModelType init(model: ModelType) } struct Product: NewModelTransforming { init(model: SwiftModel.Product) { self.productId = model.productId self.name = model.name self.review = model.review self.price = model.price self.manufacturer = model.manufacturer self.imageUrl = model.imageUrl self.reviewsScore = model.reviewsScore } } init(model: OldProduct) { init(from model: OldProduct) { : OldModelTransforming { {
  30. /// Extracts domain models from new Swift models static func

    extractFromSwiftModel<T: NewModelTransforming, U>(from items: U?) -> [T]? { // Ensure that U is [T] to exploit the init guard let castResponse = items as? [T.ModelType] else { return nil } return castResponse.map { T(model: $0) } } /// Extracts domain models from old Obj-C models static func extractFromOldModel<T: OldModelTransforming, U>(from items: U?) -> [T]? { // Ensure that U is [T] to exploit the init guard let castResponse = items as? [T.TransformType] else { return nil } return castResponse.map { T(from: $0) } } class DomainModelExtractor { }
  31. func fetchProducts(completion: @escaping ([ OldProductsApiClient.shared().getProducts(nil) { products, error in if

    let error = error { completion(nil, error) } Domain.Product]?, Error?) -> Void) { completion(products.map { Domain.Product(model: $0) }, nil) Obj-C Api Client guard let products = products else { completion(nil, nil) return } } }
  32. func fetchProducts(completion: @escaping ([ OldProductsApiClient.shared().getProducts(nil) { products, error in if

    let error = error { completion(nil, error) } Domain.Product]?, Error?) -> Void) { completion(DomainModelExtractor.extractFromOldModel(from: products), nil) Obj-C Api Client guard let products = products else { completion(nil, nil) return } } }
  33. Swift Api Client func fetchProducts(completion: ProductsSwiftClient.shared().getProducts(baseUrl: "a_base_url") { result in

    switch result { case .success(let products): completion(.success( case .failure(let error): completion(.failure(error)) } } } products.map { Domain.Product(model: $0) })) @escaping (Result<[Domain.Product], NSError>) -> Void) {
  34. Swift Api Client func fetchProducts(completion: ProductsSwiftClient.shared().getProducts(baseUrl: "a_base_url") { result in

    switch result { case .success(let products): completion(.success( case .failure(let error): completion(.failure(error)) } } } @escaping (Result<[Domain.Product], NSError>) -> Void) { DomainModelExtractor.extractFromSwiftModel(from: products))