依存関係注入のコンテキストの下で-つまり、インターフェースにはほとんど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つのオプションを見てみましょう。
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つだけ使用することに縛られることなく、デバッグとナレッジマネジメントのニーズを満たす方法を手に入れることができます。
はい、削除してください。
クラスを効果的に密結合するアルゴを実装せずに誰かが暗号化機能を実装するのを防ぎます。
単体テストや依存性注入を行うためのインターフェースは必要ありません。 DIと単体テストには抽象化が必要です。
完全なコードの補遺
サービスファクトリも使用するコードも、追加の読み取り専用プロパティを使用しません。必要なのは次のものだけです。
IEncryptor
byte[] Encrypt(string)
インターフェースをできる限り小さくして、解読を分割するなどと主張する人もいるでしょう。私はそれほど遠くまでは行きませんが、好みの問題です。
ただし、読み取り専用フィールドを呼び出すのは、単体テストでservicefactoryが正しく構築されたオブジェクトを返すことを確認することだけであると思われます。
プロパティとして値を公開せずにこれを行う方法はたくさんありますが、実際に使用するアルゴリズムとは異なるアルゴリズムを公開する暗号化を実装できるため、テストにはまったく役立たないと主張します。 。
これは、単に文字列を暗号化し、その結果を、予想されるアルゴリズムで暗号化された既知の値と比較することで、より適切に達成されます。
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.
...
}
そのような「追加の」メソッドをインターフェースに追加することについては、私は非常に慎重になるでしょう。
インターフェイスはその責任について非常に明確でなければなりません。
IEncrypter
はIEncryptionAlgo
またはICheckSumAlgo
についてあなたに伝える責任がありますか?私はそれがEncrypt
とDecrypt
だけであるべきだと思います。余分なメソッドを追加することで、実装により多くの要件/制約を課すことになります。
バイトを通過させるだけの非暗号化IEncrypter
が必要な場合、ThisEncryptionAlgo
に対して何を返しますか? null
または空白のIEncryptionAlgo
が返される場合がありますが、これにより、呼び出し元でNPE
が発生するか、問題がさらに下にプッシュされます。
そもそもそれを公開すべきではありません。暗号化の詳細は公開すべきではありません。インターフェースが何かを共有すると、そのインターフェースのユーザーはそのインターフェースに接続することができ、そのような接続はメンテナンスが必要であり、壊れにくくなっています。
IEncryptionAlgo
とICheckSumAlgo
を共有して、それらについてログに記録できるようにする場合は、それらの実装の詳細を返すのではなく、GetEncrypterInfo As String
インターフェースの一部と実装者であれば、それをはるかに簡単に実現できます。