So Clean Architecture for iOS
Bài đăng này đã không được cập nhật trong 4 năm
Trong những năm qua, ngành công nghiệp phần mềm di động
đã phát triển với một tốc độ chóng mặt. Trước đây các ứng dụng di động chủ yếu là nhỏ và thường chứa ít màn hình. Trong khi bây giờ có rất nhiều ứng dụng khổng lồ, với giao diện người dùng và business logic
rất phức tạp.
Đối với các ứng dụng phức tạp thì mô hình MVC
truyền thống bộc lộ nhiều khuyết điểm như: View
và Controller
có mối quan hệ chặt chẽ với nhau đến mức mà phần Model gần như bị tách biệt làm cho việc Testing rất khó khăn, ViewController phình to vì chưa cả logic bussiness lẫn logic view làm cho việc đọc hiểu code để bảo trì và mở rộng rất khó khăn... Clean Architecture
đã ra đời để giải quyết các vấn đề trên bằng việc phân tách rõ ràng các module dựa trên các nhiệm vụ riêng biệt.
Clean Architecture
Clean Architecture
được giới thiệu bởi Robert C. Martin (a.k.a. Chú Bob) và nhận được sự quan tâm lớn của các developer. Clean Architecture
là một business architecture, nó tách rời những xử lý nghiệp vụ khỏi UI và framework, phân rõ vai trò và trách nhiệm của từng layer trong kiến trúc của mình.
The Anatomy of the Clean Architecture
Có rất nhiều biến thể của Clean Architecture
, trong iOS bạn có thể nghe nói về VIPER
hoặc CleanSwift
. Khi nhảy sang các nền tảng khác như .NET
hay Android
thậm chí còn có nhiều biến thể hơn. Tuy nhiên các biến thể đều có nhiệm vụ như nhau là tách rời những xử lý nghiệp vụ khỏi UI và framework và tuân thủ theo các qui tắc của Clean Architecture
.
Dưới đây là danh sách các thành phần trong kiến trúc:
- View: Giao diện hiển thị nơi xảy ra tương tác giữa app và người dùng, như Storyboard hoặc XIB.
- Controller: Nhận các hành động hoặc event từ view và update nó.
- Interactor: Lớp logic nghiệp vụ nơi Controller gửi các yêu cầu.
- Presenter: Nhận phản hổi từ Interactor để gửi lại cho Controller.
- Router: Có nhiệm vụ điều hướng các ViewController.
Phần cốt lõi của kiến trúc là Controller, Interactor, và Presenter. Một điều quan trọng cần lưu ý đây là kiến trúc unidirectional Data Flow
tức là dữ liệu sẽ di chuyển theo 1 luồng xác định, điều này làm giảm đáng kể sự phức tạp, dễ dàng để quản lý.
Cách hoạt động:
- Người dùng tương tác với View
- Controller nhận sự kiện từ View để gửi đến Interactor.
- Interactor thực hiện các logic business và trả về kết quả cho Presenter
- Presenter format lại dữ liệu sau đó gửi lại về cho Controller thông qua viewModel
- Controller nhận dữ liệu từ Presenter sau đó update lại view.
Practice
Chúng ta định nghĩa các use-case cho từng layer tương ứng
protocol ListProductsDisplayable: class { // View Controller
func displayFetchedProducts(with viewModel: ListProductsModels.ViewModel)
func display(error: AppModels.Error)
}
protocol ListProductsBusinessLogic { // Interactor
func fetchProducts(with request: ListProductsModels.FetchRequest)
}
protocol ListProductsPresentable { // Presenter
func presentFetchedProducts(for response: ListProductsModels.Response)
func presentFetchedProducts(error: DataError)
}
protocol ListProductsRoutable: AppRoutable { // Router
func showProduct(for id: Int)
}
Đây là flow xảy ra tại viewDidload interactor.fetchProducts > presenter.presentFetchedProducts > controller.displayFetchedProducts.
Controller :
class ListProductsViewController: UIViewController {
private lazy var interactor: ListProductsBusinessLogic = ListProductsInteractor(
presenter: ListProductsPresenter(viewController: self),
productsWorker: ProductsWorker(store: ProductsMemoryStore())
)
private lazy var router: ListProductsRoutable = ListProductsRouter(
viewController: self
)
private var viewModel: ListProductsModels.ViewModel?
override func viewDidLoad() {
super.viewDidLoad()
interactor.fetchProducts(
with: ListProductsModels.FetchRequest()
)
}
}
extension ListProductsViewController: ListProductsDisplayable {
func displayFetchedProducts(with viewModel: ListProductsModels.ViewModel) {
self.viewModel = viewModel
tableView.reloadData()
}
func display(error: AppModels.Error) {
let alertController = UIAlertController(
title: error.title,
message: error.message,
preferredStyle: .alert
)
alertController.addAction(
UIAlertAction(title: "OK", style: .default, handler: nil)
)
present(alertController, animated: true, completion: nil)
}
}
extension ListProductsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let model = viewModel?.products[indexPath.row] else { return }
router.showProduct(for: model.id)
}
}
Controller tạo một router và một instances đến Interactor thông qua 1 presenter. Gọi Interactor ở viewDidload
để fetch list product.
struct ListProductsInteractor {
private let presenter: ListProductsPresentable
private let productsWorker: ProductsWorkerType
init(presenter: ListProductsPresentable, productsWorker: ProductsWorkerType) {
self.presenter = presenter
self.productsWorker = productsWorker
}
}
extension ListProductsInteractor: ListProductsBusinessLogic {
func fetchProducts(with request: ListProductsModels.FetchRequest) {
productsWorker.fetch {
guard let value = $0.value, $0.isSuccess else {
return self.presenter.presentFetchedProducts(error: $0.error ?? .unknownReason(nil))
}
self.presenter.presentFetchedProducts(
for: ListProductsModels.Response(products: value)
)
}
}
}
Inject 1 productsWorker
ở Interactor để xử lý API.
struct ListProductsPresenter: ListProductsPresentable {
private weak var viewController: ListProductsDisplayable?
private let currencyFormatter: NumberFormatter
init(viewController: ListProductsDisplayable?) {
self.viewController = viewController
self.currencyFormatter = NumberFormatter()
self.currencyFormatter.numberStyle = .currency
}
}
extension ListProductsPresenter {
func presentFetchedProducts(for response: ListProductsModels.Response) {
let viewModel = ListProductsModels.ViewModel(
products: response.products.map {
ListProductsModels.ProductViewModel(
id: $0.id,
name: $0.name,
content: $0.content,
price: currencyFormatter.string(from: NSNumber(value: Float($0.priceCents) / 100)) ?? "\($0.priceCents / 100)"
)
}
)
viewController?.displayFetchedProducts(with: viewModel)
}
func presentFetchedProducts(error: DataError) {
// Handle and parse error
let viewModel = AppModels.Error(
title: NSLocalizedString("products.error.title", "Title for product error"),
message: String(format: NSLocalizedString("products.error.message", "Message for product error"), error)
)
viewController?.display(error: viewModel)
}
}
Presenter
sẽ làm nhiệm vụ format lại dữ liệu respone và gọi controller để hiện thị lên View
Các Model
được gói gọn trong một enum và chỉ liên quan đến trường hợp sử dụng riêng của nó.
enum ListProductsModels {
struct FetchRequest {
}
struct SearchRequest {
let text: String
}
struct Response {
let products: [ProductType]
}
struct ViewModel {
let products: [ProductViewModel]
}
struct ProductViewModel {
let id: Int
let name: String
let content: String
let price: String
}
}
Cuối cùng Router
chịu trách nhiệm điều khiển các luồng đi của ứng dụng:
struct ListProductsRouter {
weak var viewController: UIViewController?
init(viewController: UIViewController?) {
self.viewController = viewController
}
}
extension ListProductsRouter: ListProductsRoutable {
func showProduct(for id: Int) {
let storyboard = UIStoryboard(name: "ShowProduct", bundle: nil)
guard let controller = storyboard.instantiateInitialViewController()) as? ShowProductViewController
else { return assertionFailure("Invalid controller for storyboard \(storyboard).") }
controller.productID = id
viewController?.present(controller, animated: true)
}
}
router.showProduct(for: productID)
Conclusion
Clean architechture rất linh hoạt, dễ dàng bảo trì và mở rộng.Mặc dù nó dài dòng hơn các kiến trúc khác, nhưng nó là cần thiết để làm giảm sự phụ thuộc giứa các layer trong ứng dụng.
Link tham khảo: https://basememara.com/swift-clean-architecture/
All rights reserved