Under the context of dependency injection - that is, an interface has mostly one implementation - I took the habit of exposing via the Interface a bunch of fields which are never called by the consumer classes. These fields reflect a high-level implementation strategy; I decided to expose them via my interfaces because I feel it helps to understand how the abstraction works - or let's say what is expected from that abstraction - not in terms of detailed implementation, but in terms of high-level principles and why the object/interface was needed in the first place.
For example, I have an interface IEncrypter
which encrypts strings. The idea behind this interface is to be able to choose which encryption algorithm to use. So the implementation class takes an algorithm abstraction IEncryptionAlgo
in its constructor, and stores it as a ReadOnly field: ThisEncryptionAlgo As IEncryptionAlgo
. Then when I call Encrypt(Message)
, it calls ThisEncryptionAlgo.Encrypt(Message)
.
Strictly speaking, the interface does not need to expose ThisEncryptionAlgo
, and exposing the Byte() Encrypt(Message As String)
function alone is sufficient for the consumer. However, I feel that having the Interface exposing ThisEncryptionAlgo
(as a ReadOnly) has some advantages:
- You help developers to understand the spirit behind the interface, which is useful both when implementing and when consuming.
- You make debugging easier as you can quickly inspect the property directly from the interface.
- Error logging and tracing might be easier if you generate a report based on the interface properties.
I believe it is ok because the main purpose of having this as an interface rather than a concrete class is to allow dependency injection and unit testing of the consumers, not to add a true layer of abstraction. Having said that, it also defeats the principle that interfaces should disregard any implementation details.
What is your opinion? Should I remove ThisEncryptionAlgo
from my interface?
Full Example
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
As one can see, it is "by design" that IEncrypter
holds a field which refers to the algo object to use. If one wants to use a different algo, they may implement IEncryptionAlgo
and inject it via the ServicesFactory
. Under such context, ThisEncryptionAlgo
is not needed by the consumer class, but having it exposed via the IEncrypter
interface ensures any implementation of the later fits the overall architecture. At least, that is what I intuitively feel, but I'd like to challenge this.
ThisEncryptionAlgo
private?