記錄開(kāi)發(fā)認(rèn)知的一步步。
第一步,開(kāi)始接觸開(kāi)發(fā)也沒(méi)什么模式,就是 model view control 散落一地,模塊分下文件夾,當(dāng)時(shí)大部分項(xiàng)目起步都是這樣。
第二步來(lái)到這邊接手項(xiàng)目,因?yàn)榻唤拥娜耸亲鲞^(guò)7年的后臺(tái),所以對(duì)代碼的分層很看重。然后我也就跟著習(xí)慣了 API-service-control model view 的分層方式,感覺(jué)好爽,這種做小規(guī)模的APP還是很可行的。
第三步,看到大神寫(xiě)的文章都在強(qiáng)調(diào)代碼之間的耦合性,以及各種組件化,就想嘗試下。自己實(shí)現(xiàn)了一套 function-business-main 模式,也就是底層function是實(shí)現(xiàn)各個(gè)功能的工具層,業(yè)務(wù)層business調(diào)用function層各種工具實(shí)現(xiàn)具體業(yè)務(wù),最后由 main 層組合各個(gè)業(yè)務(wù)最終實(shí)現(xiàn)完整的應(yīng)用,各個(gè)模塊之間的通信通過(guò)一個(gè)中間者去調(diào)用,配合OC 的runtime特性通信很easy。中間也嘗試過(guò)pod庫(kù)的方式實(shí)現(xiàn)組件化,無(wú)奈一個(gè)人玩太麻煩,沒(méi)堅(jiān)持下來(lái)放棄了。
casa 大神博客地址
現(xiàn)在的項(xiàng)目結(jié)構(gòu)

BaseKit 系統(tǒng)的基礎(chǔ)工具庫(kù)(基礎(chǔ)數(shù)據(jù)提供)
Function 提供各種功能性工具,比如分享、推送、各種常用擴(kuò)展等等,

Business 組合各種功能性工具實(shí)現(xiàn)各個(gè)業(yè)務(wù)模塊 具體的模塊實(shí)現(xiàn)使用MVC 還是 MVVM 對(duì)整體沒(méi)有影響。

Main 就是組合各個(gè)業(yè)務(wù)模塊,為應(yīng)用提供一個(gè)完整的入口

具體開(kāi)發(fā)實(shí)現(xiàn)
1.確定一些原則
1.1 去除項(xiàng)目對(duì)基類的依賴,所有的功能可以通過(guò) Category 的方式進(jìn)行實(shí)現(xiàn) , 例如實(shí)現(xiàn)指示器的功能,在需要的地方引入 category 即可。
//
// UIViewController+YMHUD.h
// YYDApp
//
// Created by YM on 16/8/1.
// Copyright ? 2016年 YM. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface UIViewController (YMHUD)
- (void)showHUD;
- (void)showHUDInView:(UIView *)view;
- (void)hideHUD;
- (void)hideHUDForView:(UIView *)view;
- (void)showErrMsg:(NSString *)errmsg;
- (void)showFastToastHUD:(NSString *)msg;
- (void)showFastToastHUD:(NSString *)msg complite:(void(^)())complite;
@end
使用
#import "UIViewController+YMHUD.h"
- (void)appScanHandlerStart:(TJAPPScanHandler *)handler{
[self showHUDInView:self.discernCodeView];
}
1.2 確定代碼編寫(xiě)結(jié)構(gòu) 例如Controller
@interface MSatisticVC ()<APIManagerCallBackDelegate>
@property (nonatomic,strong) MSatisticViewModel *vm;
@end
@implementation MSatisticVC
- (void)viewDidLoad {
[super viewDidLoad];
[self prepareForUI];
}
#pragma mark - Event
- (void)rightBtnClick:(id)sender{}
#pragma mark - Delegate
- (void)apiManagerCallBackDidFailed:(BaseAPIManager *)manager{}
- (void)apiManagerCallBackDidSuccess:(BaseAPIManager *)manager{}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {}
#pragma mark - Private Method
- (void)prepareForUI{}
#pragma mark - Publich Method
#pragma mark - Getter and Setter
- (MSatisticViewModel *)vm{
if (!_vm) {
_vm = [[MSatisticViewModel alloc] init];
}
return _vm;
}
@end
1.3 使用 cocoapods 進(jìn)行庫(kù)管理,在進(jìn)行 OC swift 混編到時(shí)候發(fā)現(xiàn)一個(gè)小技巧,使用 pod 管理 OC 的庫(kù),使用 Carthage 管理 swift 的庫(kù),這樣省了很多事,對(duì)于我一個(gè)人搞的項(xiàng)目來(lái)說(shuō)還是很方便的。
1.4 布局使用 Storyboard + Masonry , 這個(gè)看個(gè)人喜好
1.5 確定模塊化、組件化開(kāi)發(fā)。主要針對(duì)業(yè)務(wù)層模塊規(guī)定,禁止同層直接調(diào)用,杜絕耦合其他業(yè)務(wù)模塊代碼,解決方案為調(diào)用調(diào)度者模塊(思路介紹來(lái)自 casa 大神系列教程),A 業(yè)務(wù)模塊編寫(xiě)一個(gè)向外部暴露的方法供其他模塊的調(diào)用,B 業(yè)務(wù)模塊就可以通過(guò)調(diào)度者去調(diào)用 A 暴露的方法。(也有一種方案是蘑菇街通過(guò)路由的方式實(shí)現(xiàn)的組件件調(diào)用,受大神影響,覺(jué)得使用 Mediator 的方式更適合我現(xiàn)在的應(yīng)用。)具體實(shí)現(xiàn):
訂單模塊向外暴露方法
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface TargetOrder : NSObject
/**
* 獲取訂單支付頁(yè)面
*
* @param orderID 訂單ID
*
* @return 訂單支付頁(yè)面
*/
- (UIViewController *)ActionFetchStidumOrderPayVC:(NSDictionary *)orderDic;
調(diào)度者模塊編寫(xiě) category,通過(guò)運(yùn)行時(shí)調(diào)用訂單模塊的方法
@implementation YMMediator (OrderActions)
- (UIViewController *)MediatorViewControllerForStidumOrderPayVC:(NSString *)orderID{
NSAssert(orderID != nil, @"請(qǐng)檢查訂單號(hào)");
UIViewController *vc = [self performTarget:kMediatorTargetOrder
action:ActionFetchStidumOrderPayVC
params:@{@"orderID":orderID}];
if ([vc isKindOfClass:[UIViewController class]]) {
return vc;
}else{
//異常
return [[UIViewController alloc] init];
}
}
-------分割--------------
YMMediator 調(diào)度者的關(guān)鍵代碼
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params
{
NSString *targetClassString = [NSString stringWithFormat:@"Target%@", targetName];
NSString *actionString = [NSString stringWithFormat:@"Action%@:", actionName];
Class targetClass = NSClassFromString(targetClassString);
id target = [[targetClass alloc] init];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 這里是處理無(wú)響應(yīng)請(qǐng)求的地方之一,這個(gè)demo做得比較簡(jiǎn)單,如果沒(méi)有可以響應(yīng)的target,就直接return了。實(shí)際開(kāi)發(fā)過(guò)程中是可以事先給一個(gè)固定的target專門(mén)用于在這個(gè)時(shí)候頂上,然后處理這種請(qǐng)求的
return nil;
}
if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
} else {
// 這里是處理無(wú)響應(yīng)請(qǐng)求的地方,如果無(wú)響應(yīng),則嘗試調(diào)用對(duì)應(yīng)target的notFound方法統(tǒng)一處理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
} else {
// 這里也是處理無(wú)響應(yīng)請(qǐng)求的地方,在notFound都沒(méi)有的時(shí)候,這個(gè)demo是直接return了。實(shí)際開(kāi)發(fā)過(guò)程中,可以用前面提到的固定的target頂上的。
return nil;
}
}
}
其他模塊調(diào)用 支付模塊的 支付界面
- (void)goPayVC:(NSString *)orderID{
if (orderID == nil || orderID.length <1) {
[self showFastToastHUD:@"訂單號(hào)不能為空"];
return;
}
UIViewController *vc = [[YMMediator sharedInstance] MediatorViewControllerForStidumOrderPayVC:orderID];
[self.navigationController pushViewController:vc animated:YES];
}
2.網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求方式
2.1 主要是為了對(duì)網(wǎng)絡(luò)請(qǐng)求的過(guò)程提供盡可能多的監(jiān)控感知,通過(guò)切片設(shè)計(jì)對(duì)網(wǎng)絡(luò)請(qǐng)求過(guò)程添加想要的處理函數(shù),可以通過(guò)代理的方式實(shí)現(xiàn),也可以通過(guò) block 方式實(shí)現(xiàn)。封裝屬于自己的一套網(wǎng)絡(luò)請(qǐng)求,切勿過(guò)于依賴某一種網(wǎng)絡(luò)庫(kù)
/************************************************
APIManagerCallBackDelegate
回調(diào)協(xié)議
************************************************/
@protocol APIManagerCallBackDelegate <NSObject>
@required
- (void)apiManagerCallBackDidSuccess:(BaseAPIManager *)manager;
- (void)apiManagerCallBackDidFailed:(BaseAPIManager *)manager;
@end
/************************************************
APIManagerCallBackDateReformer
DateReformer協(xié)議
************************************************/
@protocol APIManagerCallBackDateReformer <NSObject>
@required
- (id)manager:(BaseAPIManager *)manager reformData:(NSObject *)data;
@end
/************************************************
APIManagerParamSourceDelegate
參數(shù)源
************************************************/
@protocol APIManagerParamSourceDelegate <NSObject>
@required
- (NSDictionary *)paramsForAPi:(BaseAPIManager *)manager;
- (NSDictionary *)headerFieldsForAPi:(BaseAPIManager *)manager;
@end
/************************************************
APIManagerValidator
驗(yàn)證協(xié)議
************************************************/
@protocol APIManagerValidator <NSObject>
@required
- (BOOL)manager:(BaseAPIManager *)manager isCorrectWithCallBackData:(id)data;
- (BOOL)manager:(BaseAPIManager *)manager isCorrectWithParamsData:(NSDictionary *)data;
@end
/************************************************
APIManagerInterceptor
攔截器協(xié)議
************************************************/
@protocol APIManagerInterceptor <NSObject>
@optional
- (void)manager:(BaseAPIManager *)manager beforePerformSuccessWithResponse:(APIURLResponse *)response;
- (void)manager:(BaseAPIManager *)manager afterPerformSuccessWithResponse:(APIURLResponse *)response;
- (void)manager:(BaseAPIManager *)manager beforePerformFailWithResponse:(APIURLResponse *)response;
- (void)manager:(BaseAPIManager *)manager afterPerformFailWithResponse:(APIURLResponse *)response;
- (BOOL)manager:(BaseAPIManager *)manager shouldCallAPIWithParams:(NSDictionary *)params;
- (void)manager:(BaseAPIManager *)manager afterCallingAPIWithParams:(NSDictionary *)params;
@end
3.引入單元測(cè)試
3.1 觀看過(guò)李智維的自動(dòng)化測(cè)試直播后,學(xué)習(xí)了一下單元測(cè)試接入,通過(guò)查找資料也找到了他簡(jiǎn)單封裝的測(cè)試工具宏,非常好用。
//
// YYDBaseTest.h
// YaoYD
//
// Created by TJ on 2016/11/28.
// Copyright ? 2016年 oneyd.me. All rights reserved.
//
#import <XCTest/XCTest.h>
#define assertTrue(expr) XCTAssertTrue((expr), @"")
#define assertFalse(expr) XCTAssertFalse((expr), @"")
#define assertNil(a1) XCTAssertNil((a1), @"")
#define assertNotNil(a1) XCTAssertNotNil((a1), @"")
#define assertEqual(a1, a2) XCTAssertEqual((a1), (a2), @"")
#define assertEqualObjects(a1, a2) XCTAssertEqualObjects((a1), (a2), @"")
#define assertNotEqual(a1, a2) XCTAssertNotEqual((a1), (a2), @"")
#define assertNotEqualObjects(a1, a2) XCTAssertNotEqualObjects((a1), (a2), @"")
#define assertAccuracy(a1, a2, acc) XCTAssertEqualWithAccuracy((a1),(a2),(acc))
#define WAIT \
do { \
[self expectationForNotification:@"LCUnitTest" object:nil handler:nil]; \
[self waitForExpectationsWithTimeout:60 handler:nil]; \
} while(0);
#define NOTIFY \
do { \
[[NSNotificationCenter defaultCenter] postNotificationName:@"LCUnitTest" object:nil]; \
} while(0);
@interface YYDBaseTest : XCTestCase
- (void)loginBeforSetup;
@end
-----------
#import "UserServiceDTO.h"
@implementation YYDBaseTest
- (void)loginBeforSetup{
NSDictionary *parm = @{@"username" : @"platform",
@"password" : @"tianjian.123",
@"grant_type": @"password"};
[[UserServiceDTO shareInstance] loginWithParams:parm completeBlock:^(BOOL isSuccess , NSString *errmsg) {
assertTrue(isSuccess);
NOTIFY
}];
WAIT
}
@end
使用
@interface AssetsAPITests : YYDBaseTest
@end
@implementation AssetsAPITests
- (void)setUp {
[super setUp];
// [self loginBeforSetup];
}
- (void)testPingAlipay{
NSString *orderInfo = @"snzitUjE%2FoURBVLXluOVheOmv8R%2Fjt4cB6neqXDONLArHtrQ%3D";
NSDictionary *charge = @{@"credential":@{
@"alipay":@{
@"orderInfo":orderInfo,
}
}
};
[Pingpp createPayment:charge appURLScheme:@"TJUrl" withCompletion:^(NSString *result, PingppError *error) {
NOTIFY
}];
WAIT
}
- (void)testRequest{
NSDictionary *params = @{
@"outTradeNo":@"222333",
@"refundNo":@"088788",
@"refundTotalAmount":@"1",
@"actualAmount":@"1",
@"reason":@"訂單退款",
@"notifyUrl":@"https://www.baidu.com",
@"tradeType":@"APP"
};
NSMutableURLRequest *request = [[AFJSONRequestSerializer serializer] requestWithMethod:@"POST" URLString:urlString parameters:params error:nil];
[[TJNetService shareInstance] baseRequest:request success:^(id result) {
NOTIFY
} failure:^(id result) {
NOTIFY
}];
WAIT
}
以上是這一段時(shí)間對(duì)項(xiàng)目架構(gòu)的思考。
最近看了一個(gè)視頻講移動(dòng)應(yīng)用架構(gòu)主要就兩個(gè)部分,數(shù)據(jù)+UI,覺(jué)得也是挺有道理的,待慢慢沉淀多參悟這種簡(jiǎn)單的事情吧。