AgentSkillsCN

Legacy to VIP+W Migration

从遗留代码或“意大利面式” iOS 代码重构为 VIP+W 清晰架构的分步指南

SKILL.md
--- frontmatter
name: Legacy to VIP+W Migration
description: Step-by-step guide for refactoring legacy/spaghetti iOS code to VIP+W Clean Architecture
version: 1.0.0

Legacy to VIP+W Migration Guide

A systematic approach to refactoring legacy iOS codebases (MVC, Massive ViewController, spaghetti code) to VIP+W Clean Architecture.

Migration Philosophy

Golden Rules

  1. Never rewrite from scratch - Incremental migration preserves working code
  2. One screen at a time - Migrate scene by scene, not all at once
  3. Tests first - Write tests for existing behavior before refactoring
  4. Keep it working - App should work after each migration step
  5. Strangler pattern - New code in VIP+W, gradually replace old code

Migration Priority

Start with screens that are:

  1. Frequently changed - High ROI for clean architecture
  2. Bug-prone - Messy code causes bugs
  3. Well-understood - You know the expected behavior
  4. Isolated - Fewer dependencies on other screens

Phase 1: Analysis

Step 1.1: Identify the Mess

Common legacy patterns to identify:

swift
// 🍝 SPAGHETTI: Everything in ViewController
class ProductViewController: UIViewController {
    // UI + Business Logic + Networking + Navigation ALL HERE

    func loadProduct() {
        // Direct API call in ViewController
        URLSession.shared.dataTask(with: url) { data, _, _ in
            let product = try? JSONDecoder().decode(Product.self, from: data!)

            DispatchQueue.main.async {
                // UI update mixed with business logic
                self.titleLabel.text = product?.name
                self.priceLabel.text = "$\(product?.price ?? 0)"

                if product?.isOnSale == true {
                    self.priceLabel.textColor = .red
                    self.saveBadge.isHidden = false
                }

                // Navigation logic here too
                if product == nil {
                    self.navigationController?.popViewController(animated: true)
                }
            }
        }.resume()
    }

    @IBAction func buyTapped() {
        // More API calls, validation, navigation...
    }
}

Step 1.2: Document Current Behavior

Before refactoring, document:

markdown
## ProductViewController Analysis

### Responsibilities (Current)
1. Display product details
2. Fetch product from API
3. Format price with currency
4. Show/hide sale badge
5. Handle buy action
6. Navigate back on error
7. Add to cart

### API Calls
- GET /products/{id}
- POST /cart/items

### Navigation
- Pop on error
- Push to checkout on buy

### State
- product: Product?
- isLoading: Bool
- cartItems: [CartItem]

### Side Effects
- Analytics tracking
- Local cart storage

Step 1.3: Write Characterization Tests

Capture existing behavior before changing anything:

swift
// Test existing behavior (even if messy)
final class ProductViewControllerLegacyTests: XCTestCase {

    func test_loadProduct_displaysProductName() {
        // Given
        let sut = ProductViewController()
        let mockProduct = Product(name: "Test", price: 99.99)
        // Inject mock somehow...

        // When
        sut.loadView()
        sut.viewDidLoad()

        // Then
        XCTAssertEqual(sut.titleLabel.text, "Test")
    }

    func test_loadProduct_onSale_showsSaleBadge() {
        // Capture current behavior
    }
}

Phase 2: Extract Components

Step 2.1: Create VIP+W File Structure

bash
mkdir -p Features/Product
touch Features/Product/ProductViewController.swift
touch Features/Product/ProductInteractor.swift
touch Features/Product/ProductPresenter.swift
touch Features/Product/ProductWorker.swift
touch Features/Product/ProductRouter.swift
touch Features/Product/ProductModels.swift
touch Features/Product/ProductConfigurator.swift

Step 2.2: Define Protocols First

swift
// ProductModels.swift
enum Product {

    enum FetchProduct {
        struct Request {
            let productId: String
        }

        struct Response {
            let result: Result<ProductEntity, ProductError>
        }

        struct ViewModel {
            let title: String
            let formattedPrice: String
            let showSaleBadge: Bool
            let saleBadgeText: String?
        }
    }

    enum AddToCart {
        struct Request {
            let quantity: Int
        }

        struct Response {
            let result: Result<Void, CartError>
        }

        struct ViewModel {
            let message: String
            let isSuccess: Bool
        }
    }
}
swift
// Protocols
protocol ProductDisplayLogic: AnyObject {
    func displayProduct(viewModel: Product.FetchProduct.ViewModel)
    func displayAddToCartResult(viewModel: Product.AddToCart.ViewModel)
    func displayLoading(isLoading: Bool)
    func displayError(message: String)
}

protocol ProductBusinessLogic {
    func fetchProduct(request: Product.FetchProduct.Request)
    func addToCart(request: Product.AddToCart.Request)
}

protocol ProductPresentationLogic {
    func presentProduct(response: Product.FetchProduct.Response)
    func presentAddToCartResult(response: Product.AddToCart.Response)
    func presentLoading(isLoading: Bool)
    func presentError(_ error: Error)
}

protocol ProductWorkerLogic {
    func fetchProduct(id: String) async throws -> ProductEntity
    func addToCart(productId: String, quantity: Int) async throws
}

protocol ProductRoutingLogic {
    func routeToCheckout()
    func routeBack()
}

Step 2.3: Extract Worker (Network/Data)

Move API calls from ViewController to Worker:

swift
// BEFORE (in ViewController)
URLSession.shared.dataTask(with: url) { data, _, _ in
    let product = try? JSONDecoder().decode(Product.self, from: data!)
    // ...
}

// AFTER (in Worker)
final class ProductWorker: ProductWorkerLogic {

    private let apiClient: APIClientProtocol
    private let cartStorage: CartStorageProtocol

    init(
        apiClient: APIClientProtocol = APIClient.shared,
        cartStorage: CartStorageProtocol = CartStorage.shared
    ) {
        self.apiClient = apiClient
        self.cartStorage = cartStorage
    }

    func fetchProduct(id: String) async throws -> ProductEntity {
        let endpoint = ProductEndpoint.getProduct(id: id)
        return try await apiClient.request(endpoint)
    }

    func addToCart(productId: String, quantity: Int) async throws {
        let endpoint = CartEndpoint.addItem(productId: productId, quantity: quantity)
        try await apiClient.request(endpoint)
        cartStorage.addItem(productId: productId, quantity: quantity)
    }
}

Step 2.4: Extract Presenter (Formatting)

Move formatting logic from ViewController to Presenter:

swift
// BEFORE (in ViewController)
self.priceLabel.text = "$\(product?.price ?? 0)"
if product?.isOnSale == true {
    self.priceLabel.textColor = .red
}

// AFTER (in Presenter)
final class ProductPresenter: ProductPresentationLogic {

    weak var viewController: ProductDisplayLogic?

    private let currencyFormatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = Locale.current
        return formatter
    }()

    func presentProduct(response: Product.FetchProduct.Response) {
        let viewModel: Product.FetchProduct.ViewModel

        switch response.result {
        case .success(let product):
            viewModel = Product.FetchProduct.ViewModel(
                title: product.name,
                formattedPrice: formatPrice(product.price, isOnSale: product.isOnSale),
                showSaleBadge: product.isOnSale,
                saleBadgeText: product.isOnSale ? "SALE \(product.discountPercent)% OFF" : nil
            )

        case .failure(let error):
            presentError(error)
            return
        }

        viewController?.displayProduct(viewModel: viewModel)
    }

    private func formatPrice(_ price: Decimal, isOnSale: Bool) -> String {
        let formatted = currencyFormatter.string(from: price as NSDecimalNumber) ?? ""
        return formatted
    }
}

Step 2.5: Extract Interactor (Business Logic)

Move business logic from ViewController to Interactor:

swift
// BEFORE (in ViewController)
// Business decisions mixed with UI

// AFTER (in Interactor)
final class ProductInteractor: ProductBusinessLogic, ProductDataStore {

    var presenter: ProductPresentationLogic?
    var worker: ProductWorkerLogic?
    var router: ProductRoutingLogic?

    // DataStore
    var product: ProductEntity?
    var productId: String?

    func fetchProduct(request: Product.FetchProduct.Request) {
        presenter?.presentLoading(isLoading: true)

        Task { @MainActor in
            defer { presenter?.presentLoading(isLoading: false) }

            do {
                let product = try await worker?.fetchProduct(id: request.productId)
                self.product = product
                let response = Product.FetchProduct.Response(result: .success(product!))
                presenter?.presentProduct(response: response)
            } catch {
                let response = Product.FetchProduct.Response(result: .failure(.fetchFailed))
                presenter?.presentProduct(response: response)
            }
        }
    }

    func addToCart(request: Product.AddToCart.Request) {
        guard let product = product else { return }

        Task { @MainActor in
            do {
                try await worker?.addToCart(productId: product.id, quantity: request.quantity)
                let response = Product.AddToCart.Response(result: .success(()))
                presenter?.presentAddToCartResult(response: response)
            } catch {
                let response = Product.AddToCart.Response(result: .failure(.addFailed))
                presenter?.presentAddToCartResult(response: response)
            }
        }
    }
}

Step 2.6: Refactor ViewController (Display Only)

swift
// AFTER - Clean ViewController
final class ProductViewController: UIViewController {

    // MARK: - VIP

    var interactor: ProductBusinessLogic?
    var router: ProductRoutingLogic?

    // MARK: - UI

    private let titleLabel = UILabel()
    private let priceLabel = UILabel()
    private let saleBadge = UILabel()
    private let buyButton = UIButton()
    private let loadingIndicator = UIActivityIndicatorView()

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        fetchProduct()
    }

    // MARK: - Setup

    private func setupUI() {
        // Pure UI setup - no business logic
    }

    // MARK: - Actions

    private func fetchProduct() {
        guard let productId = router?.dataStore?.productId else { return }
        let request = Product.FetchProduct.Request(productId: productId)
        interactor?.fetchProduct(request: request)
    }

    @objc private func buyTapped() {
        let request = Product.AddToCart.Request(quantity: 1)
        interactor?.addToCart(request: request)
    }
}

// MARK: - DisplayLogic

extension ProductViewController: ProductDisplayLogic {

    func displayProduct(viewModel: Product.FetchProduct.ViewModel) {
        titleLabel.text = viewModel.title
        priceLabel.text = viewModel.formattedPrice
        saleBadge.isHidden = !viewModel.showSaleBadge
        saleBadge.text = viewModel.saleBadgeText
    }

    func displayAddToCartResult(viewModel: Product.AddToCart.ViewModel) {
        if viewModel.isSuccess {
            showToast(viewModel.message)
        } else {
            showError(viewModel.message)
        }
    }

    func displayLoading(isLoading: Bool) {
        if isLoading {
            loadingIndicator.startAnimating()
        } else {
            loadingIndicator.stopAnimating()
        }
    }

    func displayError(message: String) {
        showAlert(title: "Error", message: message)
    }
}

Phase 3: Wire Everything

Step 3.1: Create Configurator

swift
enum ProductConfigurator {

    static func configure(productId: String) -> ProductViewController {
        let viewController = ProductViewController()
        let interactor = ProductInteractor()
        let presenter = ProductPresenter()
        let worker = ProductWorker()
        let router = ProductRouter()

        viewController.interactor = interactor
        viewController.router = router
        interactor.presenter = presenter
        interactor.worker = worker
        interactor.router = router
        interactor.productId = productId
        presenter.viewController = viewController
        router.viewController = viewController
        router.dataStore = interactor

        return viewController
    }
}

Step 3.2: Update Navigation

swift
// BEFORE (legacy)
let vc = ProductViewController()
vc.productId = "123"
navigationController?.pushViewController(vc, animated: true)

// AFTER (VIP+W)
let vc = ProductConfigurator.configure(productId: "123")
navigationController?.pushViewController(vc, animated: true)

Phase 4: Verify & Clean Up

Step 4.1: Run Tests

swift
// New VIP+W tests
final class ProductInteractorTests: XCTestCase {

    func test_fetchProduct_success_presentsProduct() async {
        // Given
        let mockPresenter = MockProductPresenter()
        let mockWorker = MockProductWorker()
        mockWorker.fetchProductResult = .success(ProductEntity.mock)

        let sut = ProductInteractor()
        sut.presenter = mockPresenter
        sut.worker = mockWorker

        // When
        sut.fetchProduct(request: .init(productId: "123"))
        await waitForAsync()

        // Then
        XCTAssertTrue(mockPresenter.presentProductCalled)
    }
}

Step 4.2: Remove Legacy Code

swift
// Delete old methods from ViewController
// - loadProduct()
// - All URLSession calls
// - All formatting logic
// - All business logic

Step 4.3: Update Characterization Tests

Convert legacy tests to VIP+W tests:

swift
// BEFORE: Testing ViewController directly
func test_legacy_loadProduct_displaysName() {
    let sut = ProductViewController()
    // ...
}

// AFTER: Testing each component
func test_presenter_formatsPriceCorrectly() {
    let sut = ProductPresenter()
    // ...
}

func test_interactor_fetchesFromWorker() {
    let sut = ProductInteractor()
    // ...
}

Migration Checklist

Per Screen Migration

  • Analysis

    • Document current responsibilities
    • List all API calls
    • List all navigation paths
    • Identify state variables
  • Preparation

    • Write characterization tests
    • Create VIP+W file structure
    • Define protocols
  • Extraction

    • Extract Worker (API calls)
    • Extract Presenter (formatting)
    • Extract Interactor (business logic)
    • Extract Router (navigation)
    • Create Models (Request/Response/ViewModel)
  • Integration

    • Create Configurator
    • Update ViewController to DisplayLogic only
    • Wire all components
  • Verification

    • All tests pass
    • Manual testing
    • Remove dead code
    • Code review

Common Migration Patterns

Singleton → Injected Dependency

swift
// BEFORE
class ViewController {
    func save() {
        UserDefaults.standard.set(value, forKey: "key")
    }
}

// AFTER
protocol StorageProtocol {
    func save(_ value: Any, forKey: String)
}

class Worker {
    private let storage: StorageProtocol

    init(storage: StorageProtocol = UserDefaultsStorage()) {
        self.storage = storage
    }
}

Callback Hell → Async/Await

swift
// BEFORE
func loadData() {
    api.fetchUser { user in
        api.fetchOrders(userId: user.id) { orders in
            api.fetchProducts(orderIds: orders.map { $0.id }) { products in
                self.display(user, orders, products)
            }
        }
    }
}

// AFTER
func loadData() async throws {
    let user = try await worker.fetchUser()
    let orders = try await worker.fetchOrders(userId: user.id)
    let products = try await worker.fetchProducts(orderIds: orders.map { $0.id })
    return (user, orders, products)
}

Massive Switch → Strategy Pattern

swift
// BEFORE
func handleAction(_ action: String) {
    switch action {
    case "buy": // 50 lines
    case "save": // 50 lines
    case "share": // 50 lines
    // ...
    }
}

// AFTER
protocol ActionHandler {
    func handle()
}

class BuyHandler: ActionHandler { ... }
class SaveHandler: ActionHandler { ... }
class ShareHandler: ActionHandler { ... }

Tips for Success

  1. Start small - Pick the simplest screen first
  2. Pair program - Migration is easier with a partner
  3. Time-box - Set limits to avoid endless refactoring
  4. Document decisions - Future you will thank you
  5. Celebrate wins - Each migrated screen is progress!

Pre-Migration Interview

Before starting any migration, gather requirements using AskUserQuestion:

Migration Questions

Question 1: Migration Goal

  • Header: "Goal"
  • Question: "What is your migration goal?"
  • Options:
    • Full VIP+W migration - Complete architecture transformation
    • Partial cleanup - Extract some components, keep others
    • Just analyze - Understand the mess before deciding
    • Incremental improvement - Small steps over time

Question 2: Current State

  • Header: "Current"
  • Question: "What's the current code architecture?"
  • Options:
    • Massive ViewController - Everything in one file
    • MVC with fat models - Logic spread across M and C
    • Mixed patterns - Some clean, some messy
    • Unknown - Need to analyze first

Question 3: Test Coverage

  • Header: "Tests"
  • Question: "What's the current test situation?"
  • Options:
    • Good test coverage - Can refactor confidently
    • Some tests exist - Partial safety net
    • No tests - Need to add tests first
    • Don't know - Need to check

Question 4: Risk Tolerance

  • Header: "Risk"
  • Question: "How much risk can you accept?"
  • Options:
    • Low risk (Recommended) - Small, verified changes
    • Medium risk - Larger changes with testing
    • High risk - Aggressive refactoring, tight timeline

Interview Flow

  1. Ask questions using AskUserQuestion
  2. Summarize: "Planning [goal] migration from [current state] with [tests] and [risk] tolerance"
  3. Analyze current code
  4. Present migration plan
  5. Get approval before making changes

Skip Interview If:

  • User provided detailed migration requirements
  • User says "skip questions" or "just do it"
  • User just wants specific refactoring guidance