

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# 訂閱工作流程教學第 3 部分：實作活動
<a name="swf-sns-tutorial-implementing-activities"></a>

現在我們將從一些提供活動程式碼之常用功能的基底類別開始，以實作工作流程中的每個活動。

**Topics**
+ [定義基本活動類型](#defining-a-basic-activity-type)
+ [定義 GetContactActivity](#defining-getcontactactivity)
+ [定義 SubscribeTopicActivity](#defining-subscribetopicactivity)
+ [定義 WaitForConfirmationActivity](#defining-waitforconfirmationactivity)
+ [定義 SendResultActivity](#defining-sendresultactivity)
+ [後續步驟](#implementing-activities-next-steps)

## 定義基本活動類型
<a name="defining-a-basic-activity-type"></a>

設計工作流程時，我們已識別下列活動：
+ `get_contact_activity`
+ `subscribe_topic_activity`
+ `wait_for_confirmation_activity`
+ `send_result_activity`

我們將會立即實作所有這些活動。由於我們的活動將共用一些功能，讓我們做一些基礎，並建立一些可以共用的常見程式碼。我們稱之為 **BasicActivity**，並於稱為 `basic_activity.rb` 的新檔案中加以定義。

與其他原始檔案相同，我們將包含 `utils.rb` 以存取 `init_domain` 函數，藉此設定範例網域。

```
   require_relative 'utils.rb' 
```

接下來，我們將宣告基本活動類別，及對每個活動感興趣的一些常用資料。我們將在類別屬性中儲存活動的 [AWS::SimpleWorkflow::ActivityType](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SimpleWorkflow/ActivityType.html) 執行個體、「名稱」**和「結果」**。

```
   class BasicActivity

     attr_accessor :activity_type
     attr_accessor :name
     attr_accessor :results
```

這些屬性會存取類別 `initialize`方法中定義的執行個體資料，其會採用活動*名稱*，以及在向 Amazon SWF 註冊活動時要使用的選用*版本*和*選項*映射。

```
     def initialize(name, version = 'v1', options = nil)

       @activity_type = nil
       @name = name
       @results = nil

       # get the domain to use for activity tasks.
       @domain = init_domain

       # Check to see if this activity type already exists.
       @domain.activity_types.each do | a |
         if (a.name == @name) && (a.version == version)
           @activity_type = a
         end
       end

       if @activity_type.nil?
         # If no options were specified, use some reasonable defaults.
         if options.nil?
           options = {
             # All timeouts are in seconds.
             :default_task_heartbeat_timeout => 900,
             :default_task_schedule_to_start_timeout => 120,
             :default_task_schedule_to_close_timeout => 3800,
             :default_task_start_to_close_timeout => 3600 }
         end
         @activity_type = @domain.activity_types.register(@name, version, options)
       end
     end
```

與工作流程類型註冊相同，如果某活動類型已註冊，即可透過查看網域的 [activity\_types](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SimpleWorkflow/Domain.html#activity_types-instance_method) 集合加以擷取。如果找不到活動，則會註冊活動。

另外，與工作流程類型相同，您可以在註冊活動類型時設定與其一起存放的「預設選項」**。

基本活動的最後一項要點為一致的執行方式。我們將定義可採用活動任務的 `do_activity` 方法。如下所示，我們可以使用傳入的活動任務，透過其 `input` 執行個體屬性來接收資料。

```
     def do_activity(task)
       @results = task.input # may be nil
       return true
     end
   end
```

這會包裝 **BasicActivity** 類別。現在，我們將用以定義簡單且一致的活動。

## 定義 GetContactActivity
<a name="defining-getcontactactivity"></a>

在工作流程執行期間執行的第一個活動是 `get_contact_activity`，這會擷取使用者的 Amazon SNS 主題訂閱資訊。

建立一個名為 的新檔案`get_contact_activity.rb`，並且需要兩個 `yaml`，我們將使用它來準備字串以傳遞至 Amazon SWF，以及 `basic_activity.rb`，我們將使用它作為此 **GetContactActivity** 類別的基礎。

```
   require 'yaml'
   require_relative 'basic_activity.rb'

   # **GetContactActivity** provides a prompt for the user to enter contact
   # information. When the user successfully enters contact information, the
   # activity is complete.
   class GetContactActivity < BasicActivity
```

因為我們將活動註冊碼放在 **BasicActivity** 中，所以 **GetContactActivity** `initialize`的方法非常簡單。我們只需要使用活動名稱 `get_contact_activity` 來呼叫基底類別建構函數。這就是註冊活動所需的作業。

```
     # initialize the activity
     def initialize
       super('get_contact_activity')
     end
```

我們現在將定義 `do_activity` 方法，提示輸入使用者的電子郵件和 (或) 電話號碼。

```
     def do_activity(task)
       puts ""
       puts "Please enter either an email address or SMS message (mobile phone) number to"
       puts "receive SNS notifications. You can also enter both to use both address types."
       puts ""
       puts "If you enter a phone number, it must be able to receive SMS messages, and must"
       puts "be 11 digits (such as 12065550101 to represent the number 1-206-555-0101)."

       input_confirmed = false
       while !input_confirmed
         puts ""
         print "Email: "
         email = $stdin.gets.strip

         print "Phone: "
         phone = $stdin.gets.strip

         puts ""
         if (email == '') && (phone == '')
           print "You provided no subscription information. Quit? (y/n)"
            confirmation = $stdin.gets.strip.downcase
            if confirmation == 'y'
              return false
            end
         else
            puts "You entered:"
            puts "  email: #{email}"
            puts "  phone: #{phone}"
            print "\nIs this correct? (y/n): "
            confirmation = $stdin.gets.strip.downcase
            if confirmation == 'y'
              input_confirmed = true
            end
         end
       end

       # make sure that @results is a single string. YAML makes this easy.
       @results = { :email => email, :sms => phone }.to_yaml
       return true
     end
   end
```

`do_activity` 結束時，我們會採用自使用者擷取的電子郵件和電話號碼、放入對應中，然後使用 `to_yaml` 將整個對應轉換為 YAML 字串。原因很重要：您在完成活動時傳遞給 Amazon SWF 的任何結果都*只能是字串資料*。Ruby 可輕鬆地將物件轉換為 YAML 字串後再重新轉換回物件，此功能最適合此用途。

這是 `get_contact_activity` 實作的尾聲。此資料接下來將用於 `subscribe_topic_activity` 實作。

## 定義 SubscribeTopicActivity
<a name="defining-subscribetopicactivity"></a>

我們現在將深入探索 Amazon SNS，並建立使用 產生的資訊`get_contact_activity`來訂閱使用者 Amazon SNS 主題的活動。

建立稱為 `subscribe_topic_activity.rb` 的新檔案、新增用於 `get_contact_activity` 的相同需求、宣告類別，並提供其 `initialize` 方法。

```
   require 'yaml'
   require_relative 'basic_activity.rb'

   # **SubscribeTopicActivity** sends an SMS / email message to the user, asking for
   # confirmation.  When this action has been taken, the activity is complete.
   class SubscribeTopicActivity < BasicActivity

     def initialize
       super('subscribe_topic_activity')
     end
```

現在我們已備妥程式碼來設定和註冊活動，我們將新增一些程式碼來建立 Amazon SNS 主題。為此，我們將使用 [AWS::SNS::Client](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SNS/Client.html) 物件的 [create\_topic](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SNS/Client.html#create_topic-instance_method) 方法。

將 `create_topic`方法新增至您的 類別，這會需要傳入的 Amazon SNS 用戶端物件。

```
     def create_topic(sns_client)
       topic_arn = sns_client.create_topic(:name => 'SWF_Sample_Topic')[:topic_arn]

       if topic_arn != nil
         # For an SMS notification, setting `DisplayName` is *required*. Note that
         # only the *first 10 characters* of the DisplayName will be shown on the
         # SMS message sent to the user, so choose your DisplayName wisely!
         sns_client.set_topic_attributes( {
           :topic_arn => topic_arn,
           :attribute_name => 'DisplayName',
           :attribute_value => 'SWFSample' } )
       else
         @results = {
           :reason => "Couldn't create SNS topic", :detail => "" }.to_yaml
         return nil
       end

       return topic_arn
     end
```

取得主題的 Amazon Resource Name (ARN) 後，我們可以將其與 Amazon SNS 用戶端的 [set\_topic\_attributes](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SNS/Client.html#set_topic_attributes-instance_method) 方法搭配使用，以設定主題的 *DisplayName*，這是使用 Amazon SNS 傳送簡訊時的必要項目。

最後，我們將定義 `do_activity` 方法。我們將從收集在排程活動時透過 `input` 選項所傳遞的任何資料開始。如前所述，這必須以使用 `to_yaml` 所建立的字串傳遞。進行擷取時，我們會使用 `YAML.load` 將資料轉換為 Ruby 物件。

以下是 `do_activity` 的開始，內容為擷取輸入資料。

```
     def do_activity(task)
       activity_data = {
         :topic_arn => nil,
         :email => { :endpoint => nil, :subscription_arn => nil },
         :sms => { :endpoint => nil, :subscription_arn => nil },
       }

       if task.input != nil
         input = YAML.load(task.input)
         activity_data[:email][:endpoint] = input[:email]
         activity_data[:sms][:endpoint] = input[:sms]
       else
         @results = { :reason => "Didn't receive any input!", :detail => "" }.to_yaml
         puts("  #{@results.inspect}")
         return false
       end

       # Create an SNS client. This is used to interact with the service. Set the
       # region to $SMS_REGION, which is a region that supports SMS notifications
       # (defined in the file `utils.rb`).
       sns_client = AWS::SNS::Client.new(
         :config => AWS.config.with(:region => $SMS_REGION))
```

如果我們未收到任何輸入，即無作業可進行，因此會直接讓活動失敗。

假設一切都沒問題，但我們將繼續填寫我們的`do_activity`方法，使用 取得 Amazon SNS 用戶端 適用於 Ruby 的 AWS SDK，並將其傳遞至我們的`create_topic`方法來建立 Amazon SNS 主題。

```
       # Create the topic and get the ARN
       activity_data[:topic_arn] = create_topic(sns_client)

       if activity_data[:topic_arn].nil?
         return false
       end
```

以下是一些值得注意的事項：
+ 我們使用 [https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/Core/Configuration.html#with-instance_method](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/Core/Configuration.html#with-instance_method) 來設定 Amazon SNS 用戶端的區域。我們想要傳送簡訊，因此會使用 `utils.rb` 中所宣告的啟用簡訊功能的區域。
+ 我們將主題的 ARN 儲存至 `activity_data` 對應。這是會傳遞給工作流程中「下一個」**活動的部分資料。

最後，此活動會使用傳入端點 （電子郵件和簡訊） 來訂閱使用者 Amazon SNS 主題。我們不需要使用者進入「兩個」**端點，但至少需要進入其中一個。

```
       # Subscribe the user to the topic, using either or both endpoints.
       [:email, :sms].each do | x |
         ep = activity_data[x][:endpoint]
         # don't try to subscribe an empty endpoint
         if (ep != nil && ep != "")
           response = sns_client.subscribe( {
             :topic_arn => activity_data[:topic_arn],
             :protocol => x.to_s, :endpoint => ep } )
           activity_data[x][:subscription_arn] = response[:subscription_arn]
         end
       end
```

[AWS::SNS::Client.subscribe](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SNS/Client.html#subscribe-instance_method) 採用主題 ARN 和「協定」**(其將巧妙地喬裝為對應端點的 `activity_data` 對應索引鍵)。

最後，我們會以 YAML 格式重新封裝下一個活動的資訊，以便將其傳回 Amazon SWF。

```
       # if at least one subscription arn is set, consider this a success.
       if (activity_data[:email][:subscription_arn] != nil) or (activity_data[:sms][:subscription_arn] != nil)
         @results = activity_data.to_yaml
       else
         @results = { :reason => "Couldn't subscribe to SNS topic", :detail => "" }.to_yaml
         puts("  #{@results.inspect}")
         return false
       end
       return true
     end
   end
```

至此完成了 `subscribe_topic_activity` 的實作。接下來，我們將定義 `wait_for_confirmation_activity`。

## 定義 WaitForConfirmationActivity
<a name="defining-waitforconfirmationactivity"></a>

使用者訂閱 Amazon SNS 主題後，仍需要確認訂閱請求。在此情況下，我們將等待使用者透過電子郵件或簡訊確認。

等待使用者確認訂閱的活動稱為 `wait_for_confirmation_activity`，我們將在此予以定義。若要開始，請建立稱為 `wait_for_confirmation_activity.rb` 的新檔案，並如同設定先前活動的方式加以設定。

```
   require 'yaml'
   require_relative 'basic_activity.rb'

   # **WaitForConfirmationActivity** waits for the user to confirm the SNS
   # subscription.  When this action has been taken, the activity is complete. It
   # might also time out...
   class WaitForConfirmationActivity < BasicActivity

     # Initialize the class
     def initialize
       super('wait_for_confirmation_activity')
     end
```

接下來，我們將開始定義 `do_activity` 方法，並將任何輸入資料擷取到稱為 `subscription_data` 的本機變數。

```
     def do_activity(task)
       if task.input.nil?
         @results = { :reason => "Didn't receive any input!", :detail => "" }.to_yaml
         return false
       end

       subscription_data = YAML.load(task.input)
```

既然已經有了主題 ARN，即可建立新的 [AWS::SNS::Topic](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SNS/Topic.html) 執行個體並對其傳遞 ARN 以擷取主題。

```
       topic = AWS::SNS::Topic.new(subscription_data[:topic_arn])

       if topic.nil?
         @results = {
           :reason => "Couldn't get SWF topic ARN",
           :detail => "Topic ARN: #{topic.arn}" }.to_yaml
         return false
       end
```

現在，我們將檢查主題，確認使用者是否已使用其中一個端點確認訂閱。我們只需要確認該端點將活動視為成功即可。

Amazon SNS 主題會維護該主題的[訂閱](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SNS/Topic.html#subscriptions-instance_method)清單，我們可以檢查使用者是否已確認特定訂閱，方法是查看訂閱的 ARN 是否設定為 以外的任何項目`PendingConfirmation`。

```
       # loop until we get some indication that a subscription was confirmed.
       subscription_confirmed = false
       while(!subscription_confirmed)
         topic.subscriptions.each do | sub |
           if subscription_data[sub.protocol.to_sym][:endpoint] == sub.endpoint
             # this is one of the endpoints we're interested in. Is it subscribed?
             if sub.arn != 'PendingConfirmation'
               subscription_data[sub.protocol.to_sym][:subscription_arn] = sub.arn
               puts "Topic subscription confirmed for (#{sub.protocol}: #{sub.endpoint})"
               @results = subscription_data.to_yaml
               return true
             else
               puts "Topic subscription still pending for (#{sub.protocol}: #{sub.endpoint})"
             end
           end
         end
```

如果我們取得訂閱的 ARN，則會將之儲存至活動結果資料、轉換為 YAML，然後從 `do_activity` 傳回 true，指出活動已順利完成。

由於等待訂閱確認可能需要一些時間，我們偶爾會`record_heartbeat`呼叫活動任務。這會向 Amazon SWF 發出活動仍在處理的訊號，也可以用來提供活動進度的更新 （如果您正在處理您可以報告進度的檔案等動作）。

```
         task.record_heartbeat!(
           { :details => "#{topic.num_subscriptions_confirmed} confirmed, #{topic.num_subscriptions_pending} pending" })
         # sleep a bit.
         sleep(4.0)
       end
```

這會結束 `while` 迴圈。如果我們以某種方式離開 while 迴圈而未成功，則會報告失敗，並完成 `do_activity` 方法。

```
       if (subscription_confirmed == false)
         @results = {
           :reason => "No subscriptions could be confirmed",
           :detail => "#{topic.num_subscriptions_confirmed} confirmed, #{topic.num_subscriptions_pending} pending" }.to_yaml
         return false
       end
     end
   end
```

至此完成了 `wait_for_confirmation_activity` 的實作。我們只需要再定義一個活動：`send_result_activity`。

## 定義 SendResultActivity
<a name="defining-sendresultactivity"></a>

如果工作流程目前有所進展，我們已成功將使用者訂閱 Amazon SNS 主題，而且使用者已確認訂閱。

最後一個活動 `send_result_activity` 會使用使用者所訂閱的主題及使用者用來確認訂閱的端點，將成功訂閱主題的確認傳送給使用者。

請建立稱為 `send_result_activity.rb` 的新檔案，並如同設定先前所有活動的方式加以設定。

```
   require 'yaml'
   require_relative 'basic_activity.rb'

   # **SendResultActivity** sends the result of the activity to the screen, and, if
   # the user successfully registered using SNS, to the user using the SNS contact
   # information collected.
   class SendResultActivity < BasicActivity

     def initialize
       super('send_result_activity')
     end
```

`do_activity` 方法一開始也同樣會從工作流程取得輸入資料，並從 YAML 轉換之，然後使用主題 ARN 建立 [AWS::SNS::Topic](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SNS/Topic.html) 執行個體。

```
     def do_activity(task)
       if task.input.nil?
         @results = { :reason => "Didn't receive any input!", :detail => "" }
         return false
       end

       input = YAML.load(task.input)

       # get the topic, so we publish a message to it.
       topic = AWS::SNS::Topic.new(input[:topic_arn])

       if topic.nil?
         @results = {
           :reason => "Couldn't get SWF topic",
           :detail => "Topic ARN: #{topic.arn}" }
         return false
       end
```

有了主題之後，就要將訊息[發佈](https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SNS/Topic.html#publish-instance_method)到該主題 (也會反應至螢幕)。

```
       @results = "Thanks, you've successfully confirmed registration, and your workflow is complete!"

       # send the message via SNS, and also print it on the screen.
       topic.publish(@results)
       puts(@results)

       return true
     end
   end
```

發佈至 Amazon SNS 主題會將您提供的訊息傳送至該主題存在*的所有*訂閱和已確認端點。因此，如果使用者透過電子郵件和簡訊號碼「兩者」**進行確認，則會收到兩則確認訊息，即一個端點一則。

## 後續步驟
<a name="implementing-activities-next-steps"></a>

至此完成了 `send_result_activity` 的實作。在接下來的「[訂閱工作流程教學第 4 部分：實作活動任務輪詢器](swf-sns-tutorial-implementing-activities-poller.md)」中，您可以在處理活動任務的活動應用程式中將所有這些活動都繫結在一起，並且能啟動活動予以回應。