web-dev-qa-db-ja.com

インターフェイスの不要な(?)読み取り専用フィールド

依存関係注入のコンテキストの下で-つ​​まり、インターフェースにはほとんど1つの実装があります-私は、インターフェースを介して、コンシューマークラスによって呼び出されることのない一連のフィールドを公開する習慣をつけました。これらのフィールドは、高レベルの実装戦略を反映しています。抽象化がどのように機能するかを理解するのに役立つと思うので、または詳細な実装の観点からではなく、高レベルの原理の観点から、なぜオブジェクト/そもそもインターフェースが必要でした。

たとえば、文字列を暗号化するインターフェイスIEncrypterがあります。このインターフェースの背後にある考え方は、使用する暗号化アルゴリズムを選択できるようにすることです。そのため、実装クラスはコンストラクタでアルゴリズムの抽象化IEncryptionAlgoを受け取り、それをReadOnlyフィールドとして格納します:_ThisEncryptionAlgo As IEncryptionAlgo_。次に、Encrypt(Message)を呼び出すと、ThisEncryptionAlgo.Encrypt(Message)が呼び出されます。

厳密に言うと、インターフェイスはThisEncryptionAlgoを公開する必要はなく、Byte() Encrypt(Message As String)関数を公開するだけでコンシューマは十分です。ただし、インターフェイスにThisEncryptionAlgoを(ReadOnlyとして)公開すると、いくつかの利点があると感じます。

  • 開発者がインターフェースの背後にある精神を理解するのを助けます。これは実装時と消費時の両方に役立ちます。
  • インターフェイスから直接プロパティをすばやく検査できるため、デバッグが容易になります。
  • インターフェイスのプロパティに基づいてレポートを生成すると、エラーのログとトレースが簡単になる場合があります。

具象クラスではなくインターフェースとしてこれを使用する主な目的は、真の抽象レイヤーを追加するのではなく、コンシューマの依存性注入とユニットテストを許可することであるため、問題ないと思います。そうは言っても、インターフェイスは実装の詳細を無視するという原則を無効にします。

あなたの意見は何ですか?インターフェイスからThisEncryptionAlgoを削除する必要がありますか?


完全な例

_Interface IEncryptionAlgo

    Function Encrypt(Input As Byte()) As Byte()

    Function Decrypt(Input As Byte()) As Byte()

End Interface


Interface ICheckSumAlgo

    Function GetHashSum(Input As Byte()) As Byte()

    ReadOnly Property HashLength As Integer

End Interface


Interface IEncrypter

    ReadOnly Property ThisEncryptionAlgo As IEncryptionAlgo

    ReadOnly Property ThisCheckSumAlgo As ICheckSumAlgo

    Function Encrypt(Message As String) As Byte()

    Function Decrypt(Cypher As Byte()) As String

    Function EncryptWihCheckSum(Message As String) As Byte()

    Function DecryptWithCheckSum(SumAndCypher As Byte()) As String

End Interface


Class Encrypter
    Implements IEncrypter


    Private Sub New()
    End Sub

    Sub New(Algo As IEncryptionAlgo, CheckSum As ICheckSumAlgo)
        Me.ThisEncryptionAlgo = Algo
        Me.ThisCheckSumAlgo = CheckSum
    End Sub


    Public ReadOnly Property ThisEncryptionAlgo As IEncryptionAlgo Implements IEncrypter.ThisEncryptionAlgo

    Public ReadOnly Property ThisCheckSumAlgo As ICheckSumAlgo Implements IEncrypter.ThisCheckSumAlgo


    Public Function Encrypt(Message As String) As Byte() Implements IEncrypter.Encrypt

        Dim MessageBytes As Byte() = Text.Encoding.Unicode.GetBytes(Message)

        Dim Cypher As Byte() = Me.ThisEncryptionAlgo.Encrypt(MessageBytes)
        Return Cypher

    End Function

    Public Function Decrypt(Cypher() As Byte) As String Implements IEncrypter.Decrypt

        Dim MessageBytes As Byte() = Me.ThisEncryptionAlgo.Decrypt(Cypher)

        Dim Message As String = Text.Encoding.Unicode.GetString(MessageBytes)
        Return Message

    End Function


    Public Function EncryptWihCheckSum(Message As String) As Byte() Implements IEncrypter.EncryptWihCheckSum

        Dim Cypher As Byte() = Encrypt(Message)
        Dim CypherSum As Byte() = Me.ThisCheckSumAlgo.GetHashSum(Cypher)

        Dim SumAndCypher As Byte() = CypherSum.Concat(Cypher).ToArray
        Return SumAndCypher

    End Function

    Public Function DecryptWithCheckSum(SumAndCypher() As Byte) As String Implements IEncrypter.DecryptWithCheckSum

        Dim Cypher As Byte() = SumAndCypher.Skip(Me.ThisCheckSumAlgo.HashLength).ToArray
        Dim ExpectedCypherSum As Byte() = SumAndCypher.Take(Me.ThisCheckSumAlgo.HashLength).ToArray
        Dim CurrentCypherSum As Byte() = Me.ThisCheckSumAlgo.GetHashSum(Cypher)

        If Not CurrentCypherSum.SequenceEqual(ExpectedCypherSum) Then Throw New ArgumentException("Check sum failed.", NameOf(SumAndCypher))

        Dim Message As String = Decrypt(Cypher)
        Return Message

    End Function


End Class

Class ServicesFactory
    Implements IServicesFactory

    Function NewEncrypter() As IEncrypter Implements IServicesFactory
        Return New Encrypter(My.AppSettings.GetDefaultAlgo, My.AppSettings.GetDefaultCheckSum)
    End Function

End Class


Class ConsummerClass

    Private ReadOnly Property MainFactory As IServicesFactory

    Private Sub New
    End Sub

    Sub New(MainFactory as IServicesFactory)
        Me.MainFactory=MainFactory
    End Sub

    Sub Main()

        Dim MyMessage As String = InputBox("Write something")

        Dim Encrypter As IEncrypter = Me.MainFactory.NewEncrypter
        Dim EncryptedMessage As Byte() = Encrypter.Encrypt(MyMessage)

        WebClient.SendPost(Convert.ToBase64String(EncryptedMessage))

    End Sub

End Class
_

ご覧のように、IEncrypterが使用するアルゴオブジェクトを参照するフィールドを保持しているのは「仕様」です。別のアルゴを使用したい場合は、IEncryptionAlgoを実装し、ServicesFactoryを介して挿入することができます。このような状況では、ThisEncryptionAlgoはコンシューマクラスには必要ありませんが、IEncrypterインターフェースを介して公開することで、後者の実装はアーキテクチャ全体に適合します。少なくとも直感的にはそういう感じですが、これに挑戦したいと思います。

2
Ama

私にとって非常に重要なナレッジマネジメントについて考えているのが好きです。開発チームの生産性を向上させることができれば、アーキテクチャエレガンスの小さな妥協に完全に反対するわけではありません。

そうは言っても、私はあなたの読み取り専用フィールドがクラスについて非常に異なる何かを暗示していると思います。

2つのオプションを見てみましょう。

Interface IEncrypter

    Function Encrypt(Message As String) As Byte()

    Function Decrypt(Cypher As Byte()) As String

    Function EncryptWihCheckSum(Message As String) As Byte()

    Function DecryptWithCheckSum(SumAndCypher As Byte()) As String

End Interface

上記は、文字列を暗号化および復号化できるブラックボックスです。それがアプリケーションに必要なすべてである場合、注入されるインターフェースで指定する必要があるのはそれだけです。

Interface IEncrypter

    ReadOnly Property ThisEncryptionAlgo As IEncryptionAlgo

    ReadOnly Property ThisCheckSumAlgo As ICheckSumAlgo

    Function Encrypt(Message As String) As Byte()

    Function Decrypt(Cypher As Byte()) As String

    Function EncryptWihCheckSum(Message As String) As Byte()

    Function DecryptWithCheckSum(SumAndCypher As Byte()) As String

End Interface

上記は単なる暗号化と復号化ではありません。 ヘルパークラスおよびcontainerアルゴリズムの場合。これは非常に異なる動物です。暗号化機能のすべての機能を提供しますが、その役割は、呼び出し元から実装をhideすることではなく、さらに別のインターフェースの呼び出しを支援することです。

後者は、より多くの設計上の決定にあなたをロックします。たとえば、これはnull暗号化スキーム(アルゴリズムがない)を排除し、複数のアルゴリズムを使用する可能性がある暗号化スキーム(たとえば、公開鍵暗号化を必要とするより長いメッセージに使用するRSA + AES)を排除します。

ヘルパーではなくエンクリプターだけが必要で、デバッグやロギングの目的でアルゴリズムに関するヒントを公開したい場合は、代わりにヒントを文字列として公開することを検討してください。

Interface IEncrypter

    ReadOnly Property EncryptionMethod As String

    Function Encrypt(Message As String) As Byte()

    Function Decrypt(Cypher As Byte()) As String

    Function EncryptWihCheckSum(Message As String) As Byte()

    Function DecryptWithCheckSum(SumAndCypher As Byte()) As String

End Interface

そうすれば、アルゴリズムオブジェクトを1つだけ使用することに縛られることなく、デバッグとナレッジマネジメントのニーズを満たす方法を手に入れることができます。

3
John Wu

はい、削除してください。

クラスを効果的に密結合するアルゴを実装せずに誰かが暗号化機能を実装するのを防ぎます。

単体テストや依存性注入を行うためのインターフェースは必要ありません。 DIと単体テストには抽象化が必要です。

完全なコードの補遺

サービスファクトリも使用するコードも、追加の読み取り専用プロパティを使用しません。必要なのは次のものだけです。

IEncryptor
    byte[] Encrypt(string)

インターフェースをできる限り小さくして、解読を分割するなどと主張する人もいるでしょう。私はそれほど遠くまでは行きませんが、好みの問題です。

ただし、読み取り専用フィールドを呼び出すのは、単体テストでservicefactoryが正しく構築されたオブジェクトを返すことを確認することだけであると思われます。

プロパティとして値を公開せずにこれを行う方法はたくさんありますが、実際に使用するアルゴリズムとは異なるアルゴリズムを公開する暗号化を実装できるため、テストにはまったく役立たないと主張します。 。

これは、単に文字列を暗号化し、その結果を、予想されるアルゴリズムで暗号化された既知の値と比較することで、より適切に達成されます。

8
Ewan

interfaceはメソッドと定数のみを定義するものだと思っていました。もちろん、「読み取り専用フィールド」の場合、インターフェースは定数値を返すメソッドであると想定できます。

インターフェースを定義する場合のインターフェースの要点ですが、合理的な実装を可能にします。

いずれにせよ、合理的な実装で暗号化アルゴリズムへの完全なアクセスが必要な場合は、GetEncryptionAlgo()を公開する(またはそれをパラメーターとしてインターフェースメソッドに渡す)ことはできます。

ThisEncryptionAlgo.Encryptがインターフェイスの目的で厳密に十分である場合、残りの部分を公開することは、私の考えでは適切ではありません。

あなたが「精神」と呼ぶものは、可能な実装のあなたの直感であるようです。これはlimits可能な実装です。例えば。完全に異なる暗号化ライブラリを使用している場合は、慣れている場合、.Encryptのみを必要とするインターフェイスを介して公開するのは簡単で、インターフェイスで困難または不可能です。

皮肉なことに、そのインターフェースを実装するクラスのテストのために暗号化プロバイダーをモックするのはharderになります。

一般に、インターフェイスでメソッドの厳密な最小値のみを公開することをお勧めします。単純であるほど、正しく実装する可能性が低くなります。

さらに公開したい場合は、特定の実装の便利さを備えた最小限のインターフェースを強化する拡張インターフェースを検討してください。

interface Stream<T> {  // Minimal.
  T Read();
  Boolean HasMore();
}

interface SeekableStream<T> : Stream<T> {
  long GetPosition();
  void Seek(long);
}

class File<T> : SeekableStream<T> { // Still acceptable anywhere a Stream goes.
... 
}
5
9000

そのような「追加の」メソッドをインターフェースに追加することについては、私は非常に慎重になるでしょう。

インターフェイスはその責任について非常に明確でなければなりません。

IEncrypterIEncryptionAlgoまたはICheckSumAlgoについてあなたに伝える責任がありますか?私はそれがEncryptDecryptだけであるべきだと思います。余分なメソッドを追加することで、実装により多くの要件/制約を課すことになります。

バイトを通過させるだけの非暗号化IEncrypterが必要な場合、ThisEncryptionAlgoに対して何を返しますか? nullまたは空白のIEncryptionAlgoが返される場合がありますが、これにより、呼び出し元でNPEが発生するか、問題がさらに下にプッシュされます。

そもそもそれを公開すべきではありません。暗号化の詳細は公開すべきではありません。インターフェースが何かを共有すると、そのインターフェースのユーザーはそのインターフェースに接続することができ、そのような接続はメンテナンスが必要であり、壊れにくくなっています。

IEncryptionAlgoICheckSumAlgoを共有して、それらについてログに記録できるようにする場合は、それらの実装の詳細を返すのではなく、GetEncrypterInfo As Stringインターフェースの一部と実装者であれば、それをはるかに簡単に実現できます。

3
xtratic