关于C#:构建iOS网络应用程序的最佳体系结构方法(REST客户机)

Best architectural approaches for building iOS networking applications (REST clients)

我是一个有经验的iOS开发者,这个问题对我来说非常有趣。关于这个话题,我看到了很多不同的资源和材料,但是我还是很困惑。iOS网络化应用程序的最佳体系结构是什么?我指的是基本的抽象框架、模式,它将适合每个网络应用程序,无论是一个只有少量服务器请求的小应用程序,还是一个复杂的REST客户机。苹果建议使用MVC作为所有iOS应用程序的基本体系结构方法,但MVC和更现代的MVVM模式都不能解释在何处放置网络逻辑代码以及如何对其进行总体组织。

我是否需要为Service开发类似MVCS(S)的东西,并且在这个Service层中放置所有API请求和其他网络逻辑,从这个角度来看,这些逻辑可能非常复杂?在做了一些研究之后,我发现了两种基本的方法。这里建议为Web服务的每个网络请求创建一个单独的类API(如LoginRequest类或PostCommentRequest类等),所有这些请求都继承自基本请求抽象类AbstractBaseRequest,此外还创建一些全局网络管理器,它封装了通用的网络代码和其他预取。NCES(如果我们有复杂的对象映射和持久性,或者甚至有一个自己的网络通信实现与标准API,那么它可能是AFNetworking定制或RestKit调优)。但这种方法对我来说似乎是一种开销。另一种方法是使用与第一种方法相同的一些单实例APIDispatcher或Manager类,但不是为每个请求创建类,而是将每个请求封装为该Manager类的实例公共方法,例如:fetchContactsloginUser方法等。那么,什么是最好和正确的方法?我还不知道还有其他有趣的方法吗?

我应该为所有这些网络化的东西创建另一个层,比如ServiceNetworkProvider层或者我的MVC架构之上的其他层,还是应该将这个层集成(注入)到现有的MVC层中,比如Model

我知道有很多漂亮的方法,或者像Facebook客户端或LinkedIn客户端这样的移动怪兽是如何应对网络逻辑指数级增长的复杂性的?

我知道这个问题没有确切而正式的答案。这个问题的目标是从经验丰富的iOS开发人员那里收集最有趣的方法。最好的建议方法将被标记为接受并授予声誉奖励,其他方法将被否决。这主要是一个理论和研究问题。我想了解iOS中网络应用程序的基本、抽象和正确的体系结构方法。我希望有经验的开发人员能给出详细的解释。


I want to understand basic, abstract and correct architectural approach for networking applications in iOS:没有"最佳"或"最正确"的方法来构建应用程序体系结构。这是一项非常有创造性的工作。您应该总是选择最直接和可扩展的体系结构,这对于任何开始为您的项目工作的开发人员或团队中的其他开发人员都是很清楚的,但是我同意,体系结构可以是"好的"和"坏的"。

你说:collect the most interesting approaches from experienced iOS developers我认为我的方法不是最有趣或最正确的,但我已经在几个项目中使用过,并对它感到满意。这是你上面提到的方法的混合方法,也是我自己研究工作的改进。我对构建方法的问题很感兴趣,它结合了几个著名的模式和习惯用法。我认为很多Fowler的企业模式可以成功地应用到移动应用程序中。下面是一个最有趣的列表,我们可以申请创建一个iOS应用架构(在我看来):服务层、工作单元、远程外观、数据传输对象、网关、层父类型、特殊情况、域模型。你应该总是正确地设计一个模型层,并且永远不要忘记持久性(它可以显著地提高你的应用程序的性能)。你可以用Core Data来做这个。但您不应该忘记,Core Data不是ORM或数据库,而是一个对象图形管理器,其中持久性是一个很好的选择。因此,对于您的需求,Core Data通常太重,您可以查看新的解决方案,如领域和couchbase-lite,或者基于原始sqlite或leveldb构建自己的轻量级对象映射/持久性层。另外,我建议您熟悉领域驱动的设计和CQR。

首先,我认为,我们应该为网络创建另一个层,因为我们不需要胖的控制器或沉重的,不知所措的模型。我不相信那些东西。但我确实相信江户一〔6〕的方法,因为从来没有一个班是胖的。所有网络一般都可以抽象为业务逻辑,因此我们应该有另一个层,我们可以把它放在那里。我们需要的是服务层:好的。

1
2
It encapsulates the application's business logic,  controlling transactions
and coordinating responses in the implementation of its operations.

在我们的MVC领域中,Service Layer类似于域模型和控制器之间的中介。这种方法有一种类似的变化,叫做MVCS,其中一个Store实际上是我们的Service层。Store提供模型实例并处理网络、缓存等。我想说的是,您不应该在服务层中编写所有的网络和业务逻辑。这也可以被认为是一个糟糕的设计。有关更多信息,请查看贫血和丰富的领域模型。一些服务方法和业务逻辑可以在模型中处理,因此它将是一个"富"(带有行为)模型。

我经常使用两个库:AFNetworking 2.0和ReactiveCoCoA。我认为对于任何与网络和Web服务交互或包含复杂的UI逻辑的现代应用程序来说,这是必须具备的。

建筑学

首先,我创建了一个通用的APIClient类,它是afhttpsessionmanager的一个子类。这是应用程序中所有网络的一个工作平台:所有服务类都将实际的REST请求委托给它。它包含了我在特定应用程序中需要的HTTP客户端的所有定制:ssl固定、错误处理和创建简单的NSError对象,以及所有API和连接错误的详细故障原因和描述(在这种情况下,控制器将能够为用户显示正确的消息),seting请求和响应序列化程序、HTTP头和其他与网络相关的内容。然后,根据所实现的业务逻辑,我逻辑地将所有API请求划分为子服务或更准确地说,是微服务:UserSerivcesCommonServicesSecurityServicesFriendsServices等等。这些微服务中的每一个都是单独的类。它们一起形成一个Service Layer。这些类包含每个API请求、处理域模型的方法,并始终向调用者返回带有解析响应模型或NSErrorRACSignal

我想说的是,如果您有复杂的模型序列化逻辑,那么就为它创建另一个层:类似于数据映射器,但更一般,例如json/xml->model mapper。如果您有缓存:那么也将其创建为单独的层/服务(不应该将业务逻辑与缓存混合在一起)。为什么?因为正确的缓存层可能非常复杂,有自己的gotchas。人们实现复杂的逻辑来获得有效的、可预测的缓存,例如基于profuncors的带有投影的单精度缓存。你可以阅读这个叫做卡洛斯的美丽图书馆来了解更多。别忘了,核心数据确实可以帮助您解决所有缓存问题,并且允许您编写更少的逻辑。另外,如果您在NSManagedObjectContext和服务器请求模型之间有一些逻辑,那么您可以使用存储库模式,它将检索数据的逻辑与作用于模型的业务逻辑分开,并将其映射到实体模型。所以,我建议即使您有一个基于核心数据的体系结构,也要使用存储库模式。存储库可以将诸如NSFetchRequestNSEntityDescriptionNSPredicate等内容抽象为简单的方法,如getput

在服务层中完成所有这些操作之后,调用者(视图控制器)可以借助于ReactiveCocoa原语,对响应进行一些复杂的异步处理:信号处理、链接、映射等,或者只订阅它并在视图中显示结果。我在所有这些服务类中注入依赖注入,我的APIClient将把一个特定的服务调用转换为对应的getPOSTputDELETE等请求到其余端点。在这种情况下,APIClient是隐式传递给所有控制器的,您可以使用参数化的APIClient服务类来显式传递。如果你想对特定的服务类使用不同的APIClient定制,这是有意义的,但是如果你出于某些原因不需要额外的副本,或者你确信你总是会使用APIClient的一个特定实例(没有定制),那么就把它设为一个单独的实例,但是不要,请不要设置服务类。作为单身汉。

然后,每个视图控制器再次使用DI注入它需要的服务类,调用适当的服务方法,并用UI逻辑组合它们的结果。对于依赖注入,我喜欢使用血魔或者更强大的框架台风。我从来没有用过单件的,上帝的埃多克斯(31)类或其他错误的东西。因为如果您调用类WhateverManager,这表明您不知道它的用途,而且它是一个糟糕的设计选择。单子也是一种反模式,在大多数情况下(除了稀有的)是一个错误的解决方案。只有满足以下三个标准时,才应考虑单件:好的。

  • 无法合理分配单个实例的所有权;
  • 需要延迟初始化;
  • 没有为提供全局访问。
  • 在我们的情况下,单一实例的所有权不是问题,而且在我们将上帝管理器划分为服务之后,我们不需要全局访问,因为现在只有一个或多个专用控制器需要特定的服务(例如,UserProfile控制器需要UserServices等)。

    我们应该始终尊重S原则,并使用分离的关注点,所以不要把所有的服务方法和网络调用放在一个类中,因为这是疯狂的,特别是如果您开发了一个大型企业应用程序。这就是为什么我们应该考虑依赖注入和服务方法。我认为这种方法是现代和后OO。在本例中,我们将应用程序分为两部分:控制逻辑(控制器和事件)和参数。好的。

    One kind of parameters would be ordinary"data" parameters. That’s what we pass around functions, manipulate, modify, persist, etc. These are entities, aggregates, collections, case classes. The other kind would be"service" parameters. These are classes which encapsulate business logic, allow communicating with external systems, provide data access.

    Ok.

    下面是一个我的体系结构的一般工作流。假设我们有一个FriendsViewController,它显示用户的朋友列表,我们有一个从朋友中删除的选项。我在我的FriendsServices类中创建了一个方法,名为:好的。

    1
    - (RACSignal *)removeFriend:(Friend * const)friend

    其中,Friend是模型/域对象(或者,如果它们具有相似的属性,它可以只是User对象)。该方法将json参数friend_idnamesurnamefriend_request_id等的Friend解析为NSDictionary。我总是将mantle库用于这种样板文件和我的模型层(前后解析、在JSON中管理嵌套的对象层次结构等)。解析后,调用APIClientDELETE方法发出实际的REST请求,并将RACSignal中的Response返回给调用者(在我们的情况下是FriendsViewController以显示适当的消息给用户或其他人。

    如果我们的应用程序是一个非常大的应用程序,那么我们必须更清楚地分离我们的逻辑。例如,将Repository或模型逻辑与Service混合并不总是好的。当我描述我的方法时,我曾说过,removeFriend方法应该在Service层,但如果我们更加学究,我们可以注意到,它更好地属于Repository层。让我们记住什么是存储库。埃里克·埃文斯在他的书[ddd]中对它作了精确的描述:好的。

    A Repository represents all objects of a certain type as a conceptual set. It acts like a collection, except with more elaborate querying capability.

    Ok.

    因此,Repository本质上是一个门面,它使用收集风格的语义(添加、更新、删除)来提供对数据/对象的访问。这就是为什么当你有这样的东西时:getFriendsListgetUserGroupsremoveFriend,你可以把它放在Repository中,因为这里像集合一样的语义非常清楚。代码如下:好的。

    1
    - (RACSignal *)approveFriendRequest:(FriendRequest * const)request;

    当然是一种业务逻辑,因为它超出了基本的CRUD操作,并且连接了两个域对象(FriendRequest,所以它应该放在Service层。我还想注意:不要创建不必要的抽象。明智地使用所有这些方法。因为如果你将用抽象压倒你的应用程序,这将增加它的偶然复杂性,并且复杂性在软件系统中比任何其他问题都引起更多的问题。

    我给你描述了一个"老"的客观C示例,但是这种方法可以非常容易地适应快速语言,并且有更多的改进,因为它有更多有用的特性和功能性。我强烈推荐使用这个图书馆:莫亚。它允许您创建一个更优雅的APIClient层(如您所记得,我们的Workhorse)。现在,我们的APIClient提供者将是一个值类型(enum),具有符合协议的扩展,并利用破坏模式匹配。Swift Enums+模式匹配允许我们像经典函数编程那样创建代数数据类型。我们的微服务将使用这种改进的APIClient提供商,就像通常的Objective-C方法一样。对于模型层而不是Mantle,您可以使用Objectmapper库,或者我喜欢使用更优雅、功能更强大的argo库。

    所以,我描述了我的一般体系结构方法,我认为它可以适用于任何应用程序。当然,还有很多改进。我建议你学习函数式编程,因为你可以从中受益良多,但也不要做得太过分。一般来说,消除过度的、共享的、全局可变的状态、创建不变的域模型或创建没有外部副作用的纯函数是一种良好的实践,新的Swift语言鼓励这种做法。但是请记住,用大量纯函数模式、类别理论方法重载代码是一个坏主意,因为其他开发人员会阅读和支持您的代码,他们可能会对prismatic profunctors和您的不可变模型中的此类东西感到沮丧或害怕。与ReactiveCocoa相同的是:不要太多地使用RACify代码,因为它会变得很快无法读取,特别是对于新手。当它能够真正简化您的目标和逻辑时,就可以使用它。

    所以,read a lot, mix, experiment, and try to pick up the best from different architectural approaches。这是我能给你的最好建议。好的。好啊。


    根据这个问题的目标,我想描述一下我们的体系结构方法。好的。架构方法

    我们的一般iOS应用程序体系结构支持以下模式:服务层、MVVM、UI数据绑定、依赖项注入和功能性反应式编程范式。好的。

    我们可以将典型的面向消费者的应用程序划分为以下逻辑层:好的。

    • 装配
    • 模型
    • 服务
    • 保管部
    • 管理者
    • 协调员
    • 用户界面
    • 基础设施

    汇编层是我们应用程序的一个引导点。它包含依赖项注入容器和应用程序对象及其依赖项的声明。该层还可能包含应用程序的配置(URL、第三方服务密钥等)。为此,我们使用台风图书馆。好的。

    模型层包含域模型类、验证和映射。我们使用mantle库来映射我们的模型:它支持序列化/反序列化为JSON格式和NSManagedObject模型。对于模型的验证和表单表示,我们使用fxforms和fxmodelvalidation库。好的。

    服务层声明我们用于与外部系统交互的服务,以便发送或接收在我们的域模型中表示的数据。因此,通常我们有与服务器API(每个实体)通信的服务、消息传递服务(如pubnub)、存储服务(如amazon s3)等。基本上,服务包装sdk提供的对象(如pubnub sdk)或实现它们自己的通信逻辑。对于一般网络,我们使用AFNetworking库。好的。

    存储层的目的是组织设备上的本地数据存储。我们为此使用核心数据或领域(两者都有优缺点,决定使用什么是基于具体的规范)。对于核心数据设置,我们使用MDMCoreData库和一组类-存储(类似于服务),它们为每个实体提供对本地存储的访问。对于领域,我们只使用类似的存储来访问本地存储。好的。

    经理层是我们的抽象/包装器所在的地方。好的。

    在经理角色中可以是:好的。

    • 具有不同实现的凭据管理器(keychain、nsdefaults…)
    • 知道如何保留和提供当前用户会话的当前会话管理器
    • 提供访问媒体设备(视频录制、音频、拍照)的捕获管道
    • BLE管理器,提供对蓝牙服务和外围设备的访问
    • 地理位置经理

    所以,在管理器角色中,可以是实现应用程序工作所需特定方面或关注点逻辑的任何对象。好的。

    我们尽量避免单身,但这一层是他们生活的地方,如果需要的话。好的。

    Coordinator层提供依赖于其他层(服务、存储、模型)对象的对象,以便将它们的逻辑组合成特定模块(功能、屏幕、用户故事或用户体验)所需的一系列工作。它通常链接异步操作,并知道如何对它们的成功和失败案例做出反应。例如,您可以想象一个消息传递特性和对应的MessagingCoordinator对象。处理发送消息操作可能如下所示:好的。

  • 验证消息(模型层)
  • 本地保存消息(消息存储)
  • 上传邮件附件(亚马逊S3服务)
  • 更新邮件状态和附件URL并在本地保存邮件(邮件存储)
  • 将消息序列化为JSON格式(模型层)
  • 将消息发布到pubnub(pubnub服务)
  • 更新消息状态和属性并在本地保存(消息存储)
  • 在上述每个步骤中,都会相应地处理错误。好的。

    UI层由以下子层组成:好的。

  • 视图
  • 视图控制器
  • 视图
  • 为了避免大规模视图控制器,我们使用MVM模式,并在视图中应用逻辑。一个典型的视野通常有协调人和经理作为依赖者。Viewmodels used by viewcontrollers and some kind of views(E.G.Table view cells).视图控制器和视图模型之间的胶水是数据结合和命令模式。为了使这一切成为可能,我们使用了活性生态库。

    okay.

    我们还使用了活性生态COA及其EDOCX1&0作为所有协调人、服务人员、存储方法的界面和返回值类型。这使我们能够同时运行或连续运行,并通过反应性生态COA提供了许多其他有用的东西。

    okay.

    我们试图以宣言的方式执行我们的国际联盟的行为。数据结合和自动布置有助于实现这一目标。

    okay.

    基础设施层包括所有地板、扩展、应用工作所需的公用事业。

    okay.

    这一方法对我们和我们通常建造的应用程序类型都很好。但你应该明白,这只是一个主观的方法,应该适应/改变具体的团队的目的。

    okay.

    希望这能帮助你!

    okay.

    也可以在这个博客中找到更多关于IOS开发过程的信息,作为一个服务。

    okay.好吧


    因为所有的iOS应用程序都是不同的,我认为这里有不同的方法可以考虑,但我通常是这样做的:创建一个中心管理器(singleton)类来处理所有API请求(通常称为apicommunicator),并且每个实例方法都是一个API调用。有一种中心(非公共)方法:

    -(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;

    作为记录,我使用了两个主要的库/框架,reactiveCocoa和afNetworking。reactiveCocoa完美地处理异步网络响应,您可以这样做(sendNext:,sendError:,等等)。此方法调用API,获取结果并以"raw"格式通过rac发送(如nsarray,afnetworking返回)。然后调用上述方法的getStuffList:方法订阅其信号,将原始数据解析为对象(具有类似motis的内容),并将对象逐个发送给调用方(getStuffList:和类似方法也返回控制器可以订阅的信号)。订阅的控制器通过subscribeNext:的块接收对象并进行处理。我在不同的应用程序中尝试了很多方法,但这一种方法效果最好,所以最近我在一些应用程序中使用了它,它既适用于小项目,也适用于大项目,如果需要修改的话,它很容易扩展和维护。希望这有帮助,我想听听其他人对我的方法的看法,也许其他人认为这可以改进。


    在我的情况下,我通常使用reskit库来设置网络层。它提供了易于使用的解析。它减少了我为不同的响应和内容设置映射的工作量。

    我只添加一些代码来自动设置映射。我为我的模型定义了基类(不是协议,因为要检查某个方法是否实现的代码太多,而且模型本身的代码更少):

    地图输入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @interface MappableEntity : NSObject

    + (NSArray*)pathPatterns;
    + (NSArray*)keyPathes;
    + (NSArray*)fieldsArrayForMapping;
    + (NSDictionary*)fieldsDictionaryForMapping;
    + (NSArray*)relationships;

    @end

    映射输入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @implementation MappableEntity

    +(NSArray*)pathPatterns {
        return @[];
    }

    +(NSArray*)keyPathes {
        return nil;
    }

    +(NSArray*)fieldsArrayForMapping {
        return @[];
    }

    +(NSDictionary*)fieldsDictionaryForMapping {
        return @{};
    }

    +(NSArray*)relationships {
        return @[];
    }

    @end

    关系是表示嵌套对象响应的对象:

    关系对象.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @interface RelationshipObject : NSObject

    @property (nonatomic,copy) NSString* source;
    @property (nonatomic,copy) NSString* destination;
    @property (nonatomic) Class mappingClass;

    +(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
    +(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;

    @end

    关系对象.m

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @implementation RelationshipObject

    +(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
        RelationshipObject* object = [[RelationshipObject alloc] init];
        object.source = key;
        object.destination = key;
        object.mappingClass = mappingClass;
        return object;
    }

    +(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
        RelationshipObject* object = [[RelationshipObject alloc] init];
        object.source = source;
        object.destination = destination;
        object.mappingClass = mappingClass;
        return object;
    }

    @end

    然后我设置restkit的映射,如下所示:

    对象映射初始化器.h

    1
    2
    3
    4
    5
    @interface ObjectMappingInitializer : NSObject

    +(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;

    @end

    对象映射初始化器.m

    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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    @interface ObjectMappingInitializer (Private)

    + (NSArray*)mappableClasses;

    @end

    @implementation ObjectMappingInitializer

    +(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {

        NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];

        // Creating mappings for classes
        for (Class mappableClass in [self mappableClasses]) {
            RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
            [newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
            [newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
            [mappingObjects setObject:newMapping forKey:[mappableClass description]];
        }

        // Creating relations for mappings
        for (Class mappableClass in [self mappableClasses]) {
            RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
            for (RelationshipObject *relation in [mappableClass relationships]) {
                [mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
            }
        }

        // Creating response descriptors with mappings
        for (Class mappableClass in [self mappableClasses]) {
            for (NSString* pathPattern in [mappableClass pathPatterns]) {
                if ([mappableClass keyPathes]) {
                    for (NSString* keyPath in [mappableClass keyPathes]) {
                        [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
                    }
                } else {
                    [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
                }
            }
        }

        // Error Mapping
        RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
        [errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
        for (NSString *pathPattern in Error.pathPatterns) {
            [[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
        }
    }

    @end

    @implementation ObjectMappingInitializer (Private)

    + (NSArray*)mappableClasses {
        return @[
            [FruiosPaginationResults class],
            [FruioItem class],
            [Pagination class],
            [ContactInfo class],
            [Credentials class],
            [User class]
        ];
    }

    @end

    mappableEntry实现的一些示例:

    用户,h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @interface User : MappableEntity

    @property (nonatomic) long userId;
    @property (nonatomic, copy) NSString *username;
    @property (nonatomic, copy) NSString *email;
    @property (nonatomic, copy) NSString *password;
    @property (nonatomic, copy) NSString *token;

    - (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;

    - (NSDictionary*)registrationData;

    @end

    用户,m

    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
    35
    @implementation User

    - (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
        if (self = [super init]) {
            self.username = username;
            self.email = email;
            self.password = password;
        }
        return self;
    }

    - (NSDictionary*)registrationData {
        return @{
            @"username": self.username,
            @"email": self.email,
            @"password": self.password
        };
    }

    + (NSArray*)pathPatterns {
        return @[
            [NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
            [NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
        ];
    }

    + (NSArray*)fieldsArrayForMapping {
        return @[ @"username", @"email", @"password", @"token" ];
    }

    + (NSDictionary*)fieldsDictionaryForMapping {
        return @{ @"id": @"userId" };
    }

    @end

    关于请求包装:

    我有头文件和块定义,以减少所有apiRequest类中的行长度:

    APICLALBACK

    1
    2
    3
    4
    typedef void(^SuccessCallback)();
    typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
    typedef void(^ErrorCallback)(NSError *error);
    typedef void(^ProgressBlock)(float progress);

    以及我正在使用的apiRequest类的示例:

    LogiAPI

    1
    2
    3
    4
    5
    @interface LoginAPI : NSObject

    - (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;

    @end

    LogiAPI

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @implementation LoginAPI

    - (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
        [[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
            onSuccess(mappingResult.array);
        } failure:^(RKObjectRequestOperation *operation, NSError *error) {
            onError(error);
        }];
    }

    @end

    在代码中您需要做的就是初始化API对象,并在需要时调用它:

    SomeView控制器.m

    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
    @implementation SomeViewController {
        LoginAPI *_loginAPI;
        // ...
    }

    - (void)viewDidLoad {
        [super viewDidLoad];

        _loginAPI = [[LoginAPI alloc] init];
        // ...
    }

    // ...

    - (IBAction)signIn:(id)sender {
        [_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
            // Success Block
        } onError:^(NSError *error) {
            // Error Block
        }];
    }

    // ...

    @end

    我的代码并不完美,但很容易设置一次,并用于不同的项目。如果这对任何人都有意思的话,我可以花点时间在Github和Cocoapods上为它制定一个通用的解决方案。


    在我看来,所有的软件架构都是由需求驱动的。如果这是为了学习或个人目的,那么就确定主要目标,并让它驱动体系结构。如果这是一项雇佣工作,那么业务需求是最重要的。诀窍是不要让闪亮的东西分散你对真正需求的注意力。我觉得这很难做到。在这个行业里总有新的闪亮的东西出现,而且很多都是无用的,但是你不能总是提前告诉他们。专注于需求,如果可以的话,愿意放弃不好的选择。好的。

    例如,我最近为一家本地企业制作了一个照片共享应用程序的快速原型。由于业务需要做一些快速和肮脏的事情,架构最终是一些iOS代码弹出一个摄像头和一些网络代码附加到一个发送按钮,上传图像到S3存储并写入simpledb域。代码很简单,成本也很低,客户机有一个可扩展的照片集合,可以通过Web通过REST调用进行访问。这个应用程序既便宜又愚蠢,有很多缺陷,有时会锁定用户界面,但如果为原型做更多的工作,那将是浪费,而且它允许用户部署到自己的员工身上,轻松生成数千个测试图像,而无需考虑性能或可伸缩性。糟糕的架构,但它完全符合需求和成本。好的。

    另一个项目涉及实现本地安全数据库,当网络可用时,该数据库在后台与公司系统同步。我创建了一个后台同步器,使用restkit,因为它似乎拥有我需要的一切。但是为了处理特殊的JSON,我不得不为restkit编写大量定制代码,这样我就可以通过编写自己的JSON到coredata转换来更快地完成这一切。但是,客户希望将这个应用程序带到自己的内部,我觉得restkit将类似于他们在其他平台上使用的框架。我等着看这是不是个好决定。好的。

    同样,我面临的问题是集中于需求,让它决定体系结构。我尽量避免使用第三方软件包,因为它们带来的成本只有在应用程序在该领域出现一段时间后才会出现。我尽量避免使用类层次结构,因为它们很少有回报。如果我能在一段合理的时间内写一些东西,而不是采用一个不完美的包,那么我就做了。我的代码具有良好的调试结构和适当的注释,但很少有第三方包。有了这句话,我发现自动对焦网络太有用了,不能忽视,结构良好,评论良好,维护良好,我经常使用它!restkit涵盖了很多常见的情况,但我觉得我在使用它的时候遇到了麻烦,我遇到的大多数数据源都充满了怪癖和问题,这些问题最好用自定义代码处理。在我最近的几个应用程序中,我只是使用内置的JSON转换器并编写一些实用方法。好的。

    我经常使用的一种模式是从主线程获取网络调用。我所做的最后4-5个应用程序使用Dispatch_Source_Create设置了一个后台计时器任务,它每隔一段时间就会唤醒一次,并根据需要执行网络任务。您需要做一些线程安全工作,并确保将修改代码的UI发送到主线程。它也有助于以用户不会感到负担或延迟的方式进行您的入职/初始化。到目前为止,这已经相当有效。我建议调查一下这些东西。好的。

    最后,我认为随着我们的工作越来越多,随着操作系统的发展,我们倾向于开发更好的解决方案。我花了好几年的时间来克服我的信念,即我必须遵循别人声称是强制性的模式和设计。如果我的工作环境是当地宗教的一部分,嗯,我指的是部门的最佳工程实践,那么我会按照惯例行事,这就是他们付钱给我的。但我很少发现遵循旧的设计和模式是最佳解决方案。我总是试图通过业务需求的棱镜来看待解决方案,构建与之匹配的架构,并尽可能保持简单。当我觉得那里不够,但一切都正常时,我就走上了正确的轨道。好的。好啊。


    我使用的方法是:https://github.com/constantine-fry/foursquare-api-v2。我已经用swift重写了这个库,您可以从代码的这些部分看到架构方法:

    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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()

    class Foursquare{
        var authorizationCallback: OperationCallback?
        var operationQueue: NSOperationQueue
        var callbackQueue: dispatch_queue_t?

        init(){
            operationQueue = NSOperationQueue()
            operationQueue.maxConcurrentOperationCount = 7;
            callbackQueue = dispatch_get_main_queue();
        }

        func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
            let parameters: Dictionary <String, String> = [
               "venueId":venueID,
               "shout":shout,
               "broadcast":"public"]
            return self.sendRequest("checkins/add", parameters: parameters, httpMethod:"POST", callback: callback)
        }

        func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
            let url = self.constructURL(path, parameters: parameters)
            var request = NSMutableURLRequest(URL: url)
            request.HTTPMethod = httpMethod
            let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
            self.operationQueue.addOperation(operation)
            return operation
        }

        func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
            var parametersString = kFSBaseURL+path
            var firstItem = true
            for key in parameters.keys {
                let string = parameters[key]
                let mark = (firstItem ?"?" :"&")
                parametersString +="\(mark)\(key)=\(string)"
                firstItem = false
            }
        return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
        }
    }

    class Operation: NSOperation {
        var callbackBlock: OpertaionCallback
        var request: NSURLRequest
        var callbackQueue: dispatch_queue_t

        init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
            self.request = request
            self.callbackBlock = callbackBlock
            self.callbackQueue = callbackQueue
        }

        override func main() {
            var error: NSError?
            var result: AnyObject?
            var response: NSURLResponse?

            var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)

            if self.cancelled {return}

            if recievedData{
                result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
                if result != nil {
                    if result!.isKindOfClass(NSClassFromString("NSError")){
                        error = result as? NSError
                }
            }

            if self.cancelled {return}

            dispatch_async(self.callbackQueue, {
                if (error) {
                    self.callbackBlock(success: false, result: error!);
                } else {
                    self.callbackBlock(success: true, result: result!);
                }
                })
        }

        override var concurrent:Bool {get {return true}}
    }

    基本上,有nsOperation子类可以生成nsurlRequest,解析JSON响应,并将带有结果的回调块添加到队列中。主API类构造nsurlrequest,初始化nsoperation子类并将其添加到队列中。


    根据情况,我们使用一些方法。对于大多数情况,afnetworking是最简单和最可靠的方法,因为您可以设置头文件、上载多部分数据、使用get、post、put&delete,而且uikit还有一系列附加类别,允许您例如从URL设置图像。在一个有很多调用的复杂应用程序中,我们有时会将其抽象为我们自己的一个方便方法,类似于:

    1
    -(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

    但是,在一些情况下,AFNetworking不合适,例如您正在创建框架或其他库组件,因为AFNetworking可能已经在另一个代码库中。在这种情况下,如果正在进行单个调用或抽象为请求/响应类,则可以使用nsmutableurlRequest内联。


    我在设计应用程序时避免使用单例。对于很多人来说,这是一种典型的尝试,但我认为你可以在其他地方找到更优雅的解决方案。通常我所做的是在coredata中构建我的实体,然后将我的REST代码放在nsmanagedObject类别中。例如,如果我想创建和发布一个新用户,我会这样做:

    1
    2
    User* newUser = [User createInManagedObjectContext:managedObjectContext];
    [newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];

    我使用restkit进行对象映射,并在启动时对其进行初始化。我发现把你所有的电话通过一个单一的方式路由是浪费时间的,并且添加了很多不需要的样板文件。

    在nsmanagedObject+extensions.m中:

    1
    2
    3
    4
    5
    + (instancetype)createInContext:(NSManagedObjectContext*)context
    {
        NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
        return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
    }

    在nsmanagedObject+networking.m中:

    1
    2
    3
    4
    5
    - (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
    {
        [[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
        [self handleInputBlocking:blockInput];
    }

    当您可以通过类别扩展公共基类的功能时,为什么还要添加额外的助手类?

    如果您对我的解决方案的详细信息感兴趣,请告诉我。我很乐意分享。


    这个问题已经有很多非常好和广泛的答案,但我觉得我必须提到它,因为没有其他人。

    Alamoire代表Swift。网址:https://github.com/alamofire/alamofire

    它是由与AFNetworking相同的人创建的,但更直接的设计思想是迅捷的。


    从纯类设计的角度来看,您通常会有这样的情况:

    • 控制一个或多个视图的视图控制器
    • 数据模型类——它实际上取决于您处理的实际不同实体的数量以及它们之间的关系。

      例如,如果要以四种不同的表示形式(列表、图表、图形等)显示一个项目数组,则项目列表将有一个数据模型类,项目列表将有一个数据模型类。项目类列表将由四个视图控制器共享-选项卡栏控制器或导航控制器的所有子级。

      数据模型类不仅可以方便地显示数据,还可以序列化数据,其中每个类都可以通过json/xml/csv(或任何其他)导出方法公开自己的序列化格式。

    • 重要的是要理解,您还需要直接映射到RESTAPI端点的API请求生成器类。假设您有一个将用户登录的API,那么您的登录API构建器类将为登录API创建后JSON负载。在另一个示例中,目录项列表api的api请求生成器类将为相应的api创建get查询字符串,并触发其余get查询。

      这些API请求生成器类通常会从视图控制器接收数据,并将相同的数据传回视图控制器进行UI更新/其他操作。然后,视图控制器将决定如何用该数据更新数据模型对象。

    • 最后,REST客户机的核心——API数据获取器类,它忽略了应用程序发出的各种API请求。这个类更可能是单例,但正如其他人指出的,它不必是单例。

      请注意,链接只是一个典型的实现,不考虑会话、cookie等场景,但它足以让您在不使用任何第三方框架的情况下继续工作。


    尝试https://github.com/kevin0571/stnettaskqueue

    在单独的类中创建API请求。

    stnettaskqueue将处理线程和委托/回调。

    可扩展到不同的协议。