Feature Driven Development(特性驱动开发)

一、Feature Driven Development(特性驱动开发)

1.What do I mean by Feature Driven Development?

简而言之:在你的代码本身中包含app需要用到的一些特性和操作的信息,使用这些信息我们只需要通过添加一些少量的代码就可以添加app需要的各种功能。调试和诊断故障变得更加容易,因为您可以清楚地识别用户在做什么,而且它可以使应用程序的内部体系结构更加清晰。这可以帮助开发产品的每个人使用相同的术语。

2.Why is this useful?

客户: “应用程序崩溃了。我打算给你一个星评价“

开发者: “我很抱歉。请告诉我们你在碰撞中遇到了什么,以便我们可以重现它并帮助你“

我在开发过程中会遇到这样的情况,尽管我们用了类似于fabric这样的bug追踪日志,但还是很难知道用户在应用程序中到底做了什么。所以如果代码能够更好地了解用户在功能级别上所做的事情时,我们将会得到更多有用的信息。Feature Driven Development就是围绕其功能设计应用程序,以及用户在与这些功能交互时执行的高级操作。这个想法并不是特定于任何一个平台或特定类型的应用程序,但似乎特别适合于移动和桌面应用程序。苹果平台发布了一个名为Flint的开源项目,实现了这些想法。

3.The core concepts

当你增加一个功能的时候,可能会遇到这个功能在某些条件下不可用的状况,比如一个做法功能我们要求它的添加和编辑操作都不能用,通常我们会禁掉添加和编辑按钮点击事件。Feature Driven DevelopmentFeature作为a first-class object,我们不仅可以描述action能不能用,也可以直接描述Feature能不能用,整个Feature不能用的话,功能绑定的操作也就不能使用。

FDD有四个基本组成部分:Feature, Action, Input and Presenter.

feature功能是人们可以用你的应用程序来做的事情,action动作是您的程序实际为用户使用功能所做的事情。即使对于一个微不足道的特性,也总是需要多个操作。

feature功能是我们设计和销售的。action动作是我们在代码中“执行”动作使功能发生,并通过FDD工具(分析、日志等)跟踪这些功能。

例子:

  • “文档编辑”是一项高级功能。它可能有许多子功能,例如“插入图片”,“拼写检查”。(Features 功能组的概念)
  • “发布推文”是一项功能,其中包括多项操作,例如“显示撰写UI”,“另存为草稿”,“取消”和“发布”。它可能具有子功能“附加媒体”,其操作为“显示媒体库”,“附加所选媒体”,“关闭”。(一个feature功能绑定多个action)

没有严格的规则,你可以根据分割功能的方式做些适合自己的规则。似乎有效的一般规则是:

如果您需要多个show/dismiss操作,则可能需要将这些操作拆分为子功能

Feature在代码中定义

属性名 描述
id 功能的唯一ID,以便可以在日志中轻松识别。
name 一个功能名称,用于向开发人员和QA显示。
description 功能的描述,显示给开发人员和QA。
isAvailable 该功能是否当前可用
availability 确定该功能的可用性的指示-例如需要运行时检查,基于用户偏好或设置需要购买
actions 此功能中可用动作的列表
variation 哪种A/B测试变体目前有效

以上只是一个指南,实际上由于框架设计的静态特性,Flint与上述有一些分歧,以利用Swift语言特性实现编译时的安全性。

例如,Flint将不总是可用的功能分隔为单独的类型ConditionalFeature。它仅具有该类型的可用性属性,这使其能够有选择地提供在非条件或条件特征上有选择地可用的函数。这使得在不首先检查特征是否可用的情况下不可能执行条件特征的动作。

Flint在Feature里也没有actions属性,而是使用简单的特定于域的语言来声明Action,以便在调用Action input和Presenter类型时具有编译时类型安全性。

Action在代码中定义

属性名 描述
id 操作的唯一ID,以便可以在日志中轻松识别。
name 一个操作名称,用于向开发人员和QA显示。
description 操作的描述,显示给开发人员和QA。
inputType 操作所需的输入类型
presenterType 操作所需的演示者类型用来提供输出。
analyticsID 分析事件ID报告到分析后端,可选的,因为有些操作不需要分析事件
perform(input, presenter, completion) 实际执行操作的函数

操作实现通常是简单的逻辑片段,它们将业务逻辑代码中的所有组件连接在一起,获取输入并在必要时显示操作的结果。

通常操作实现只有几行代码:

if input is valid then

  call service to do the work, passing input

  call presenter to show results of the action

  call completion handler indicating success or failure

else

  call completion handler indicating failure

Input是您的操作所需的任何类型,通常将几条信息折叠成单个包装类型,例如a struct或class。

Presenter是你定义的动作可以用它来提供输出的任何类型。使用这些interface或协议类型将允许您将操作与所有UI分离,并使您的应用程序的主要操作非常容易进行单元测试,而无需启动UI。这样做也会使您的行为跨平台。

4.Putting it into practice

当您的应用需要调用操作时,它必须首先验证操作的功能当前是否可用,然后将输入和演示者传递给操作本身。如果您知道该功能始终可用,则可以避免此功能可用性步骤,但这取决于您的FDD实施。

执行动作是最重要的想法,这里是伪代码的简单表达:

if documentEditFeature is enabled then

  perform duplicateShapeAction with

    the current shape as input and

    the current view controller as presenter 

上面的伪代码非常适合于自动化任务,比如报告分析,跟踪用户在调试中做了什么,以及用户必须额外付费才能享受特性的这种阻塞性功能。

FDD action diagram.png

FDD帮助您的团队在生成高质量应用程序所需的整个“堆栈”中使用相同的功能和操作术语。当讨论与“打开文件”相关的错误时,您实际应该得到的错误是与“文档管理功能”的“打开文件操作”明显相关的错误。您的测试脚本将使用相同的语言…并且通过FDD,您的日志也将使用相同的语言。您开始考虑功能和操作的产品设计过程,并从那里开始着手。

5.What FDD gives you
  • 功能标记(例如,仅在某些内部版本或某些用户中启用新功能)

  • 跟踪分析以了解用户操作

  • 启用特定功能的应用内购买和订阅

  • 从URL深入链接到应用程序的内容

  • 自动化工作流程

  • 操作系统集成(例如Handoff,Spotlight Search)

  • 提供A / B测试(为不同的用户群提供不同的功能变体)

  • 调试日志记录和崩溃报告,维护与用户操作的明确链接

  • 记录用户操作“时间轴”,显示用户按顺序执行的所有操作

  • 改进甚至自动化的测试计划

  • 用户行为分析(用户如何使用您的应用)

  • 通过将UI与执行操作的代码分离,更轻松地对功能进行单元测试

  • 了解每个用户使用模式的高级功能。想象一下用户在您的应用程序中完成的操作的持久时间表,能够在运行时查询以建议下一步操作或将机器学习应用于它

6.Contextual Logging

可以包括写入日志输出时的功能和操作。它还可以包括输入的文本描述。更重要的是,您可以在运行时或作为事后调查的一部分过滤特定功能的所有日志输出。每个日志条目的“主题路径” 是分层的。

下面是一个示例,其中包含有关触发它的功能和操作的信息的日志记录。这是Flint演示应用程序的简化输出,该应用程序已启用日志记录以跟踪所有操作的开始和完成,并且具有“ActivitiesFeature”,可自动将所有符合条件的成功完成的操作发布到操作系统,以进行搜索索引:

16:56:02.687 DEBUG FlintDemo.AppFeatures/DocumentManagementFeature/#DocumentCreateAction | Starting
16:56:02.704 DEBUG FlintDemo.AppFeatures/DocumentManagementFeature/#DocumentCreateAction | Completed (success)
16:56:07.067 DEBUG FlintDemo.AppFeatures/DocumentManagementFeature/#DocumentSaveAction | Starting with input "How FDD Works.txt"
16:56:07.076 DEBUG FlintDemo.AppFeatures/DocumentManagementFeature/#DocumentSaveAction | Completed (success)
16:56:08.192 DEBUG FlintDemo.AppFeatures/DocumentManagementFeature/#DocumentOpenAction | Starting
16:56:08.199 DEBUG FlintDemo.AppFeatures/DocumentManagementFeature/#DocumentOpenAction | Completed (success)
16:56:08.261 DEBUG FlintCore.FlintFeatures/ActivitiesFeature/#PublishCurrentActionActivityAction | Setting user activity: Activity type: co.montanafloss.FlintDemo.document-open. Title:"How FDD Works.txt"
16:56:08.261 DEBUG FlintCore.FlintFeatures/ActivitiesFeature/#PublishCurrentActionActivityAction | End of breadcrumb trail
16:56:08.261 DEBUG FlintCore.FlintFeatures/ActivitiesFeature/#PublishCurrentActionActivityAction | Starting
16:56:08.262 DEBUG FlintCore.FlintFeatures/ActivitiesFeature/#PublishCurrentActionActivityAction | Completed (success)

Flint使用这个提供了自己的称为“Focus”的功能,开发人员可以调用一个函数将特定功能添加到“Focus Area”,并且除了记录结果外所有应用程序范围内的记录都被从上下文记录器中获取当前关注的功能或子功能。这可以在运行时更改,以简化调试并消除获取全部应用程序日志记录的冗杂感。

7.your own implementations

我们已经介绍了基本的构建模块:Features, Actions, Inputs and Presenters.

在构建Flint时,还出现了一些其他有用的概念

Feature Group — 有一种特殊的功能,它可以具有子功能,这使您能够构建功能的层次结构。这意味着,如果用户没有进行特定的应用程序内购买,您可以很容易地禁用整个功能组。

Action Session —用于调用操作并提供分组,以便在日志中清楚地识别它们,以及强制执行线程合同。您可以想象一个多窗口应用程序每个打开的文档或窗口会有一个会话,以及一个或多个后台会话。

Action Dispatcher -会话使用调度程序实际调用操作。它提供了一个单独的位置来观察所有会话中的所有调用,用于自动日志记录和与操作相关的辅助作用。

Action Context —它们包含上下文日志记录器、输入和便利功能,并在执行这些功能时传递给操作。

Contextual Logger -这是一个简单的日志接口,它私有地存储每个动作调度的特性和动作信息,以及动作输入的文本表示和其他有用信息。底层日志实现实现上下文日志所需的一切。它被传递给操作,这样它们就可以执行特定于上下文的日志记录,并将其转发给其他子系统。

FDD Flint action diagra          m.png更多详情参考:http://www.montanafloss.co/blog/feature-driven-development

初探Flint

Flint

Flint是一个用Swift编写的应用程序框架,它帮助您利用Feature Driven Development(特性驱动开发)中的想法,从特性和操作中构建应用程序。

一、使用flint能做什么?

令人愉快的用户体验意味着将与大量的API进行交互。我们必须仔细请求权限,处理应用内购买,跟踪使用情况分析,并通过网址,通知或Siri深入链接到我们的应用。使用Flint,您可以添加少量代码来描述构成应用程序的功能和操作,剩下的就交给flint框架了。

二、如何将Flint集成到您的项目中

1.为了使用Flint,您需要构建框架并将其及其依赖关系添加到您的项目中。在你自己的项目中使用 Flint 最简单的方法是通过Carthage。您可以像这样将依赖项添加到您的Cartfile中:

github "MontanaFlossCo/Flint" "master"

2.然后运行==carthage bootstrap==,为所有平台构建它。对于更快的构建,您可以限制在一个平台上,并使用缓存来避免重新构建,如果没有对您已经拥有的版本进行更改,例如:

carthage bootstrap --platform iOS —cache-builds

3.将框架添加到项目中

3.1将FlintCore.framework从./Carthage/Build/iOS(或任何你的项目需要的平台文件夹)拖拽到你的app target的Xcode常规选项卡的“Linked Frameworks & Libraries”部分

3.2如果iOS需要FlintUI,对这个FlintUI框架做同样的操作

3.3.对ZIPFoundation.framework (Flint的一个依赖库)也做同样的事情

3.4在Build Phases验证这些框架是否列在“Link Binary With Libraries”部分

3.5确保您的项目为Carthage的复制框架构建阶段正确设置。参见Carthage Quick Setup

三、创建功能和操作

定义你的应用的功能和他们提供的操作是Flint实现Feature Driven Development功能驱动开发的基本出发点。

1.定义您的第一个特性组

您的应用程序通常有一个主功能组,它具有应用程序支持的所有根级功能。在应用程序启动时将其设置为Flint时将其传递。

它是一个类,您可以提供任何名称,但通常您会使用类似AppFeatures的东西。它只需要符合FeatureGroup并定义一组子特性。

final class AppFeatures: FeatureGroup {                   
    static var description = "My main app features"      
    static var subfeatures: [FeatureDefinition.Type] = []
}   

你可以在启动时调用setup或quickSetup时使用此命令:

funcapplication(_application:UIApplication,didFinishLaunc hingWithOptios launchOptions:                             [UIApplicationLaunchOptionsKey: Any]?) -> Bool {         
    Flint.quickSetup(AppFeatures.self)                                 
    …                                                                   
}   
                                                                                                                      您可以注册多个根级别的特性组,例如,如果您有一些框架也公开特性的话。用Flint.register()手动注册这些。您应该只调用setup()或quickSetup()一次。
2.定义你的第一个功能

现在您可以定义一个真实的特性,并将其添加到根特性的子特性中。

一个常规的特性总是可用的,它不能被关闭。这意味着您的代码可以始终执行属于它的操作,而无需三思。

class DocumentManagementFeature: Feature {              
    static let description = "Create, Open and Save       documents”                                                       
    static func prepare(actions: FeatureActionsBuilder) {
    	// We'll add actions here soon                        
    }                                                    
 }  

注意:Feature要实现FeatureDefinition协议,每个特性都有一个Xcode项目组,其中包含特性的.swift文件,以及特性所需的其他类型(如操作和演示者)。现在我们必须返回并编辑功能组,以及DocumentManagementFeature这个功能的子功能:

final class AppFeatures: FeatureGroup {                     
    static var description = "My main app features”      
    static var subfeatures: [FeatureDefinition.Type] = [ 
    	DocumentManagementFeature.self                        
    ]}   

#

3.定义你的第一个操作

现在我们有了一个特性,让我们添加一个动作。Flint中的一个动作是一小块逻辑,它代表了应用程序可以做的事情——通常是响应一些用户驱动的事件或外部刺激(如推送通知或位置更改)。有时候,您需要的操作不是由用户直接调用的,而是必需的,因为您需要跟踪应用程序中发生的事情,或者需要响应URL。

操作接收单个input和一个presenter,action将执行工作,然后更新演示程序。 input是您在定义操作时指定的任何类型, presenter也一样。然后,我们将使用Swift关联类型的强大功能,将传递给操作的inputs和presenters 限制为那些类型。

final class DocumentOpenAction: Action {

// Define the type of input this action expects       
typealias InputType = DocumentRef                    

// Define the type of presenter this action expects   
typealias PresenterType = DocumentPresenter          

static var description = "Open a document”           
static func perform(with context:                     ActionContext<DocumentRef>, using presenter:               DocumentPresenter, completion: @escaping                                ((ActionPerformOutcome) -> ())) {                           
    presenter.openDocument(documentRef)                  
    completion(.success(closeActionStack: false))    
}                                                    

}

这里我们需要做的是接收对文档的引用作为输入,然后将其传递给演示者进行显示。为了简单起见,此时我们实际上并没有加载文档。

动作的perform执行函数接收上下文,其中包括输入和对特定上下文日志记录器的访问、表示器和完成回调。注意,执行函数必须使用InputType和PresenterType。所以context参数必须是ActionContext<…用特定的输入类型替换>,或者如果您愿意,可以使用InputType别名。 presenter的论点也是如此。如果这些类型不匹配,您将会得到令人困惑的Swift编译器错误。

不需要输入的操作必须使用NoInput作为它们的InputType。如果不需要 presenter,则必须使用NoPresenter作为演示者类型。这些是特殊的Flint类型,可以让你表明你不需要输入或演示者。下面是一个既没有输入也没有演示者的操作示例:

final class BeepAction: Action {                            
    typealias InputType = NoInput                          
    typealias PresenterType = NoPresenter                     
    static func perform(with context:                      ActionContext<InputType>, using presenter: PresenterType, completion: @escaping ((ActionPerformOutcome) -> ())) {  
        print(“Beep!")                                    
        completion(.success(closeActionStack: false))    
    }                                                     
}   

您将注意到,在传递给完成处理程序的结果上设置了closeActionStack参数。此标志指示操作是否“关闭”功能上的一系列操作,操作堆栈使用这些操作。一个完美的例子是关闭一个文档,你可以让Flint丢弃操作堆栈。

4.将动作action与功能(特性)feature绑定

现在我们必须将操作添加到特性中,因此我们从前面编辑特性声明,为操作和特性的绑定添加一个静态属性,并通过声明该操作来准备特性。

class DocumentManagementFeature: Feature {               
    static let description = "Create, Open and Save       documents”                                               
    static let openDocument =                              action(DocumentOpenAction.self)                           
    static func prepare(actions: FeatureActionsBuilder) {
        actions.declare(openDocument)                      
    }                                                    
 }  

首先,上面显示的静态操作属性 openDocument表示您以后将访问该action并执行它。其次,必须为操作值分配action()函数的结果,它是操作和特性的绑定。我们使用它来知道当您调用该操作时,该操作属于哪个特性。

最后,prepare函数必须调用actions.declare,用于所有要使用的操作。您可以调用publish来代替它,如果您有显示动作的UI(例如带有工具面板的绘画应用程序),则将它们标记为“visible to the user(对用户可见)”。

5.调用功能的操作

在您的应用中,您需要执行此操作。操作在一个内部执行ActionSession,它为您的日志记录和调试中的操作提供概念分组(例如,每个窗口一个会话,或当前打开的文档),跟踪操作堆栈并确保线程卫生。默认情况下,操作在主会话中执行ActionSession.main,期望在main队列中调用。如果你不在main队列调用它,它会崩溃,fatalError抛出崩溃信息。

正确示例,在视图控制器的某处,你可以添加:

DocumentManagementFeature.openDocument.perform(using:self , with: document)   

这里表示调用了文件管理功能下的打开文档操作,using参数是presenter,也就是说在我们的这个VC里需要实现DocumentPresenter协议,with是input,输入类型是document

如果操作使用NoInput或NoPresenter或两者都使用,那么在执行操作时可以省略这些参数:

TestFeature.beep.perform()  

如上所述,上面的调用都是在 main ActionSession 进行的。如果我们在应用程序中有后台处理,我们可以创建另一个ActionSession并分派到它,使用稍微不同的语法:

// Do this at startup or "session setup" time, stash it   globally somewhere                                        
let bgProcessingSession = ActionSession(name: “bg",                                 userInitiatedActions: false,                                                    	callerQueue:     myBackgroundQueue)                                       
// Later on your background queue”                        
bgProcessionSession.perform(MachineLearningFeature.proces sImages, using: nil, with: imageStore) 
6.Conditional Features(条件特征)

定义:仅在满足权限,购买和其他要求时才使您的功能可用

6.1使用约束定义条件特征

第一步是将您的功能定义为符合协议ConditionalFeature而不是Feature。然后,您可以通过实现该constraints函数来声明对该功能的约束,该功能将告知Flint何时此功能在运行时可用。此函数在启动时仅调用一次。

以下是Flint自己的深度链接支持条件功能的示例:

/// Flint's deep linking feature for URL Routes is conditional so that you can disable
/// it if you don't want it.
public class DeepLinkingFeature: ConditionalFeature {
    public static var description: String = "Deep Linking and app-URL handling"
	
    // Declare the constraints that must be met for
    // the feature to be available
    public static func constraints(requirements: FeatureConstraintsBuilder) {
    	requirements.runtimeEnabled()
    }

    // It's on by default. 
    public static var isEnabled: Bool? = true

    /// The action to use to perform the URL
    public static let performIncomingURL = action(PerformIncomingURLAction.self)
    
    public static func prepare(actions: FeatureActionsBuilder) {
        actions.declare(performIncomingURL)
    }
}

上述约束.runtimeEnabled意味着isEnabled要素的属性必须返回true才能使该要素可用。此外,Flint提供的默认实现始终返回true。您可以在自己的功能中覆盖该属性,以允许在编译或运行时分配它,或者调用其他代码以查明是否应该启用它。

某些约束不会随着时间的推移而改变它们的值 - 例如最小操作系统版本 - 而其他约束则在运行时在需要时进行评估,因为它们的状态可以在应用程序运行时更改。例如,如果用户进行应用内购买,则一旦确认购买有效,现在就可以获得依赖于该购买的功能。

6.2定义前提条件

支持三种前置条件功能:

  • purchase(requirement: PurchaseRequirement) - 该功能在购买之前需要一次或多次购买。
  • runtimeEnabled()- 每当请求使用该功能时,YourFeature.isEanbled将检查其值。
  • userToggled(defaultValue: Bool) - 功能将检查用户的当前设置以查看是否已启用此功能。
let premiumSubscription = Product(name: "💎 Premium Subscription",
                                  description: "Unlock the level builder",
                                  productID: "SUB0001")
public class LevelBuilderFeature: ConditionalFeature {
    public static var description: String = "Premium Level Builder"
    public static func constraints(requirements: FeatureConstraintsBuilder) {
    	requirements.userToggled(defaultValue: true)
    	requirements.runtimeEnabled()   requirements.purchase(PurchaseRequirement(premiumSubscription))
    }
    static var isEnabled: Bool? = MyPlayerProgressTracker.shared.tutorialCompleted
    ...
}
6.3定义所需的权限

要声明权限约束,请使用permission()permissions(...)函数:

public class SelfieFeature: ConditionalFeature {
    public static var description: String = "Selfie Posting"
    public static func constraints(requirements: FeatureConstraintsBuilder) {
    	requirements.permissions(.camera,
                                 .photos,
                                 .location(usage: .whenInUse))
    }
    ...
}

该函数描述了当用户使用相机或相册的时候请求用户授权

6.4定义平台限制

使用.iOS.watchOS.tvOSmacOS在构建器属性可以设置为可用此功能所需的最小版本。您可以选择将其设置为.any,或者.unsupported如果您想要阻止某个平台上没有意义的功能。所有平台的默认值为.any

public class ExampleFeature: ConditionalFeature {
    public static func constraints(requirements: FeatureConstraintsBuilder) {
    	requirements.iOS = 9
    	requirements.macOS = "10.12.1"
    	requirements.tvOS = .any
    	requirements.watchOS = .unsupported
    }
    ...
}

四、调试工具

用于在iOS上查看基于Flint的应用程序,以验证您的功能和操作是否已正确设置,浏览时间轴和焦点日志以查找问题,生成调试报告等

1.注册FlintUI功能
import FlintCore
import FlintUI
...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    Flint.quickSetup(FakeFeatures.self, domains: [], initialDebugLogLevel: .debug, initialProductionLogLevel: .info)
    // Add the FlintUI features
    Flint.register(FlintUIFeatures.self)
    ...
}
2.功能浏览器

此功能允许您深入查看应用程序中声明的所有功能的层次结构以及其操作声明。这对于调试和验证代码已正确设置一切非常有用。您还可以查看给定版本和用户配置文件中当前启用的功能。

let vc: UIViewController
vc = FeatureBrowserViewController.instantiate()

时间线浏览器截图

3.时间线浏览器

此功能将显示时间轴的当前内容,以便您可以查看用户执行的操作,并深入查看其详细信息,显示输入和相关属性。

let vc: UIViewController
vc = TimelineViewController.instantiate()

时间线浏览器截图

4.动作堆栈浏览器

此功能允许您查看应用程序中活动的操作,按会话和启动堆栈的原始功能进行细分。

您可以深入查看堆栈以查看到目前为止已发生的操作,包括用户开始使用其他功能的子堆栈。

let vc: UIViewController
vc = ActionStackListViewController.instantiate()

时间线浏览器截图

更多详情参考:https://flint.tools

git地址:https://github.com/MontanaFlossCo/FlintDemo-iOS