Giới thiệu về Clean Architecture
Đây là mồ hình giúp cấu trúc gíup phân tách các chức năng qua các lớp. Thông qua mô hình này, sẽ hỗ trợ rất nhiều thông qua việc suy nghĩ logic cẩn thận và hiệu quả, nhận ra sự không phù hợp giữa Use Cases và Entities, đặt ra hướng đi trong hệ thống. Nó cũng hướng tới sự độc lập tối đa của bất kì thư viện hay tool nào, để phù hợp cho việc kiểm tra và thay thế chúng.
Áp dụng cùng MVVM
- Domain Layer: Entites + Use Cases + Gatway Protocols
- Data Layer: Gateway Implementations + API(Network) + Database
- Presentation Layer: ViewModels + Views+ Navigator + Scene Use Cases
Hướng xử lý
Đi vào chi tiết
Lớp Domain
Entities
Chứa các Business logic. Là lớp quan trọng nhất, nơi bạn thực hiện giải quyết các vấn đề – mục đích khi xây dựng app. 1 entity có thể là 1 object với các phương thức, hoặc nó là 1 tập hợp của struct và hàm. Điều đó không quan trọng, entities có thể sử dụng bởi nhiều cách khác nhau trong cùng 1 ứng dụng.
Entites có thể đơn giản là dữ liệu structures:
1 2 3 4 5 6 |
<span class="token keyword">struct</span> <span class="token builtin">Product</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> id <span class="token operator">=</span> <span class="token number">0</span> <span class="token keyword">var</span> name <span class="token operator">=</span> <span class="token string">""</span> <span class="token keyword">var</span> price <span class="token operator">=</span> <span class="token number">0.0</span> <span class="token punctuation">}</span> |
Use Cases
Chưa các rule về apploaction-specific. Nó đóng gói và triển khai tất cả các ca sử dụng trong hệ thống. Các ca sử dụng cấu trúc các luồng dữ liệu tới và đi từ entites, và hướng các entities đó tới các Critical Business Rules -> mục đích của các ca sử dụng.
UseCases là những protocol, để làm những việc cụ thể như:
1 2 3 4 5 6 7 8 9 10 |
<span class="token keyword">protocol</span> <span class="token builtin">GettingProductList</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> productGateway<span class="token punctuation">:</span> <span class="token builtin">ProductGatewayType</span> <span class="token punctuation">{</span> <span class="token keyword">get</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">extension</span> <span class="token builtin">GettingProductList</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">getProductList</span><span class="token punctuation">(</span>dto<span class="token punctuation">:</span> <span class="token builtin">GetPageDto</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Observable</span><span class="token operator"><</span><span class="token builtin">PagingInfo</span><span class="token operator"><</span><span class="token builtin">Product</span><span class="token operator">></span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> productGateway<span class="token punctuation">.</span><span class="token function">getProductList</span><span class="token punctuation">(</span>dto<span class="token punctuation">:</span> dto<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Gateway Protocols
Về cơ bản, gateway chỉ là 1 phần trừu tượng mà sẽ hiển những công việc được triển khai phía dưới. Nó có thể là 1 Data Store (pattern Repository), 1 API gateway, v…v. Chẳng hạn với Database gateway sẽ có những phương thức để thực hiện yêu cầu của ứng dụng. Tuy nhiên, đừng cố gắng ẩn hết các quy tắc rule quan trọng qua gateway. Tất cả truy vấn tới database phải tương đối đơn giản như phương pháp CRUD , và tất nhiên 1 số bộ lọc cũng được chấp nhận.
1 2 3 4 5 6 |
protocol ProductGatewayType { func getProductList(dto: GetPageDto) -> Observable<PagingInfo<Product>> func deleteProduct(dto: DeleteProductDto) -> Observable<Void> func update(_ product: ProductDto) -> Observable<Void> } |
Lớp Data
Lớp Data chứa các triển khai Gateway và 1 hoặc nhiều Data Stores. Gateway là phản hồi cho dữ liệu điều phối từ những Data Store. Data Store có thể online hoặc offline (ví dụ presistent database). Lớp Data chỉ phụ thuộc vào lớp Domain.
Gateway Implementations
1 2 3 4 5 6 7 8 9 10 |
<span class="token keyword">struct</span> <span class="token builtin">ProductGateway</span><span class="token punctuation">:</span> <span class="token builtin">ProductGatewayType</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">getProductList</span><span class="token punctuation">(</span>dto<span class="token punctuation">:</span> <span class="token builtin">GetPageDto</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Observable</span><span class="token operator"><</span><span class="token builtin">PagingInfo</span><span class="token operator"><</span><span class="token builtin">Product</span><span class="token operator">></span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token constant">API</span><span class="token punctuation">.</span>shared<span class="token punctuation">.</span><span class="token function">getProductList</span><span class="token punctuation">(</span><span class="token constant">API</span><span class="token punctuation">.</span><span class="token function">GetProductListInput</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token builtin">map</span> <span class="token punctuation">{</span> <span class="token function">PagingInfo</span><span class="token punctuation">(</span>page<span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">,</span> items<span class="token punctuation">:</span> $<span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">func</span> <span class="token function">deleteProduct</span><span class="token punctuation">(</span>dto<span class="token punctuation">:</span> <span class="token builtin">DeleteProductDto</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Observable</span><span class="token operator"><</span><span class="token builtin">Void</span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token keyword">func</span> <span class="token function">update</span><span class="token punctuation">(</span><span class="token number">_</span> product<span class="token punctuation">:</span> <span class="token builtin">ProductDto</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Observable</span><span class="token operator"><</span><span class="token builtin">Void</span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
UserDefaults
1 2 3 4 5 |
<span class="token keyword">enum</span> <span class="token builtin">AppSettings</span> <span class="token punctuation">{</span> @<span class="token function">Storage</span><span class="token punctuation">(</span>key<span class="token punctuation">:</span> <span class="token string">"didInit"</span><span class="token punctuation">,</span> defaultValue<span class="token punctuation">:</span> <span class="token boolean">false</span><span class="token punctuation">)</span> <span class="token keyword">static</span> <span class="token keyword">var</span> didInit<span class="token punctuation">:</span> <span class="token builtin">Bool</span> <span class="token punctuation">}</span> |
APIs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<span class="token keyword">extension</span> <span class="token constant">API</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">getRepoList</span><span class="token punctuation">(</span><span class="token number">_</span> input<span class="token punctuation">:</span> <span class="token builtin">GetRepoListInput</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Observable</span><span class="token operator"><</span><span class="token builtin">GetRepoListOutput</span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">request</span><span class="token punctuation">(</span>input<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment">// MARK: - GetRepoList</span> <span class="token keyword">extension</span> <span class="token constant">API</span> <span class="token punctuation">{</span> <span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">GetRepoListInput</span><span class="token punctuation">:</span> <span class="token builtin">APIInput</span> <span class="token punctuation">{</span> <span class="token keyword">init</span><span class="token punctuation">(</span>page<span class="token punctuation">:</span> <span class="token builtin">Int</span><span class="token punctuation">,</span> perPage<span class="token punctuation">:</span> <span class="token builtin">Int</span> <span class="token operator">=</span> <span class="token number">10</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> params<span class="token punctuation">:</span> <span class="token builtin">JSONDictionary</span> <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token string">"q"</span><span class="token punctuation">:</span> <span class="token string">"language:swift"</span><span class="token punctuation">,</span> <span class="token string">"per_page"</span><span class="token punctuation">:</span> perPage<span class="token punctuation">,</span> <span class="token string">"page"</span><span class="token punctuation">:</span> page <span class="token punctuation">]</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token keyword">init</span><span class="token punctuation">(</span>urlString<span class="token punctuation">:</span> <span class="token constant">API</span><span class="token punctuation">.</span><span class="token builtin">Urls</span><span class="token punctuation">.</span>getRepoList<span class="token punctuation">,</span> parameters<span class="token punctuation">:</span> params<span class="token punctuation">,</span> requestType<span class="token punctuation">:</span> <span class="token punctuation">.</span><span class="token keyword">get</span><span class="token punctuation">,</span> requireAccessToken<span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">GetRepoListOutput</span><span class="token punctuation">:</span> <span class="token builtin">APIOutput</span> <span class="token punctuation">{</span> <span class="token keyword">private</span><span class="token punctuation">(</span><span class="token keyword">set</span><span class="token punctuation">)</span> <span class="token keyword">var</span> repos<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token builtin">Repo</span><span class="token punctuation">]</span><span class="token operator">?</span> <span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function">mapping</span><span class="token punctuation">(</span><span class="token builtin">map</span><span class="token punctuation">:</span> <span class="token builtin">Map</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">mapping</span><span class="token punctuation">(</span><span class="token builtin">map</span><span class="token punctuation">:</span> <span class="token builtin">map</span><span class="token punctuation">)</span> repos <span class="token operator"><</span><span class="token operator">-</span> <span class="token builtin">map</span><span class="token punctuation">[</span><span class="token string">"items"</span><span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Map JSON Data tới Domain Entities sử dụng ObjectMapper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<span class="token keyword">import</span> <span class="token builtin">ObjectMapper</span> <span class="token keyword">extension</span> <span class="token builtin">Product</span><span class="token punctuation">:</span> <span class="token builtin">Mappable</span> <span class="token punctuation">{</span> <span class="token keyword">init</span><span class="token operator">?</span><span class="token punctuation">(</span><span class="token builtin">map</span><span class="token punctuation">:</span> <span class="token builtin">Map</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">self</span><span class="token punctuation">.</span><span class="token keyword">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">mutating</span> <span class="token keyword">func</span> <span class="token function">mapping</span><span class="token punctuation">(</span><span class="token builtin">map</span><span class="token punctuation">:</span> <span class="token builtin">Map</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> id <span class="token operator"><</span><span class="token operator">-</span> <span class="token builtin">map</span><span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span> name <span class="token operator"><</span><span class="token operator">-</span> <span class="token builtin">map</span><span class="token punctuation">[</span><span class="token string">"name"</span><span class="token punctuation">]</span> price <span class="token operator"><</span><span class="token operator">-</span> <span class="token builtin">map</span><span class="token punctuation">[</span><span class="token string">"price"</span><span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
CoreData Repositories
Map CoreData Entities tới Domain Entitites và ngược lại:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import MagicalRecord protocol UserRepository: CoreDataRepository { } extension UserRepository where Self.ModelType == User, Self.EntityType == CDUser { func getUsers() -> Observable<[User]> { return all() } func add(dto: AddUserDto) -> Observable<Void> { guard let users = dto.users else { return Observable.empty() } return addAll(users) } static func map(from item: User, to entity: CDUser) { entity.id = item.id entity.name = item.name entity.gender = Int64(item.gender.rawValue) entity.birthday = item.birthday } static func item(from entity: CDUser) -> User? { guard let id = entity.id else { return nil } return User( id: id, name: entity.name ?? "", gender: Gender(rawValue: Int(entity.gender)) ?? Gender.unknown, birthday: entity.birthday ?? Date() ) } } |
Vậy là chúng ta đã kết thúc phần 1, về kết hợp mô hình MVVM và cấu trúc Clean Architectures. Hẹn gặp lại vào phần 2.
Nguồn bài gốc anh Tuấn