web-dev-qa-db-ja.com

Swift内部関数とプロパティを備えたパブリックプロトコル

プロトコルを操作するときに、一部の関数をパブリックにし、一部を内部に公開する場合のベストプラクティスは何でしょうか。

フレームワークとしてAVPlayerをラップする AudioManager inSwift 3を書いています。

いくつかのメソッドを公開したいので、たとえばAudioManagerを利用するViewControllerは一部のメソッドにアクセスできますが、一部のメソッドはフレームワークの外部に公開されません。
->つまり、internalの代わりにアクセス修飾子publicを使用します。

私はプロトコル駆動型の設計でフレームワークを書いています。ほとんどすべての部分にプロトコルが必要です。
したがって、プロトコルはフレームワーク内のプロトコルと通信しています。
例えば。メインクラス--AudioManager--にはAudioPlayerがあり、その上でいくつかのinternal関数を呼び出すことができるはずです。
例えば。 pause(reason:)ですが、そのメソッドはinternalであり、フレームワークの外部に公開されるべきではありません。

これが例です。

internal enum PauseReason {
    case byUser
    case routeChange
}

// Compilation error: `Public protocol cannot refine an internal protocol`
public protocol AudioPlayerProtocol: InternalAudioPlayerProtocol { 
   func pause() // I want 
}

internal protocol InternalAudioPlayerProtocol {
    func pause(reason: PauseReason) // Should only be accessible within the framework
}

public class AudioPlayer: AudioPlayerProtocol {
    public func pause() {
        pause(reason: .byUser)
    }

    // This would probably not compile because it is inside a public class...
    internal func pause(reason: PauseReason) { //I want this to be internal
        // save reason and to stuff with it later on
    }
}

public protocol AudioManagerProtocol {
    var audioPlayer: AudioPlayerProtocol { get }
}

public class AudioManager: AudioManagerProtocol {
    public let audioPlayer: AudioPlayerProtocol

    init() {
        audioPlayer = AudioPlayer()
        NotificationCenter.default.addObserver(self, selector: #selector(handleRouteChange(_:)), name: NSNotification.Name.AVAudioSessionRouteChange, object: nil)
    }

    func handleRouteChange(_ notification: Notification) {
        guard
        let userInfo = notification.userInfo,
        let reasonRaw = userInfo[AVAudioSessionRouteChangeReasonKey] as? NSNumber,
        let reason = AVAudioSessionRouteChangeReason(rawValue: reasonRaw.uintValue)
        else { print("what could not get route change") }
        switch reason {
        case .oldDeviceUnavailable:
            pauseBecauseOfRouteChange()
        default:
            break
        }
    }
}

private extension AudioManager {
    func pauseBecauseOfRouteChange() {
        audioPlayer.pause(reason: .routeChange)
    }
}

// Outside of Audio framework
class PlayerViewController: UIViewController {
    fileprivate let audioManager: AudioManagerProtocol 
    @IBAction didPressPauseButton(_ sender: UIButton) {
        // I want the `user of the Audio framwwork` (in this case a ViewController)
        // to only be able to `see` `pause()` and not `pause(reason:)` 
        audioManager.audioPlayer.pause()
    }
}

メソッドpauseBecauseOfRouteChangeを次のように変更することで機能させることができます:

func pauseBecauseOfRouteChange() {
    guard let internalPlayer = audioPlayer as? InternalAudioPlayerProtocol else { return }
    internalPlayer.pause(reason: .routeChange)
}

しかし、もっとエレガントな解決策があるかどうか疑問に思っていますか?
AudioPlayerProtocolInternalAudioPlayerProtocol ..を洗練することをマークするようなもの.

または、仲間のプログラマーはどのようにそれを行いますか?
内部使用を目的としたメソッドと変数を公開しない場合、フレームワークはより美しくなります。

ありがとう!

12
Sajjon

いいえ、少なくともプロトコルを検討する場合、これに対するより洗練された解決策はありません。その理由は次のとおりです。

フレームワークを使用している誰かがAudioPlayerProtocolの拡張機能を書きたいというシナリオを想像してみてください。それが内部の場合、pause(reason:)メソッドをどのように実装できますか?

サブクラス化するだけでそれを実現でき、このコードは実際にコンパイルされます。

public class AudioPlayer: AudioPlayerProtocol {
    public func pause() {
        pause(reason: .byUser)
    }

    internal func pause(reason: PauseReason) {
    }
}

プロトコルの場合、これは当てはまりません。パブリックアクセスレベルの誰かがパブリック/内部の混合プロトコルを使用したい場合、内部機能の実装を保証できないためです。

2
Wladek Surala

これは古いトピックですが、実際にできることは逆です。 internalProtocolを拡張するpublicProtocolの代わりに、publicProtocolを拡張するinternalProtocolがあります。

public protocol AudioPlayerProtocol { 
   func pause() // I want 
}

internal protocol InternalAudioPlayerProtocol: AudioPlayerProtocol {
    func pause(reason: PauseReason) // Should only be accessible within the framework
}

public class AudioPlayer: InternalAudioPlayerProtocol {
    public func pause() {
        pause(reason: .byUser)
    }

    internal func pause(reason: PauseReason) { 
        //Do stuff
    }
}

それからマネージャーで

public class AudioManager: AudioManagerProtocol {
    public let audioPlayer: AudioPlayerProtocol
    private let intAudioPlayer: InternalAudioPlayerProtocol

    init() {
        intAudioPlayer = AudioPlayer()
        audioPlayer = intAudioPlayer
        ...
    }
    ...
    private func pauseBecauseOfRouteChange() {
        intAudioPlayer.pause(reason: .routeChange)
    }
}
1
La pieuvre

プロトコルを内部実装とパブリックに分割してから、パブリック実装クラスを内部実装に委任する場合はどうでしょうか。そのようです

internal protocol InternalAudioPlayerProtocol {
    func pause(reason: PauseReason) 
}

public protocol AudioPlayerProtocol {
    func pause()
}

internal class InternalAudioPlayer: InternalAudioPlayerProtocol {
    internal func pause(reason: PauseReason) { 
    }
}

public class AudioPlayer: AudioPlayerProtocol  {
    internal var base: InternalAudioPlayerProtocol

    internal init(base: InternalAudioPlayerProtocol) {
        self.base = base
    }

    public func pause() {
        base.pause(reason: .byUser)
    }
}

public protocol AudioManagerProtocol {
    var audioPlayer: AudioPlayerProtocol { get }
}

public class AudioManager: AudioManagerProtocol {
    internal let base = InternalAudioPlayer()
    public let audioPlayer: AudioPlayerProtocol

    public init() {
        audioPlayer = AudioPlayer(base: base)
    }

    internal func handleSomeNotification() {            
        pauseBecauseOfRouteChange() //amongst other things
    }

    internal func pauseBecauseOfRouteChange() {
        base.pause(reason: .routeChange)
    }
}
1
user3763801