のほほん停留所

びぼうろく

成長の幅を狭めない「育成」

このブログにおける育成とは以下を想定しています。

  • トレーナーとメンバーが「1 : 1」であること
  • 成熟しておらずサポートを受ける必要があるメンバー
  • トレーナーとメンバーが同じチーム

育成の方針

  • 依頼は方法ではなく目的を伝える
  • 期待は繰り返し伝える
  • 任せた仕事は途中で介入しない
  • 行動の結果ではなく考え方について議論する
  • 成功はメンバーの成果、失敗は自分の責任と捉える
  • 自分の考え方や方法を強制しない

過去の育成について振り返ってみると、自分は上記の方針で育成に取り組んでいました。

依頼は方法ではなく目的を伝える

メンバーと同じチームなので、自分からメンバーに共同作業や作業依頼する機会があります。 既存の実現法の流用で達成できる依頼が多いのですが、その際に方法ではなく目的を伝えるようにしています。

成果の達成は方法を伝えると早いケースは多いですが、既存の実現法との乖離がある可能性を常に考慮して進めて欲しいからです。 これは将来的にメンバーが新しい仕組みを作る際、既存の実現法を活かして進める能力を育成する意図があります。

期待は繰り返し伝える

「将来的に2, 3人のメンバーのチームのリーダーを任せたい」などの長期的な期待は繰り返し伝えるように意識しています。 目の前の仕事に集中すると実現が目的になり、目指すキャリアに近づけていない状態が長続きします。 この状態に陥らないように定期的に振り返る目的で繰り返し伝えています。

任せた仕事は途中で介入しない

致命的な失敗がある場合は介入することはありますが、基本的には任せ切っています。 これは自分が介入することで、その仕事についての思考を奪ったり、自分の能力の限界がその仕事の限界にしないためです。

行動の結果ではなく考え方について議論する

外部的な要因により本人の能力に依らず失敗することがよくあります。 その際に振り返ることは行動の結果ではなく行動を起こした考え方について議論するように意識しています。 その考え方が妥当であれば、今後の仕事の際に成功する確率が高いと考えているからです。

成功はメンバーの成果、失敗は自分の責任と捉える

基本は任せ切りなので成功はメンバーの成果になります。 ただ、失敗した際は自分の行動によって解決できたかをまず考えるようにしています。 ジョブサイズや外部要因などを切り分けて、次の仕事の際にどのぐらいのジョブサイズ、サポートが適切かを検討するためです。

自分の考え方や方法を強制しない

自分が余裕がない時期についやってしまうのですが、意識して「自分は〜」という発言はしません。 自分の考え方や方法について話すのはメンバーに聞かれた際に限定しています。 メンバーに正解のような形で伝えることで思考の幅を狭めないようにするためです。

まとめ

全ての方針について共通しているのは「思考してもらう」「思考の幅を狭めない」ということです。 期待する成果を出してもらうためにメンバーをコントロールしようとして発言や行動をしてしまうので、上記を意識して行動しています。

この育成との相性もあると思うのですが、将来的に自分の思考で切り開ける人材になってほしいと期待してるので、自分の想像を超えてこれるように幅を狭めないように育成をしています。

会社やチームにおける個人の目標設定の役目について

全体目標と個人目標

これまでに自分が所属してきた会社やチームは、半年や3ヶ月毎に目標を設定して、その目標を達成するために期間内に実施することを考えています。 その会社やチームの目標に付随するように所属するメンバー毎の個人目標も設定しています。

会社やチームなどの全体目標は意思決定の方針に役に立つと思うのですが、それに比べて個人目標の役目については曖昧になっていたり理解が進んでいない印象があります。

全体目標を細分化して個人目標に配分する手法もあり、全体目標に対してのタスクのように個人目標は捉えられがちです。 ただ、そのような手法はタスクリストを作成したいわけではなく、全体目標を達成できるチームになるために個人がどのように変化していくべきかを示すためだと自分は考えています。

個人目標の設定

目標自体は外的要因などにより達成できない可能性もあるので、個人目標に関しては目標を考えるまでの時間が大切だと考えています。

しかし、全体目標は会議などで考える時間がしっかりと設定されているのに対して、個人目標の設定は日常業務と同様なタスクとして積まれることが多いです。 そのため、個人目標の設定は終わらせることが目的になる傾向があります。 それにより、個人目標自体もタスクリストのような内容になったり、深く考えられていない目標になる傾向があります。 そのような目標だと振り返りの際に意義のある内容になることは難しいです。

自分はこのような現象は個人の成長に対しての機会損失だと考えています。 過去にこのように失敗した経験をして、それ以降の目標設定に対しての取り組み方や意識が変わりました。

まず、個人目標の設定をタスクの間の短い時間で行うのではなく、まとまった時間を確保して考えるようにしました。 また、その時間で以下のような順番で考えるようにしました。

  1. チームや自分の現状の課題について考える
  2. 考えた課題を解消した際にチームや自分がどのようになっているかを想像する
  3. 将来像に対して近づくためのアクションを考える

このように考えてから評価者やマネージャーなどと話すと、「課題に対する解像度」「将来像の方向性」「アクションの妥当性」などのより具体的なフィードバックを受けられるようになったと感じました。 評価者やマネージャーは複数のメンバーの目標を確認しないといけないので、今までゴールやアクションしか書いていなく対話で解決していた部分を目標の文章として思考のフローを表現できるようにしました。

組織によって目標設定の役割は異なりますが、自分は紹介したような方針で個人目標を活用しています。 個人の目標設定は評価者や被評家者の双方の対話によって、相互理解が出来て認識齟齬を減らすことが出来るツールだと捉えています。

目標設定の機会は頻度がそれほど高いものではないので、より有意義なものにすることをオススメします。

Combine × propertyWrapperでイベント発行・購読するオブジェクトの定義をまとめる

Swift 5.2

Combineでは継続的にイベント発行するオブジェクト PassthroughSubjectを活用することが多いが、このオブジェクトを公開するとイベントの購読のみならず外部からのイベント発行も可能になってしまう。これを回避するためにPassthroughSubjectをAnyPublisherに変換して、イベント購読のみ可能なオブジェクトを用意する必要がある。

import Combine

struct OrdinaryCombine {
 
    let subject: AnyPublisher<String, Never>
    private let _subject: PassthroughSubject<String, Never>

    init() {
        self.subject = _subject.eraseToAnyPublisher()
    }
  
    func print() {
        _subject.accept("Hello, world")
    }
}

var cancellables: [AnyCancellable] = []

let combine = OrdinaryCombine()

combine.subject
    .sink { value in
        print(value) // Hello, world
    }
    .store(in: &cancellables)

combine.print()

PassthroughSubject x AnyPublisherこの方法だと毎回2つのプロパティを定義しなくてはならず、また関連がコンストラクタでのみ記載されており、命名を揃えることで関連を明確にして可読性を向上するなどの工夫が求められレビューコストが上がってしまう。

RelayWrapper (Combine × propertyWrapper)

import Combine

public typealias PublishWrapper<T> = RelayWrapper<AnyPublisher<T, Never>, T>

@propertyWrapper
public struct RelayWrapper<Wrapped, Element> {

    public let wrappedValue: Wrapped
    public let accept: (Element) -> Void

    init(wrapped: Wrapped, accept: @escaping (Element) -> Void) {
        self.wrappedValue = wrapped
        self.accept = accept
    }
}

public extension RelayWrapper where Wrapped == AnyPublisher<Element, Never> {
    init() {
        let relay = PassthroughSubject<Element, Never>()
        self.init(wrapped: relay.eraseToAnyPublisher(), accept: { relay.send($0) })
    }
}

RelayWrapper (Combine × propertyWrapper)

propertyWrapperを活用して定義したRelayWrapperを活用することで、内部的にPassthroughSubjectを保持して、外部にはAnyPublisherのみを公開することが可能になる。内部で保持しているPassthroughSubjectはRelayWrapper型のプロパティが宣言されているクラスのみで参照可能なので、外部からのイベント発行は出来ない。

import Combine

struct AdvancedCombine {

    @PublishWrapper()
    var subject: AnyPublisher<String, Never>

    func print() {
        _subject.accept("Hello, world")
    }
}

var cancellables: [AnyCancellable] = []

let combine = AdvancedCombine()

combine.subject
    .sink { value in
        print(value) // Hello, world
    }
    .store(in: &cancellables)

combine.print()

RelayWrapperの活用AdvancedCombine内では_subject: RelayWrapper<AnyPublisher<String, Never>, String>にアクセスしてイベント発行・購読が出来るが、AdvancedCombine外ではsubject: AnyPublisher<String, Never>のみしかアクセスできずイベント購読のみしか出来ない。また、イベント発行とイベント購読のオブジェクトの命名がずれずにレビューなどで可読性を担保する必要がなくなる。


(おまけ) RxRelay + RxSwiftへの応用

RelayWrapperをPublishRelay → Observable, BehaviorRelay → RxPropertyにも応用することが出来る。

import RxRelay
import RxSwift

public typealias PublishWrapper<T> = RelayWrapper<Observable<T>, T>
public typealias BehaviorWrapper<T> = RelayWrapper<Property<T>, T>

@propertyWrapper
public struct RelayWrapper<Wrapped, Element> {

    public let wrappedValue: Wrapped

    public let accept: (Element) -> Void

    init(wrapped: Wrapped, accept: @escaping (Element) -> Void) {
        self.wrappedValue = wrapped
        self.accept = accept
    }
}

public extension RelayWrapper where Wrapped == Observable<Element> {
    init() {
        let relay = PublishRelay<Element>()
        self.init(wrapped: relay.asObservable(), accept: { relay.accept($0) })
    }
}

public extension RelayWrapper where Wrapped == Property<Element> {
    init(value: Element) {
        let relay = BehaviorRelay(value: value)
        self.init(wrapped: Property(relay), accept: { relay.accept($0) })
    }
}

型消去を用いて複数の型を含んだ配列をDecodeする

[
    {
        "type": "Cat",
        "name": "Tama"
    },
    {
        "type": "Dog",
        "name": "Pochi",
        "doghouse": "Outside"
    }
]

複数の型を含んだ配列 (Heterogeneous Collection)上のJSONには"animals"のKeyに対して、 Cat型とDog型の複数の型が含んだ配列が対応しています。このような配列は「寄せ集め = Haterogeneous Collection」と呼ばれます。Haterogeneous CollectionはSwiftのコンパイラが出力するエラーメッセージでも確認することができます。

f:id:nonchalant0303:20211027180150p:plain
Haterogeneous Collectionを含んだエラーメッセージ

Haterogeneous Collectionを含んだエラーメッセージこのような配列をSwiftで扱う場合はいくつかのアプローチがあります。1つはエラーメッセージに提案されているように、[Any]型として配列を扱う方法です。しかし、この方法だと配列に含まれる型を制限できず、扱いに困るケースが考えられます。

そこで、よく使われる方法がProtocolを用いて型を制限する方法です。Cat型とDog型の共通要素をAnimalというProtocolを定義して、[Animal]型として扱う方法です。この方法だと、配列にはAnimalのProtocolに準拠している型しか含めることができず、扱う型をコントロールできます。

protocol Animal {
    var name: String { get }
}

struct Cat: Animal, Decodable {
    let name: String
}

struct Dog: Animal, Decodable {
    let name: String
    let doghouse: String
}

let cat = Cat(name: "Tama")
let dog = Dog(name: "Pochi", doghouse: "Outside")

let animals: [Animal] = [cat, dog]

このようにProtocolを用いて配列を扱うことにより、日常的にHatenegeous Collectionを回避していることが分かります。では、本題のどのようにDecodeして、このような配列を生成するかという話に入ります。

1. 全ての型のプロパティを含んだDecode用の型を用いる

let data: [Any] = [
    [
        "type": "Cat",
        "name": "Tama"
    ],
    [
        "type": "Dog",
        "name": "Pochi",
        "doghouse": "Outside"
    ]
]

struct AnimalDecode: Decodable {
    let name: String
    let doghouse: String?

    private let type: AnimalType

    private enum AnimalType: String, Decodable {
        case cat = "Cat"
        case dog = "Dog"
    }

    func convertTo() -> Animal? {
        switch self.type {
        case .cat:
            return Cat(name: name)
        case .dog:
            guard let doghouse = doghouse else {
                return nil
            }

            return Dog(name: name, doghouse: doghouse)
        }
    }
}

let json = try JSONSerialization.data(withJSONObject: data)
let animals = try JSONDecoder().decode([AnimalDecode].self, from: json).compactMap { $0.convertTo() }

AnimalDecode型はAnimalプロトコルで定義されているプロパティと型を特定するためのtypeプロパティをRequiredなプロパティ、Cat型とDog型の個別のプロパティをOptionalなプロパティとして定義しているDecode用の型です。

一旦、配列の各要素をAnimalDecode型としてDecodeして、その後 convertTo メソッドでAnimalに準拠した型に変換することにより、[Animal]型の配列を取得します。

しかし、この方法はAnimalDecode型が肥大する傾向があるという問題があります。Animalに準拠する型が増えれば増えるほど、AnimalDecode型のOptinalプロパティが増えると共に convertTo メソッドのSwitch文が肥大化してしまいます。

2. 型消去されたDecode用の型を用いる

struct AnyAnimal: Decodable {
    let animal: Animal?

    private enum AnimalType: String, Decodable {
        case cat = "Cat"
        case dog = "Dog"
    }

    private enum Discriminator: String, CodingKey {
        case type
    }

    init(from decoder: Decoder) throws {
        let typeContainer = try decoder.container(keyedBy: Discriminator.self)
        let animalContainer = try decoder.singleValueContainer()

        guard let type = try? typeContainer.decode(AnimalType.self, forKey: .type) else {
            self.animal = nil
            return
        }

        switch type {
        case .cat:
            self.animal = try? animalContainer.decode(Cat.self)
        case .dog:
            self.animal = try? animalContainer.decode(Dog.self)
        }
    }
}

let json = try JSONSerialization.data(withJSONObject: data)
let animals = try JSONDecoder().decode([AnyAnimal].self, from: json).compactMap { $0.animal }

AnyAnimal型はOptional型のプロパティだけ定義されている型です。Decodableに準拠していて、カスタムなinit(from decoder: Decoder)メソッドが定義されています。そのメソッド内で、最初にAnimalType型のプロパティのみをDecodeして、switch文でそれぞれの型に対応したAnimalプロトコルに準拠した型をDecodeします。

この方法はそれぞれの型の生成処理をDecoderに任せることで、Switch文をシンプルに書くことができ、1の方法と違ってAnimalに準拠する型が増えてもSwitch文がそれほど増えることがありません。また、Switch文で同じ型の違う名前のプロパティを入れ間違えるリスクもありません。


そもそも、複数の型を含む配列をDecodeしないといけないケースが稀で、他のレイヤーで解決できるならそうすべきだと思います。ただ、外部サービスのレスポンスなどで自分がコントロールできないケースもあるので、その際はせめてメンテナンスしやすい型消去されたDecode用の型を用いるのがよいかと思います。

Interface Builderでの生成時のClassがレイアウトに与える影響について

f:id:nonchalant0303:20211027175016p:plain

Interface Builder

XcodeのInterface Builder (IB)エディタはMacOS, iOSアプリ開発でレイアウトを組み際に直感的に操作できるレイアウト生成ツールです。

コードからデザインに関する処理を分離することができるので、活用している人も多いと思います。

今回はIBでデザインを組む際に、Object生成時のClassが与える影響について書いていきます。

f:id:nonchalant0303:20211027175113p:plain
Object生成時のClass

サンプルコード

今回の説明に使うコードは全て以下のレポジトリに含まれています。

github.com

Object生成時のClassが与える影響

f:id:nonchalant0303:20211027175331p:plain
Object生成時のクラスが異なる2画面

Object生成時のクラスが異なる2画面上の2画面はクラス名などを除いてコードは全て同じもので実行しています。しかし、左の画面はCellの背景色がありますが、右の画面ではCellの背景色がありません。

// 背景色がついているCell
class ContentTableViewCell: UITableViewCell {
    @IBOutlet private(set) weak var label: UILabel!
}

// 背景色がついていないCell
class NonContentTableViewCell: UITableViewCell {
    @IBOutlet private(set) weak var label: UILabel!
}

IB上で違う点はそれぞれのTableViewで使用しているCellのデザインを組んでいるxibでのObject生成時のクラスが異なることです。

正しく背景色が表示されている左の画面ではObject生成時のクラス(ContentTableViewCell)をUITableViewCellにしており、背景色が表示されていない右の画面ではObject生成時のクラス(NonContentTableViewCell)をUIViewにしています。

f:id:nonchalant0303:20211027175439p:plain

Object生成時のクラスによってIB上でUITableViewCellのデフォルトプロパティのContent Viewの有無が違います。

developer.apple.com

しかし、コード上ではどちらもUITableViewCellを継承しているので、コンパイルエラーがなく参照できます

ただ、Debug View Hierarchyで確認するとNonContentTableViewCellにはContentViewが存在しません。

f:id:nonchalant0303:20211027175555p:plain

この2種類のCellのObject情報を確認するとopaque = NO;という要素が異なります。

print(cell.contentView) // ContentTableViewCell

<UITableViewCellContentView: 0x7f8a11414ff0; frame = (0 0; 414 43.6667); opaque = NO; gestureRecognizers = <NSArray: 0x6000004344b0>; layer = <CALayer: 0x600000a1eac0>>

---

print(cell.contentView) // NonContentTableViewCell

<UITableViewCellContentView: 0x7f8a1150ebd0; frame = (0 0; 414 43.6667); gestureRecognizers = <NSArray: 0x60000043cb10>; layer = <CALayer: 0x600000a02b60>>

opaque = NO;

opaque要素はisOpaqueプロパティで確認できます。

developer.apple.com

If set to true, the drawing system treats the view as fully opaque, which allows the drawing system to optimize some drawing operations and improve performance.

isOpaqueはtrueになっているときはdrawing systemが描画しないので、Debug View Hierarchy上で表示されません。

The default value of this property is true.

NonContentTableViewCellではcontentViewがIB上では存在しないので、UITableViewCellのデフォルトプロパティのcontentViewがレイアウトされずにisOpaqueでデフォルト値のtrueのままになっていると推測できます。

xmlの違い

ContentTableViewCell.xibとNonContentTableViewCell.xibのxmlでは、Object生成時のClass情報が保持されています。

また、ContentTableViewCellではtableViewCellContentViewが存在しますが、NonContentTableViewCellではsubviewsになっています。

https://github.com/Nonchalant/IBInitialClassTrap/blob/a4a65a5bc0bb4ee3f1a5eaee1c089fde2401bfd1/IBInitialClassTrap/ContentView/ContentTableViewCell.xib#L14

https://github.com/Nonchalant/IBInitialClassTrap/blob/a4a65a5bc0bb4ee3f1a5eaee1c089fde2401bfd1/IBInitialClassTrap/Non-ContentView/NonContentTableViewCell.xib#L14

解決策

isOpaqueはSetterが提供されているプロパティなので、drawRectメソッド内でisOpaque = falseにすると解決できると考えたのですが、system-provided classesでは作用しないと公式ドキュメントに書いていました。

You only need to set a value for the opaque property in subclasses of UIView that draw their own content using the drawRect: method. The opaque property has no effect in system-provided classes such as UIButton, UILabel, UITableViewCell, and so on.

ですので、xib側でObjectをUITableViewCellを選択して作り直すことで解決しました。

最後に

この現象は元々StackViewで組んでいたViewをUITableViewに置き換える際に、SubviewをUIViewからUITableViewCellに変更する作業で発見しました。

コード上では継承元のクラスをUIViewからUITableViewCellに変更したのですが、xibはそのまま使用していました。

今回はUIView → UITableViewCellの変換を例に挙げて説明をしましたが、他のクラス間の変換でも同様の現象が発生しますので、クラスを変更する場合はIB側の確認もする必要があります。

個人的にはIB上でCustom Classを特定Classを継承しているClassを適用したら、Constraintsの情報を保持したままIB上でサポートしているClassに変換されてほしいので、Bug Reporterから意見として送信しました。

あまり期待はしていないですが、反応があったらTwitterでお知らせします。

twitter.com

Go beginner builds Slack Bot

From Go Installation

Target of this article

  • Go beginner
  • Would like to make a Slack Bot with Go.

My Achivement

f:id:nonchalant0303:20211027174217p:plain

Slack Bot displays information on connected iOS devices. Once connected, information of the device holds locally, so it can also display information on unconnected devices.


Preparation

Go

Go can be installed by homebrew. You should set a path to handle Go libraries.

$ brew install go
$ export PATH=${HOME}/go/bin:${PATH} or export PATH=${GOPATH}/bin:${PATH}

Go Command Line Tools (CLI) and Libraries

$ go get -u github.com/spf13/cobra
$ go get -u github.com/nlopes/slack

spf13/cobra

  • Create a template of Go Command Line Tools
  • Used by develop of Docker (distribution) and Kubernetes
  • Supports arguments and subcommand.

github.com

nlopes/slack

  • Handles Slack API with Go
  • Supports to watch message event and Interactive message

github.com

Slack Bot

You should create Slack Bot and note API Token. Bot enables to respond messages on Slack by this token.


Builds Slack Bot

Create project

Project is created by installed cobra. Firstly, prepare the cobra configuration file.

$ cat 'author: <YOUR_NAME>' > ~/.cobra.yaml
$ cat 'license: MIT' > ~/.cobra.yaml

Next, creates project.

$ cobra init <YOUR_APPLICATION_NAME>
$ cobra add run // Add subcommand

Let's run it to a trial!

$ cd ~/go/src/github.com/<YOUR_NAME>/<YOUR_APPLICATION_NAME>
$ <YOUR_APPLICATION_NAME> run
run called

If you got run called, you created project successfully!🎉

Watch messages on Slack

Edit implements of run. Let's open cmd/run.go

var runCmd = &cobra.Command{
        Use:   "hoge",
        Short: "...",
        Long: `...`,
        Run: func(cmd *cobra.Command, args []string) {
                fmt.Println("run called")
        },
}

Inside of Run function is called when calls run. You will write it here.

import (
 "github.com/nlopes/slack"
 "github.com/spf13/cobra"
)

var runCmd = &cobra.Command{
        Use:   "hoge",
        Short: "...",
        Long: `...`,
        Run: func(cmd *cobra.Command, args []string) {
                api := slack.New(<YOUR_SLACK_API_TOKEN>)
                os.Exit(run(api))
        },
}

func run(api *slack.Client) int {
    rtm := api.NewRTM()
    go rtm.ManageConnection()
    
    for {
        fmt.Println(rtm.IncomingEvents)
        
        select {
            case msg := <-rtm.IncomingEvents:
            switch ev := msg.Data.(type) {
            case *slack.MessageEvent:
                fmt.Printf("Message: %v\n", ev)
            }
        }
    }
}

You should replace <YOUR_SLACK_API_TOKEN> with the noted API Token.

Real Time Messaging API (RTM API) watchs messages on Slack. When post messages to joined channels, slack.MessageEvent is called and outputs info of the messages on console.

Change fmt.Printf("Message: %v\n", ev) and let Bot posts messages to Slack.

func run(api *slack.Client) int {
    rtm := api.NewRTM()
    go rtm.ManageConnection()
    
    for {
        fmt.Println(rtm.IncomingEvents)
        
        select {
            case msg := <-rtm.IncomingEvents:
            switch ev := msg.Data.(type) {
            case *slack.MessageEvent:
                rtm.SendMessage(rtm.NewOutgoingMessage(ev.text, ev.Channel))
            }
        }
    }
}

fmt.Message is changed to rtm.SendMessage. This change enable bot to post same messages posted to Slack.

You can easily make a Slack Bot with Go. Please, try it!

Nonchalant/kikanbo

Code of Introduced Slack Bot is open source. Please also refer to this as well.

github.com


Notes

You can make easy to manage Slack API Token by using joho/godotenv.

Reference

http://blog.kaneshin.co/entry/2016/12/03/162653

Swiftで複数フラグの管理にOptionSetを使うと便利だった

Swiftで複数のフラグを管理するためにOptionSetを使うと便利だった サービスを作っているとき、ユーザーの状態(e.g. 課金)によって特定の機能が解放されていることがあります。それを管理するにはフラグのような仕組みを使って管理するのがよくある手段なのですが、それが複数になってしまうと管理のコストが高くなってしまいます。その複数のフラグを管理するためにOptionSetを使うと便利でした。

Boolによるフラグ管理

struct Permission {
    let isAllownA: Bool // Aという機能が開放されているかのフラグ
    let isAllownB: Bool // Bという機能が開放されているかのフラグ
    let isAllownC: Bool // Cという機能が開放されているかのフラグ
    let isAllownD: Bool // Dという機能が開放されているかのフラグ
    let isAllownE: Bool // Eという機能が開放されているかのフラグ
    let isAllownF: Bool // Fという機能が開放されているかのフラグ
}

管理するフラグが多くなってくるとツライですね…

OptionSetを用いたフラグ管理

struct Permission: OptionSet {
    let rawValue: Int

    static let A = Permission(rawValue: 1 << 0)
    static let B = Permission(rawValue: 1 << 1)
    static let C = Permission(rawValue: 1 << 2)
    static let D = Permission(rawValue: 1 << 3)
    static let E = Permission(rawValue: 1 << 4)
    static let F = Permission(rawValue: 1 << 5)

    init(rawValue: Int) {
        self.rawValue = rawValue
    }

    init(rawValues: [Int]) {
        self.rawValue = rawValues.reduce(0) { $0 + (1 << $1) }
    }
}

今回のOptionSetを用いたフラグの管理はビット列を用いています。各機能のフラグは各ビットのフラグが立っているか見れば分かります。Aという機能が開放されているかどうかは1ビット目を見れば分かり、同様にBという機能が開放されているかは2ビット目を見れば分かります。C ~ Fも同様です。

以下のように、[1, 3, 5]というArray<Int>はB, D, Fという機能が開放されていることを示しており、それをPermissionのコンストラクタで与えています。生成されたPermissionの.contains(member: Permission)メソッドに知りたい機能を与えてあげるとビットが立っているかどうかを返してくれます。

// 101010みたいなイメージ
let permission = Permission(rawValue: [1, 3, 5])
permission.contains(.A) // false
permission.contains(.B) // true
permission.contains(.C) // false
permission.contains(.D) // true
permission.contains(.E) // false
permission.contains(.F) // true

OptionSetによるフラグ管理の良い点

同値判定が楽に書ける

例えば、ユーザーがある行動をした際に特定の機能が開放されたかどうかのテストを書くとき権限の同値判定を行いたいケースを考えます。その際にPermissionオブジェクト同士を比較する際に使う==()の処理がOptionSetを用いたPermissionでは簡単に書けます。

// Boolを用いたフラグ管理のEquatable
extension Permission: Equatable {
    static func == (lhs: Permission, rhs: Permission) -> Bool {
        return lhs.isAllowedA == rhs.isAllowedA
            && lhs.isAllowedB == rhs.isAllowedB
            && lhs.isAllowedC == rhs.isAllowedC
            && lhs.isAllowedD == rhs.isAllowedD
            && lhs.isAllowedE == rhs.isAllowedE
            && lhs.isAllowedF == rhs.isAllowedF
    }
}

// OptionSetを用いたフラグ管理のEquatable
extension Permission: Equatable {
    static func == (lhs: Permission, rhs: Permission) -> Bool {
        return lhs.rawValue == rhs.rawValue
    }
}

Boolを用いたフラグ管理の同値判定はそれぞれのプロパティの等式を比較して、それのAND条件を評価します。プロパティがすべてBoolなので同じプロパティ同士を比べているかどうかは型的には解決できず、新しいフラグが追加されたときは==()の処理を更新しなければなりません。  それに比べて、OptionSetを用いたフラグ管理の同値判定はrawValueの等式を評価すればよいだけで、これは管理するべきフラグが増えた際も常に同様です。

OptionSetによるフラグ管理の悪い点

Permissionへのマッピングが大変

クライアント内でフラグ管理が完結しているとそこまで問題はないと思うのですが、APIのレスポンスなどでフラグの状態を取得する際はその値をPermissionにオブジェクトにマッピングするのが大変です。 以下のようなレスポンスで返ってくると、結局Aがtrueなら1, Bがtrueなら2のようなマッピングをしなくてはなりません。これは結局、複数の方法で管理するのでそのマッピング部分のコストが高くなってしまいます。

{
    "A": true,
    "B": false,
    "C": true,
    "D": false,
    "E": true,
    "F": true
}

APIのレスポンスがArray<Int>のような形であればOptionSetを用いたフラグ管理を考えてみる価値はあると思います。

[
    0,
    2,
    4,
    5
]

参考

https://developer.apple.com/reference/swift/optionset