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
PostCodeValidatorProtocolimplementation—no changes needed to the ViewModel. - Liskov Substitution: You can swap the
HomeViewModelwith any other implementation ofHomeViewModelProtocolwithout breaking the View. - Interface Segregation: The protocols are minimal—
HomeViewModelProtocolonly exposes what the View needs, andPostCodeValidatorProtocolonly 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




