You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

MVVM架构下Protocol使用可选类型的合理性及基于SOLID原则优化getText方法的咨询

Great question—let’s break this down step by step, because you’re already on the right track with separating concerns between your View and ViewModel!

Your Core Idea Is Totally Correct

First off, your instinct to keep the View free of business logic (including optional binding) is spot-on. The View’s job in MVVM should be only to render UI and forward user interactions to the ViewModel. Handling optional values, validation, or any data transformation belongs squarely in the ViewModel—this aligns perfectly with the Single Responsibility Principle (one of SOLID’s pillars), where each component has one clear job.

There’s no "mistake" in passing optional postCode values to the ViewModel; in fact, this is exactly how MVVM is supposed to work. The View shouldn’t care if the input is nil or empty—it just passes what the user entered, and lets the ViewModel handle the rest.

Fixing & Optimizing Your Implementation (With SOLID in Mind)

Let’s look at your code and refine it to be more robust and SOLID-compliant:

1. Fix the Immediate Code Issue

First, your ViewModel has a syntax error: in getText, you’re trying to reassign the postCode parameter (which is a let by default in Swift). Instead, you should process the optional value and use it to update state (or perform logic) in the ViewModel.

2. Use Reactive Binding for UI Updates

Instead of having the View call a method on the ViewModel and wait for a result, use reactive patterns (like Combine) to let the ViewModel publish state changes. This keeps the View passive and follows the Dependency Inversion Principle (the View depends on an abstraction, not a concrete ViewModel).

3. Separate Concerns with Abstractions

If your getText logic involves things like postcode validation, extract that into a separate service. This follows the Single Responsibility Principle and makes your code easier to test and modify.

Here’s a revised implementation:

ViewModel & Protocols

import Combine

// Define a minimal interface for your ViewModel (Interface Segregation Principle)
protocol HomeViewModelProtocol {
    var displayTextPublisher: AnyPublisher<String, Never> { get }
    func handlePostCodeInput(_ postCode: String?)
}

class HomeViewModel: HomeViewModelProtocol {
    // Inject a validator dependency (Dependency Inversion Principle)
    private let postCodeValidator: PostCodeValidatorProtocol
    @Published private var displayText: String = ""
    
    var displayTextPublisher: AnyPublisher<String, Never> {
        $displayText.eraseToAnyPublisher()
    }
    
    // Use dependency injection for flexibility (e.g., swap validators for testing)
    init(validator: PostCodeValidatorProtocol = DefaultPostCodeValidator()) {
        self.postCodeValidator = validator
    }
    
    func handlePostCodeInput(_ postCode: String?) {
        // Handle optional binding and cleanup in the ViewModel
        guard let trimmedPostCode = postCode?.trimmingCharacters(in: .whitespacesAndNewlines),
              !trimmedPostCode.isEmpty else {
            displayText = "Please enter a postcode"
            return
        }
        
        if postCodeValidator.isValid(trimmedPostCode) {
            // Add your business logic here (e.g., fetch address, format text)
            displayText = "Valid postcode: \(trimmedPostCode)"
        } else {
            displayText = "Invalid postcode format"
        }
    }
}

// Abstract validator interface (Dependency Inversion)
protocol PostCodeValidatorProtocol {
    func isValid(_ postCode: String) -> Bool
}

// Concrete validator implementation
class DefaultPostCodeValidator: PostCodeValidatorProtocol {
    func isValid(_ postCode: String) -> Bool {
        // Example validation: adjust to your region's rules
        return postCode.count == 6 
    }
}

View Implementation

class HomeViewController: UIViewController {
    private let viewModel: HomeViewModelProtocol
    private var cancellables = Set<AnyCancellable>()
    
    @IBOutlet weak var postCodeTextField: UITextField!
    @IBOutlet weak var resultLabel: UILabel!
    
    // Inject ViewModel for testability (Dependency Inversion)
    init(viewModel: HomeViewModelProtocol = HomeViewModel()) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTextField()
        bindViewModel()
    }
    
    private func setupTextField() {
        postCodeTextField.addTarget(self, action: #selector(postCodeFieldDidChange(_:)), for: .editingChanged)
    }
    
    private func bindViewModel() {
        // Subscribe to ViewModel state changes to update UI
        viewModel.displayTextPublisher
            .receive(on: DispatchQueue.main)
            .assign(to: \.text, on: resultLabel)
            .store(in: &cancellables)
    }
    
    @objc private func postCodeFieldDidChange(_ textField: UITextField) {
        // View only forwards input—no logic here!
        viewModel.handlePostCodeInput(textField.text)
    }
}

How This Follows SOLID

Let’s map this to SOLID principles:

  • Single Responsibility: View handles UI; ViewModel handles business logic; Validator handles postcode validation. No component does more than one job.
  • Open/Closed: Want to add a new postcode validation rule? Just create a new PostCodeValidatorProtocol implementation—no changes needed to the ViewModel.
  • Liskov Substitution: You can swap the HomeViewModel with any other implementation of HomeViewModelProtocol without breaking the View.
  • Interface Segregation: The protocols are minimal—HomeViewModelProtocol only exposes what the View needs, and PostCodeValidatorProtocol only defines validation logic.
  • Dependency Inversion: The ViewModel depends on an abstract validator, not a concrete class. This makes testing easy (you can mock the validator) and the code flexible.

Final Takeaway

Your initial approach was correct—keeping optional handling out of the View is exactly what MVVM and SOLID recommend. By adding reactive binding, dependency injection, and separating concerns with abstractions, you’ll end up with code that’s more maintainable, testable, and scalable.

内容的提问来源于stack exchange,提问作者Kariny

火山引擎 最新活动