Swiftで実装提供用のプロトコルを継承する際にそのためのrequirementsが意図せず露出する問題を回避するための実装パターン

# 概要

Swiftである型`C`があるプロトコル`P`にconformするとき、`P`のrequirementsを与える`C`のメンバの可視性は、`C`自身の可視性と同じかより緩くなければなりません。この仕様は、プロトコル`P`の実装を提供するプロトコル`PBase`などを利用する際に、期待しない過剰な実装の露出を引き起こす場合があります。この記事では、そのような場合に実装を再利用する利便性を維持しつつも可視性の過剰な露出を回避する実装パターンを紹介します。

# 導入: 実装提供プロトコル継承パターンの紹介

以降では、下記のサンプルコードを元に説明します。まず、下記のように、あるプロトコル`MediaSink`があったとします。

```swift
public protocol MediaSink: AnyObject {
    associatedtype InputMedia

    func didConnect<X: MediaSourceConnection>(connection: X)

    func close()

    func send<X: MediaSourceConnection>(event: MediaEvent<InputMedia>,
                                        connection: X) -> Bool
}
```

このプロトコルは、とあるメディアストリーム処理のライブラリに登場します。このライブラリのコンセプトは、プルベースの非同期ストリーム処理です。Sourceが送信側、Sinkが受信側、SourceとSinkの接続がそれぞれに対応するConnectionオブジェクトで表現されます。例えば、SinkにはSource側を表すSourceConnectionが与えられます。SourceとSinkはこのConnectionオブジェクトを介して通信を行います。MediaEventは`next`, `complete`, `error`の3種類のcaseを持つenumです。`MediaSink`のメソッドはそれぞれ、`didConnect`が確立した接続の通知、`close`が接続の切断、`send`がSource側から見たイベントの送信(Sink側から見たイベントの受信)を意味します。

このライブラリにおいて、Sinkを実装する際には、そのSinkに対する操作をスレッドセーフにすること、確立した接続を保持すること、その接続に対して機能を呼び出すこと、流れてきたイベントと接続の対応をチェックすることなど、決まった作法があります。このような決まった作法がある場合には、個別の実装においてその作法を守るために実装を共通化したくなります。そこで、プロトコルとプロトコルエクステンションを用いて、共通の実装と個別の実装部分を与えることにします。それが下記の`MediaSinkBase`です。

```swift
public protocol MediaSinkBase: MediaSink {
    var queue: DispatchQueue { get }

    var _sourceConnection: WeakAnyMediaSourceConnection { get set }

    func _send(event: MediaEvent<InputMedia>,
               readyAfterDone: inout Bool)
}

extension MediaSinkBase {
    public func didConnect<X: MediaSourceConnection>(connection: X) {
        queue.sync {
            // (略)
        }
    }

    public func close() {
        queue.syncAndRunAfter {
            // (略)
        }
    }

    public func send<X: MediaSourceConnection>(event: MediaEvent<InputMedia>,
                                               connection: X) -> Bool
    {
        return queue.sync {
            // (略)
        }
    }
}
```

`MediaSink`を実装したい型があるとき、この`MediaSinkBase`を継承することによって、`queue`, `_sourceConnection`, `_send`を実装するだけで、スレッドセーフな`MediaSink`の実装が得られるようになります。

下記の`VideoDisplaySink`は、`MediaSinkBase`を継承して`MediaSink`を実装した例です。

```swift
public final class VideoDisplaySink<Frame: VideoFrameProtocol>:
    MediaSinkBase
{
    public typealias InputMedia = Frame

    public let queue = DispatchQueue(label: "VideoDisplaySink.queue")
    public var _sourceConnection = WeakAnyMediaSourceConnection()
    public func _send(event: MediaEvent<Frame>, readyAfterDone: inout Bool) {
        // (略)
    }

    // (略)
}
```

このパターンは複雑なプロトコルに共通の実装を与えたい場合に便利です。実際のコーディングにおいては、プロコトルの継承まで記述してからコンパイラのAutoFixを利用することにより、自分で実装しなければならないメンバの宣言が自動的にソースに入力され、後はその中身だけコーディングすれば作業が完了します。ここで型検査とAutoFixが効くので、ミスする可能性を低くできます。このパターンを便宜上、実装提供プロトコル継承パターンと呼ぶことにします。

# 動機: 実装提供プロトコル継承パターンの問題

上述したとおり、実装提供プロトコル継承パターンは便利ですが、問題があります。それは、この実装提供用のプロトコルのrequirementsに対応するメンバの可視性が、その型自身の可視性と同じかより緩くなければならないことです。`VideoDisplaySink`の例においては、`queue`, `_sourceConnection`, `_send`の可視性が`public`になっている点のことです。もしこれらの可視性を`internal`や`private`に制限した場合、コンパイルエラーとなり、露出する事が強制されています。このような露出はバグの原因となり得ます。例えば`VideoDisplaySink`においては、`queue`にアクセスする事ができるため、間違って使用すればデッドロックが生じます。他にも、本来`queue`で同期する事を想定している`_sourceConnection`や`_send`に直接アクセスする事によって、マルチスレッドバグが生じえます。この記事では、このような過剰露出の問題を回避する実装パターンを紹介します。

# 途中解: 実装内部化の手動実装

この問題の解決策として、実装を提供するプロトコルをインナータイプで継承することで、余計な露出を隠蔽する方法があります。いわゆる`is-a`から`has-a`への書き換えと言えます。下記に例を示します。

```swift
public final class VideoDisplaySink<Frame: VideoFrameProtocol>:
    MediaSink
{
    public final class Impl : MediaSinkBase {
        public typealias InputMedia = Frame
        
        public weak var wrapper: VideoDisplaySink<Frame>!
        public let queue = DispatchQueue(label: "VideoDisplaySink.queue")
        public var _sourceConnection = WeakAnyMediaSourceConnection()
        private var _time: CMTime
        
        public var time: CMTime {
            get { return queue.sync { _time } }
            set {
                queue.sync {
                    // 略
                }
            }
        }
        
        public func _send(event: MediaEvent<Frame>, readyAfterDone: inout Bool) {
            // 略
        }
    }
    
    public typealias InputMedia = Frame
    
    private let impl: Impl
    
    public init(time: CMTime) {
        impl = Impl(time: time)
        impl.wrapper = self
    }
    
    public func didConnect<X>(connection: X) where X : MediaSourceConnection {
        impl.didConnect(connection: connection)
    }
    
    public func close() {
        impl.close()
    }
    
    public func send<X>(event: MediaEvent<Frame>, connection: X) -> Bool where X : MediaSourceConnection {
        impl.send(event: event, connection: connection)
    }
    
    public var time: CMTime {
        get { return impl.time }
        set { impl.time = newValue }
    }
}
```

上記のコードの構造を解説します。まず、`MediaSinkBase`を継承して`VideoDisplaySink`の実装を与えるクラスを、インナークラスの`Impl`として定義します。そして、外側の`VideoDisplaySink`はその`Impl`のインスタンスを`impl`プロパティで保持するようにします。そして、ただの`MediaSink`を継承します。AutoFixでrequirementsのテンプレートを生成したら、その実装は全て`impl`プロパティに転送するようにします。またこの例では、`VideoDisplaySink`固有の機能である`time`プロパティの実装例も示しています。ポイントとして、この例のように`time`の実装も`Impl`の中で完全に実装して、外側のプロパティは転送するだけにしています。うっかり外側に実装を持ってしまうと、内側と外側の行き来が発生してコードの見通しが悪くなりがちなので避けましょう。さらに、場合によっては外部の関数に自分自身である`VideoDisplaySink`を渡したい場合があります。しかし、`Impl`自身は異なる型なので、`Impl`の`self`は使えません。そこで、外側の`VideoDisplaySink`への参照プロパティの`weak var wrapper: VideoDisplaySink<Frame>!`を定義して、外側の`VideoDisplaySink`で`Impl`を生成した後に`self`を渡しています。この外側と内側の型は常にペアで使用するため、内側から外側への参照の型は弱参照のIUOで良いでしょう。

# 提案: 実装転送用クラス継承パターン

しかし、上記の方法は若干面倒です。プロトコルを実装するたびに`impl`に実装を転送する固定のコードを書かなければなりません。それに、プロトコルのrequirementsの数と同じだけ、この転送も書かなければなりません。そこで今回提案したいのが、実装を転送するための継承用のクラスを用意する方法です。下記にコード例を示します。

```swift
open class MediaSinkInheritanceProxy<Impl: MediaSink> : MediaSink {
    public typealias InputMedia = Impl.InputMedia
    
    private let impl: Impl
    
    public init(impl: Impl) {
        self.impl = impl
    }
    
    open func didConnect<X>(connection: X) where X : MediaSourceConnection {
        impl.didConnect(connection: connection)
    }
    
    open func close() {
        impl.close()
    }
    
    open func send<X>(event: MediaEvent<InputMedia>, connection: X) -> Bool
        where X : MediaSourceConnection
    {
        impl.send(event: event, connection: connection)
    }
}
```

`MediaSinkInheritanceProxy`は、型パラメータ`Impl`の型のインスタンスを`init`で受け取って`impl`プロパティとして保持し、自身の`MediaSink`の実装はその`impl`に転送するだけのクラスです。別モジュールからでもプロトコルとセットで再利用できるように可視性は`open`にしてあります。このクラスを継承することで、`VideoDisplaySink`の実装において転送のコードを書く必要がなくなります。下記に例を示します。

```swift
public final class VideoDisplaySink<Frame: VideoFrameProtocol>:
    MediaSinkInheritanceProxy<VideoDisplaySink<Frame>.Impl>
{
    public final class Impl : MediaSinkBase {
        public typealias InputMedia = Frame
        
        public weak var wrapper: VideoDisplaySink<Frame>!
        public let queue = DispatchQueue(label: "VideoDisplaySink.queue")
        public var _sourceConnection = WeakAnyMediaSourceConnection()
        private var _time: CMTime
        
        public var time: CMTime {
            get { return queue.sync { _time } }
            set {
                queue.sync {
                    // 略
                }
            }
        }
        
        public func _send(event: MediaEvent<Frame>, readyAfterDone: inout Bool) {
            // 略
        }
    }

    private let impl: Impl
    
    public init(time: CMTime) {
        impl = Impl(time: time)
        super.init(impl: impl)
        impl.wrapper = self
    }

    public var time: CMTime {
        get { return impl.time }
        set { impl.time = newValue }
    }
}
```

このようになります。`VideoDisplaySink`は`MediaSinkInheritanceProxy`を継承しており、自身のインナークラスである`Impl`を型パラメータとして渡しています。ここで、`<Impl>`と書けたら嬉しいのですが、駄目みたいなので完全名で`<VideoDisplaySink<Frame>.Impl>`と書いています。`init`において`super.init(impl:)`の呼び出しが追加されていますが、その代わりに全てのプロトコルrequirementsのメンバ実装が省略できました。`associatedtype`である`InputMedia`についての`typealias`も親から継承しているので不要になっています。このように継承するだけで転送できるクラスを用意することで、プロトコルを実装する型全てで共通の転送実装を利用できるため、先述の方法と比べてかなり楽になります。プロトコルの定義が拡大した場合も、転送用のクラスのみ修正すれば良いので、転送部分の変更が生じません。もちろん、定義が拡大した時は各`Impl`側の修正は必要ですが。このパターンを実装転送用クラス継承パターンと呼ぶことにします。

## Type erasureとの類似性

この実装パターンは[Type erasureの実装パターンの継承box方式](https://qiita.com/omochimetaru/items/5d26b95eb21e022106f0#type-erasure-継承-box-方式)と似ています。構造として、継承box方式における内部boxサブクラスが、型パラメータにeraseしたいコンクリートな型を焼き込んでいるところが同じ仕組みになっています。erasureの場合は元のコンクリートな型を完全に消去するために、型情報の無い親クラスにサブタイピングさせていますが、今回のproxyの場合は別にそこを秘匿する必要はないので型パラメータをそのまま露出しています。検証はしていませんが、こうしておくことで真の型が露出するため最適化が期待できます。

## 類似解: 実装転送用プロトコル継承パターン

上記の方法はクラス継承を利用しているので、同時に複数のプロトコルを実装しつつそれらを転送したい場合には使用することができません。そこで代替案として、転送機能をクラスではなくプロトコルで提供するパターンがあります。下記に例を示します。

```swift
public protocol MediaSinkProxyProtocol : MediaSink {
    associatedtype Impl: MediaSink where Impl.InputMedia == Self.InputMedia
    
    var impl: Impl { get }
}

extension MediaSinkProxyProtocol {
    public func didConnect<X>(connection: X) where X : MediaSourceConnection {
        impl.didConnect(connection: connection)
    }
    
    public func close() {
        impl.close()
    }
    
    public func send<X>(event: MediaEvent<InputMedia>, connection: X) -> Bool where X : MediaSourceConnection {
        impl.send(event: event, connection: connection)
    }
}
```

元の方式では型パラメータ`Impl`とプロパティ`impl`でしたが、`associatedtype Impl`とプロパティ制約`impl`になっています。これを使って`VideoDisplaySink`を実装すると下記のようになります。

```swift
public final class VideoDisplaySink<Frame: VideoFrameProtocol>:
    MediaSinkProtocolProxy
{
    public final class Impl : MediaSinkBase {
        public typealias InputMedia = Frame
        
        public weak var wrapper: VideoDisplaySink<Frame>!
        public let queue = DispatchQueue(label: "VideoDisplaySink.queue")
        public var _sourceConnection = WeakAnyMediaSourceConnection()
        private var _time: CMTime
        
        public var time: CMTime {
            get { return queue.sync { _time } }
            set {
                queue.sync {
                    // 略
                }
            }
        }
        
        public func _send(event: MediaEvent<Frame>, readyAfterDone: inout Bool) {
            // 略
        }
    }

    public typealias InputMedia = Frame

    public let impl: Impl
    
    public init(time: CMTime) {
        impl = Impl(time: time)
        impl.wrapper = self
    }

    public var time: CMTime {
        get { return impl.time }
        set { impl.time = newValue }
    }
}
```

継承する親を書く部分では、型パラメータの指定が不要になるため簡潔になります。また、親クラスが無いため`super.init`の呼び出しも不要です。`associatedtype Impl`の割り当ては、インナークラスの型名と同一名になっているのでそこもこのまま接続できます。一方、`typealias InputMedia`を書く必要があります。総じてこの方式のほうが楽に見えます、しかし、一つ致命的な問題点があります。それは、`public let impl: Impl`プロパティの可視性が`public`になってしまった事です。その理由は実装提供用プロトコルの問題のときと同じです。実装提供用プロトコルのrequirementsが露出する問題を解決したかったはずなのに、`impl`プロパティ経由でそれを呼び出せるようになってしまうので、元々の問題が解決できなくなってしまいます。

ただ、この点は考え方次第で採用しても良いと思います。元々の状態では、実装用のメンバ全てが直接対象の型から露出していたのに対して、あくまで露出が`impl`プロパティ経由だけに限定されたからです。元々の状態ではたとえ「実装用のメンバを触ってはいけない」と理解しているプログラマでもあっても、「外部利用のためのメンバと間違えて使う」ミスが考えられます。これは実装提供用プロトコルの仕様を完全に把握していないと見分けられない事なので、将来的に変化する要素でもあり、人間が気をつけて達成するのは難しいと思います。一方「`impl`プロパティに触ってはいけない」という1つの固定のルールであれば運用上意識して守る事はそんなに難しくないように思います。便宜上こちらには実装転送用プロトコル継承パターンという名前を付けておくので、好みの方を採用すると良いと思います。

# お願い

最後まで読んでくれてありがとうございました。コメントが貰えると嬉しいのでお願いします。

### コメントの例

- もっと良い方法があるので共有します。
- 他の方法があるので共有します。
- 私も同じ方法を使っています。
- 同じ方法のコードをこちらで見かけたので共有します。
- 同じ問題で困っていたので今度使ってみようと思います。
- この問題に出会ったことはないけど今度使ってみようと思います。
- 実装提供用のプロトコルを書いた事が無かったので今度やってみます。
- 実装提供用のプロトコルは使うけど可視性の露出する問題を知りませんでした。
- 最後まで読みました。