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
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?
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.