اذا كنت مطور SwiftUI فمن المحتمل أنك استخدمت MVVM في أحد مشاريعك أو على الأقل سمعت به كثيراً. ومع أن MVVM يعتبر من أكثر الأنماط انتشاراً بين مطوري iOS
إلا أنك ستلاحظ أن العديد من المشاريع الكبيرة لا تكتفي به وحده، بل تضيف طبقات أخرى تعرف بـ Clean Architecture
في البداية قد يبدو الأمر وكأنه تعقيد إضافي أو مجرد زيادة في عدد الملفات، لكن مع نمو حجم التطبيق وازدياد عدد الشاشات والخدمات والمنطق البرمجي، تبدأ الفوائد الحقيقية بالظهور
في هذا المقال سنتعرف على كيفية دمج MVVM مع Clean Architecture، وما الدور الذي تؤديه كل طبقة، ولماذا تعتمد عليه الكثير من الفرق والمشاريع الاحترافية لبناء تطبيقات أكثر تنظيماً وقابلية للتوسع والصيانة
لماذا MVVM لحاله لا يكفي وحده في المشاريع الكبيره؟
في MVVM، يمثل الـ ViewModel المحور الاساسي فهو يستقبل تفاعلات المستخدم من الـ View، ويتواصل مع الخدمات الخارجية لمعالجة البيانات. في التطبيقات الصغيرة، يعمل هذا النموذج بشكل رائع
لكن مع نمو التطبيق وتعدد الميزات، يبدأ الـ ViewModel بالتضخم بشكل متسارع ليتحول إلى ما يُعرف بـ Massive View Model
ستجده فجأة مسؤولاً عن طلبات الشبكة و الـ Business Logic، وتجهيز البيانات لعرضها. هذا التداخل يجعل الكود معقداً، ويقلل من قابلية إعادة استخدامه
والأهم من ذلك يجعل كتابة اختبارات Unit Tests أمراً صعبا ومعقداً
هنا يأتي دور Clean Architecture لإعادة هيكلة المسؤوليات وتوزيعها بشكل منطقي
فكرة Clean Architecture وهيكلة الطبقات
يعتمد الـ Clean Architecture على مبدأ فصل الاهتمامات ، فالفكرة الأساسية هي تقسيم التطبيق إلى طبقات معزولة
بحيث تكون الطبقات الداخلية (التي تحتوي على منطق العمل الأساسي) مستقلة تماماً عن الطبقات الخارجية (مثل واجهات المستخدم وقواعد البيانات)
شرح مفهوم الـ Clean Architecture مع MVVM
الـ Clean Architecture مع MVVM في مشاريع SwiftUI تقسم المشروع إلى ثلاث طبقات رئيسية:
1- طبقة الـ Domain Layer
تعتبر هذه الطبقة عقل التطبيق والمسؤولة عن القواعد والمنطق الأساسي للتطبيق
المميز فيها أنها لا تعرف من أين تأتي البيانات ولا تهتم إذا كانت من الإنترنت أو قاعدة بيانات
كل ما يهمها هو تنفيذ منطق العمل بشكل صحيح
وتنقسم لعدة اقسام
- Entities
وهي الـ Models التي تمثل البيانات الأساسية للتطبيق والتي يتم استخدامها داخل طبقة Domain بعيداً عن تفاصيل الـ API أو قواعد البيانات
- Use Cases
هي العمليات أو المهام التي يستطيع التطبيق تنفيذها وكل Use Case يكون مسؤول عن مهمة واحدة فقط
الغرض منها هو فصل المنطق عن الواجهة وعن مصادر البيانات بحيث يصبح الكود أكثر تنظيما واسهل في الاختبار
- Repositories
هي عباره عن Protocols تحدد البيانات والعمليات التي تحتاجها طبقة Domain دون معرفة كيفية تنفيذها
2- طبقة الـ Data Layer
هذه الطبقة مسؤولة عن التعامل مع البيانات داخل التطبيق سواء من حيث جلبها أو حفظها أو تحديثها وهي تعبر الحلقة التي توصل طبقة الـ Domain بمصادر البيانات المختلفة دون أن تحتاج الطبقات الأخرى لمعرفة تفاصيل التنفيذ
وتنقسم لعدة أقسام
- DTOs (Data Transfer Objects)
وهي نماذج مخصصة للتعامل مع البيانات القادمة من مصادر البيانات المختلفة مثل الـ API أو قواعد البيانات
وتكون مسؤولة عن تمثيل البيانات بالشكل الذي يصل من المصدر ثم تحويلها إلى Entities داخل طبقة الـ Domain ليتم استخدامها داخل التطبيق
أهميتها تكون في عزل التطبيق عن التغيرات التي قد تحدث في مصادر البيانات ففي حال تم تغيير هيكل البيانات القادم من الـ API، فقط سوف تحتاج الى تعديل الـ DTO فقط دون الحاجة الى تعديل الـ Entities
- Data Sources
هي المكونات المسؤولة عن التواصل المباشر مع مصادر البيانات المختلفة وقراءة البيانات أو حفظها
وتحتوي على أكواد Networking للتعامل مع الـ APIs، وأكواد التخزين المحلي مع قواعد البيانات مثل SwiftData و CoreData وغيرها
- Repositories
وهي المسؤولة عن تنفيذ الـ Protocols الموجودة في طبقة الـ Domain وتوفير البيانات المطلوبة للتطبيق
تعتبر حلقة الوصل بين طبقة الـ Domain ومصادر البيانات المختلفة، حيث تقوم بالاعتماد على الـ Data Sources لجلب البيانات أو حفظها ثم إرجاعها بالشكل المناسب لطبقة الـ Domain
كما أنها تخفي تفاصيل مصدر البيانات عن بقية أجزاء التطبيق، لذلك لا تحتاج طبقة الـ Domain لمعرفة ما إذا كانت البيانات قادمة من API أو قاعدة بيانات محلية أو أي مصدر آخر
3. طبقة الـ Presentation Layer
وهي الطبقة المسؤولة عن كل ما يراه المستخدم ويتفاعل معه
في تطبيقات SwiftUI غالباً يتم تطبيق الـ MVVM داخل طبقة Presentation Layer
وتنقسم الى
-
View
هي المسؤولة عن عرض البيانات ورسم الواجهة واستقبال تفاعلات المستخدم
-
ViewModel
وهي حلقة الوصل بين الـ View وطبقة الـ Domain
تستقبل الأحداث والطلبات القادمة من الـ View، ثم تستدعي الـ Use Cases المناسبة في طبقة الـ Domain، وبعد الحصول على النتيجة تقوم بتحديث الـ State ليتم عكس التغييرات تلقائياً على الواجهة
كما أنها مسؤولة عن تجهيز البيانات بالشكل المناسب للعرض وإدارة حالات الشاشة المختلفة مثل التحميل ونجاح العملية والأخطاء
عند تفاعل المستخدم مع الواجهة، ينتقل الطلب أولاً إلى الـ ViewModel. بعد ذلك يستدعي الـ ViewModel الـ Use Case المناسب داخل طبقة الـ Domain، والذي بدوره يتواصل مع الـ Repository للحصول على البيانات المطلوبة. يقوم الـ Repository بالتعامل مع الـ Data Sources لجلب البيانات، ثم تعود النتيجة مرة أخرى إلى الـ ViewModel ليتم تحديث الواجهة وعرض البيانات للمستخدم
تطبيق عملي
بعد أن تعرفنا على الطبقات المختلفة ودور كل طبقة، حان الوقت لرؤية كيفية تطبيق هذه المفاهيم في المشروع
في هذا المشروع سوف نعتمد على API محاني يدعى Jsonplaceholder
وتحديدا هذا الـ API الذي يحتوي على مقالات عشوائية
https://jsonplaceholder.typicode.com/posts
قبل أن نبدأ قم بعمل مجلدات بهذا الشكل

الان نبدأ في التطبيق
طبقة الـ Domain
Entities
قم بإنشاء ملف Swift بإسم Post في داخل مجلد Entities
ثم اكتب هذا الكود
import Foundation
struct Post: Identifiable, Hashable {
let userId: Int
let id: Int
let title: String
let body: String
}
تذكر أن الـ Entity هو الـ Model الأساسي الذي يستخدم داخل التطبيق وطبقة الـ Domain وليس الـ Model الذي يمثل شكل البيانات القادمة من الـ API
Repositories
قم بإنشاء ملف Swift بإسم PostRepository في داخل مجلد Repositories
ثم اكتب هذا الكود
import Foundation
protocol PostRepository {
func fetchPost() async throws -> [Post]
}
هنا نقوم فقط بتعريف الـ functions المطلوبة على شكل Protocol بدون أي تفاصيل للتنفيذ
الهدف هو أن تعرف طبقة الـ Domain ما هي العمليات المتاحة دون معرفة كيفية تنفيذها
بينما سيتم تنفيذ هذه العمليات لاحقاً داخل طبقة الـ Data
UseCases
قم بإنشاء ملف Swift بإسم GetPostsUseCase في داخل مجلد UseCases
ثم اكتب هذا الكود
import Foundation
protocol GetPostsUseCase {
func execute() async throws -> [Post]
}
struct DefaultGetPostsUseCase: GetPostsUseCase {
private let repository: PostRepository
init(respository: PostRepository) {
self.repository = respository
}
func execute() async throws -> [Post] {
try await repository.fetchPost()
}
}
الـ Use Case يمثل عملية واحدة فقط داخل التطبيق
في هذا المثال مسؤوليته جلب قائمة المقالات لاحظ أن الـ Use Case لا يعرف من أين تأتي البيانات ولا يتعامل مباشرة مع الـ API أو قاعدة البيانات
كل ما يفعله هو استدعاء الـ Repository وطلب البيانات منه بهذه الطريقة يبقى منطق التطبيق معزولاً عن تفاصيل جلب البيانات، مما يجعل الكود أسهل في الاختبار والصيانة
طبقة الـ Data
DTOs
قم بإنشاء ملف Swift بإسم PostDTO في داخل مجلد DTOs
ثم اكتب هذا الكود
struct PostDTO: Decodable {
let userId: Int
let id: Int
let title: String
let body: String
func toDomain() -> Post {
Post(userId: userId, id: id, title: title, body: body)
}
}
تذكر أن الـ DTO يمثل البيانات كما تصل من مصدر البيانات مثل الـ API
بينما الـ Entity يمثل البيانات التي يستخدمها التطبيق داخل طبقة الـ Domain
قد تبدو الحقول متشابهة في هذا المثال البسيط لكن في المشاريع غالباً يختلف شكل البيانات القادمة من الـ API عن الشكل الذي يحتاجه التطبيق
لذلك نقوم بفصل الـ DTO عن الـ Entity حتى لا يصبح التطبيق مرتبطاً بشكل البيانات القادم من الـ API
لاحظ يوجد function باسم toDomain
وظيفته تحويل الـ DTO الموجود في طبقة الـ Data إلى Entity في طبقة الـ Domain.
بهذه الطريقة تبقى طبقة الـ Domain معزولة تماماً عن تفاصيل الـ API ولا تتعامل إلا مع الـ Entities الخاصة بها
فإذا تغيرت استجابة الـ API مستقبلاً، غالباً سنحتاج إلى تعديل الـ DTO فقط دون التأثير على بقية أجزاء التطبيق
DataSources
قم بإنشاء ملف Swift بإسم PostRemoteDataSource في داخل مجلد DataSources
ثم اكتب هذا الكود
import Foundation
protocol PostRemoteDataSource {
func getPosts() async throws -> [PostDTO]
}
struct DefaultPostRemoteDataSource: PostRemoteDataSource {
private let session: URLSession
init(session: URLSession = .shared) {
self.session = session
}
func getPosts() async throws -> [PostDTO] {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let (data, response) = try await session.data(from: url)
guard let http = response as? HTTPURLResponse, 200..<300 ~= http.statusCode else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([PostDTO].self, from: data)
}
}
هذا الجزء يمثل مصدر البيانات الفعلي الذي يتعامل معه التطبيق
قد يكون API عبر الإنترنت أو قاعدة بيانات محلية أو ملفاً مخزناً داخل الجهاز أو أي مصدر بيانات آخر
كما تلاحظ في الكود السابق قمنا بإنشاء Data Source مسؤول عن جلب البيانات من الـ API
مهمة الـ Data Source هي التعامل المباشر مع مصدر البيانات فقط
لذلك ستجد بداخله أكواد الـ Networking أو أكواد التعامل مع قواعد البيانات بينما لا يحتوي على أي Business Logic خاص بالتطبيق
لاحظ أيضاً أن الـ Data Source يقوم بإرجاع PostDTO وليس Post
وذلك لأننا ما زلنا داخل طبقة الـ Data، وبالتالي نتعامل مع DTOs
أما تحويل البيانات إلى Entities فسيتم لاحقاً قبل إرسالها إلى طبقة الـ Domain
Repositories
قم بإنشاء ملف Swift بإسم DefaultPostRepository في داخل مجلد Repositories
ثم اكتب هذا الكود
struct DefaultPostRepository: PostRepository {
private let remote: PostRemoteDataSource
init(remote: PostRemoteDataSource) {
self.remote = remote
}
func fetchPost() async throws -> [Post] {
return try await remote.getPosts().map{$0.toDomain()}
}
}
أحياناً يتم تسمية هذا المجلد بـ RepositoryImpl أو RepositoriesImpl للتمييز بين الـ Repository Protocols
الموجودة في طبقة الـ Domain وبين التطبيق الفعلي لها داخل طبقة الـ Data
في هذا الملف نقوم بتنفيذ الـ PostRepository الذي قمنا بتعريفه سابقاً في طبقة الـ Domain
كما تلاحظ قمنا بحقن PostRemoteDataSource داخل الـ Repository
وبالتالي فإن الـ Repository لا يقوم بجلب البيانات بنفسه،بل يعتمد على الـ Data Sources للحصول عليها
بعد الحصول على البيانات من الـ Data Source والتي تكون على شكل PostDTO نقوم بتحويلها إلى Post باستخدام toDomain
قبل إرجاعها إلى طبقة الـ Domain
وبهذه الطريقة تبقى طبقة الـ Domain معزولة تماماً عن الـ DTOs وعن تفاصيل مصادر البيانات
بمعنى آخر الـ Repository يعتبر حلقة الوصل بين طبقة الـ Domain وطبقة الـ Data
طبقة الـ Presentation
قم بإنشاء ملف Swift بإسم PostsViewModel في داخل مجلد Presentation
ملاحظة : لا يوجد أسلوب واحد متفق عليه لتنظيم هذه الطبقة
بعض المطورين يفضلون إنشاء مجلد مستقل للـ Views ومجلد آخر للـ ViewModels
بينما يفضل الكثير من المطورين تنظيم الملفات حسب الصفحة بحيث يتم إنشاء مجلد لكل صفحة داخل التطبيق
على سبيل المثال يمكن إنشاء مجلد باسم Posts يحتوي على:
- PostsView.swift
- PostsViewModel.swift
شخصياً أفضل هذا الأسلوب لأنه يجعل الملفات المتعلقة بنفس الصفحة موجودة في مكان واحد ويسهل التنقل داخل المشروع مع زيادة عدد الصفحات
ومن ثم اكتب كود ملف PostsViewModel
import SwiftUI
@MainActor @Observable
final class PostsViewModel {
enum State: Equatable {
case idle
case loading
case loaded([Post])
case error(String)
}
private(set) var state: State = .idle
private let getPosts: GetPostsUseCase
init(getPosts: GetPostsUseCase) {
self.getPosts = getPosts
}
func onAppear() async {
state = .loading
do {
let posts = try await getPosts.execute()
state = .loaded(posts)
} catch {
state = .error(error.localizedDescription)
}
}
}
في هذا الـ ViewModel قمنا بإنشاء State لمتابعة حالة الصفحة.
عن تجربة هذه من أفضل الطرق لتنظيم حالات الواجهة
لأن الصفحة غالباً تكون في واحدة من هذه الحالات:
idle الحالة الافتراضية قبل تنفيذ أي طلب
loading عند بدء جلب البيانات
loaded عند نجاح الطلب ورجوع البيانات
error عند حدوث خطأ
الفائدة من هذا الأسلوب أنه يجعل حالة الصفحة واضحة ومحددة بدلاً من استخدام أكثر من متغير مثل isLoading و posts و errorMessage، مما قد يسبب تداخل أو حالات غير منطقية
الجزء الأهم هنا هو حقن الـ GetPostsUseCase داخل الـ ViewModel
كما شرحنا سابقاً، الـ ViewModel لا يتعامل مباشرة مع الـ API أو الـ Repository بل يستدعي الـ Use Case فقط
بعد استدعاء execute ينتقل الطلب إلى الـ Use Case ثم إلى الـ Repository ثم إلى الـ Data Source الذي يقوم بتنفيذ الطلب وجلب البيانات
بعد رجوع النتيجة يقوم الـ ViewModel بتحديث الـ state وبناءً على هذه الحالة يتم تحديث الواجهة وعرض البيانات للمستخدم
الـ View
الآن وصلنا إلى آخر جزء في التطبيق وهو الـ View.
إذا تذكر ما شرحته سابقاً فإن الـ View في نمط MVVM يجب أن تكون مسؤولة فقط عن عرض البيانات واستقبال تفاعلات المستخدم
بينما يتم نقل منطق العمل وإدارة البيانات إلى الـ ViewModel
قم بإنشاء ملف Swift باسم PostsView ثم أضف الكود التالي:
import SwiftUI
struct PostsView: View {
@State private var viewModel: PostsViewModel
init(viewModel: PostsViewModel) {
self.viewModel = viewModel
}
var body: some View {
NavigationStack {
content
.navigationTitle("Posts")
.navigationDestination(for: Post.self) { post in
PostsDetailView(post: post)
}
.task { await viewModel.onAppear() }
}
}
@ViewBuilder
private var content: some View {
switch viewModel.state {
case .idle, .loading:
ProgressView()
case .loaded(let posts):
List(posts) { post in
NavigationLink(value: post) {
VStack(alignment: .leading, spacing: 4) {
Text(post.title).font(.headline)
Text(post.body)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
case .error(let message):
ContentUnavailableView("Something went wrong", systemImage: "exclamationmark.triangle", description: Text(message))
}
}
}
struct PostsDetailView: View {
let post: Post
var body: some View {
List {
LabeledContent("ID", value: "\(post.id)")
LabeledContent("User Id", value: String(post.userId))
LabeledContent("Title", value: post.title)
LabeledContent("Body", value: "\(post.body)")
}
.navigationTitle("\(post.id)")
}
}
في هذا المثال تلاحظ أن الـ View تستقبل الـ ViewModel من الخارج عبر الـ init بدلاً من إنشائه داخل الصفحة وهذا يجعل الصفحة مستقلة ويسهل اختبارها وإعادة استخدامها
كما تلاحظ أيضاً أننا لا نقوم بجلب البيانات مباشرة من الـ API أو استدعاء الـ UseCase داخل الـ View بل يتم الاعتماد بالكامل على الـ ViewModel
مجلد الـ App
الآن انتهينا من بناء طبقات الـ Clean Architecture، لكن ما زال هناك جزء مهم لم نقم به وهو ربط هذه الطبقات مع بعضها البعض
إذا لاحظت في الأمثلة السابقة،ستجد أن أغلب الكلاسات تستقبل Dependencies عبر الـ init
على سبيل المثال:
- الـ ViewModel يعتمد على الـ UseCase.
- الـ UseCase يعتمد على الـ Repository.
- الـ Repository يعتمد على الـ DataSource.
بدلاً من إنشاء هذه الـ Dependencies داخل كل كلاس نقوم بتمريرها من الخارج باستخدام مفهوم يسمى Dependency Injection
هذا الأسلوب يجعل الكود أكثر مرونة وأسهل في الاختبار والصيانة كما يسمح باستبدال أي جزء من التطبيق دون التأثير على بقية الطبقات
ولتنظيم عملية إنشاء وربط جميع الـ Dependencies في مكان واحد سنقوم بإنشاء ملف بإسم AppDIContainer
ثم نكتب فيه هذا الكود
import Foundation
final class AppDIContainer {
static let shared = AppDIContainer()
private func makePostRepositories() -> PostRepository {
DefaultPostRepository(remote: DefaultPostRemoteDataSource())
}
private func makeGetPostsUseCase() -> GetPostsUseCase {
DefaultGetPostsUseCase(respository: makePostRepositories())
}
@MainActor
func makePostViewModels() -> PostsViewModel {
PostsViewModel(getPosts: makeGetPostsUseCase())
}
}
ومن ثم في صفحة الـ main تستدعيها هكذت
@main
struct MVVMWithCleanArchitectureApp: App {
var body: some Scene {
WindowGroup {
PostsView(viewModel: AppDIContainer.shared.makePostViewModels())
}
}
}
وبهذا الشكل تصبح جميع الـ Dependencies مدارة من مكان واحد
و تبقى كل طبقة مستقلة عن تفاصيل الطبقات الأخرى
لماذا هذا التقسيم يسهل Unit Testing؟
من أهم فوائد استخدام Clean Architecture مع MVVM و Dependency Injection أن الاختبار يصبح أسهل بكثير
لأن كل طبقة تعتمد على Protocol وليس على تنفيذ مباشر يمكننا في الاختبارات استبدال الـ Repository الحقيقي بـ Mock Repository
بهذه الطريقة نستطيع اختبار الـ UseCase أو الـ ViewModel بدون الحاجة إلى API حقيقي أو Network Request.
لتحربة عمل اختبار
افتح صفحة File ثم اختار New ثم Target ثم اختار Unit Testing Bundle
وقم بتسميته PostsViewModelTests
اختبار الـ Repository
في هذا الاختبار سنقوم بالتأكد من أن الـ Repository يقوم بتحويل الـ DTO إلى Entity بشكل صحيح
import Testing
@testable import MVVMWithCleanArchitecture
struct MockPostRemoteDataSource: PostRemoteDataSource {
func getPosts() async throws -> [PostDTO] {
[
PostDTO(
userId: 1,
id: 10,
title: "DTO Title",
body: "DTO Body"
)
]
}
}
struct PostRepositoryTests {
@Test
func convertsDTOToDomainModel() async throws {
// Given
let repository = DefaultPostRepository(
remote: MockPostRemoteDataSource()
)
// When
let posts = try await repository.fetchPost()
// Then
#expect(posts.count == 1)
#expect(posts.first?.id == 10)
#expect(posts.first?.title == "DTO Title")
}
}
اختبار الـ ViewModel
في هذا الاختبار سنقوم بالتأكد من أن الـ ViewModel يتعامل بشكل صحيح مع حالات النجاح والفشل والحالة الابتدائية
import Testing
@testable import MVVMWithCleanArchitecture
import Foundation
struct MockGetPostsUseCase: GetPostsUseCase {
let result: Result<[Post], Error>
func execute() async throws -> [Post] {
try result.get()
}
}
@MainActor
struct PostsViewModelTests {
@Test
func startsWithIdleState() {
// Given
let sut = PostsViewModel(
getPosts: MockGetPostsUseCase(
result: .success([])
)
)
// Then
#expect(sut.state == .idle)
}
@Test
func showsLoadedState_whenFetchSucceeds() async {
// Given
let posts = [
Post(
userId: 1,
id: 11,
title: "SwiftUI Post",
body: "Test Body"
)
]
let sut = PostsViewModel(
getPosts: MockGetPostsUseCase(
result: .success(posts)
)
)
// When
await sut.onAppear()
// Then
#expect(sut.state == .loaded(posts))
}
@Test
func showsErrorState_whenFetchFails() async {
// Given
let sut = PostsViewModel(
getPosts: MockGetPostsUseCase(
result: .failure(
URLError(.notConnectedToInternet)
)
)
)
// When
await sut.onAppear()
// Then
guard case .error(let message) = sut.state else {
Issue.record(
"Expected .error but got \(sut.state)"
)
return
}
#expect(!message.isEmpty)
}
}
قمنا بإنشاء MockGetPostsUseCase حتى نستطيع التحكم في النتيجة التي يحصل عليها الـ ViewModel دون الحاجة إلى API حقيقي
في الاختبار الأول تأكدنا أن الحالة الابتدائية للـ ViewModel هي idle
في الاختبار الثاني قمنا بإرجاع بيانات ناجحة من الـ Mock ثم تأكدنا أن الـ ViewModel قام بتغيير الحالة إلى loaded
أما في الاختبار الثالث قمنا بمحاكاة حدوث خطأ وتأكدنا أن الـ ViewModel انتقل إلى حالة error
بهذه الطريقة نستطيع اختبار الـ ViewModel بشكل كامل دون الحاجة إلى تشغيل التطبيق أو تنفيذ أي Network Request حقيقي
بهذا نكون قد طبقنا Clean Architecture مع MVVM خطوة بخطوة بدءاً من طبقة Domain ثم Data ثم Presentation وصولاً إلى Dependency Injection و Unit Testing.
قد يبدو عدد الملفات أكبر مقارنة باستخدام MVVM فقط، لكن مع نمو المشروع ستصبح إدارة الكود واختباره وصيانته أسهل بكثير، وهو السبب الذي يجعل العديد من الفرق تعتمد هذا الأسلوب في المشاريع المتوسطة والكبيرة.

