This is a follow up to my 2 video tutorial “Interfaces in C#”. There were some comments left on the YouTube thread suggesting that some folks are understanding how interfaces work, but still don’t see the point in using them. As always, I say that exposure is the best answer. You have to encounter the problems before you could ever hope to understand the proposed solutions to them.
Before we get started, I’d like to comment on one of the user comments which was something along the lines of
“What is the point of using interfaces when you still have to put the code for the interface in the class that implements it?”.
Well, again — implementing interfaces isn’t like inheriting from another class. You don’t do it to gain functionality; you do it declare functionality. I think some of us get caught up on the fact that the syntax is the same for implementing an interface as it is to inherit from another class. Well forget all of that; it’s just syntax.
By implementing one or more interfaces, you’re basically just telling the compiler that your class has the functionality described in the interface. How that functionality is implemented is irrelevant in this context. It’s like me asking you “Hey do you know all the words to *Ice Ice Baby*?” and you say “Why yes I do! It’ll cost you $10!” Then I say cool and call you out on it: SingIceIceBaby(me, you);
Whether you actually do or not is another story, but by telling me that you did, you’ve given me an interface to ask you to do so.
1 2 3 4 5 6 7 8 |
public void SingIceIceBaby(ISingIceIceBaby singer, Person requester) { if(requester.HasEnoughMoney(10.00M)) { singer.TakeMoney(10.00M, requester); singer.SingIceIceBaby(); } } |
Now, it could be that when I invoke SingIceIceBaby
on you, you get half the words wrong, but I don’t care. As the invoker, I just needed a way to ask you to try it. Now if you never heard the song, then you probably wouldn’t tell me that you could sing it, so I wouldn’t ask you to. In otherwords, you have not implemented that interface.
“I ask again, why not just implement the method on the class itself? Why bother with interfaces?”
Well that makes sense in a lot of cases, we’re object oriented programming experts here and we don’t like to have to rewrite identical logic in multiple places, so what if we wanted to be able to call SinglceIceBaby()
on any object that knows how? Well I could make some class that knows how to do it, and have other classes could inherit from it, but by doing so, all of those classes are a) using up their ability to inherit from one single class and b) Now everything that inherits from that class become part of the same family of objects. In other words, if Person inherited from the same class that Jukebox inherits from, you are saying that People are from the same family as jukeboxes. That’s uh… that ain’t right and almost guaranteed mess you up in the future! Interfaces give you a way to declare functionality or traits without joining the wrong families. Those functions and methods that look for the interface, don’t care about the details, they just want to interact with members of the interface that it needs.
“I’m gonna need a practical example…”
I thought you’d never ask. And away we go! Let’s start by playing a little game…
What do the following objects have in common?
Ahhh… the abstraction game! Well let’s start by grouping them by the somewhat obvious traits…
Ok, so we were able to break these up into 3 groups: phones, payment terminals, and generic input devices. By grouping them like this, we can see that it’s possible to probably base class some of these if we were to represent them in code as objects. In other words, we have 3 groups of objects that are not semantically related across groups. For instance, a rotary phone is in no way semantically related to a cash register. So assuming we needed to tie them together somehow, what’s another common factor?
Ohhh… they all have different kinds of number pads! Interesting. So these things all have something in common, but are not related. This could pose a potential problem in code. Imagine our code needed a way to interact with anything in the world that had a number pad. Like a person in a video game, or a robot that runs chores for you, or even a 3rd party plugin like a password manager that needs to be able to interface with something in order to punch in your password on your behalf. That’s exactly what these number pads are: Interfaces.
So let’s run through a scenario in code — and I’m going to kinda play devils advocate here while trying to avoid the need for an interface until I can no longer. Imagine all of the objects above are represented in code and we have an object that needs to be able to interact with all of these objects. Without doing much thinking, maybe we would come up with something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public class Person { public void InputToCashRegister(char key) { } public void InputToCreditCardTerminal(char key) { } public void InputToComputerNumPad(char key) { } public void InputToAccessPanel(char key) { } public void InputToCellphone(char key) { } public void InputToSmartPhone(char key) { } public void InputToRotaryPhone(char key) { } } |
This object has a method to deal with inputting information for each individual kind of device. I think it goes without saying that this is absurd. Any time a new type of device is created, a new method would have to be added to this object and any other object that hoped to use it.
Well the first though of most programmers I know would be to try and abstract the devices down to a point where we can be more generic about invoking the input command of the device. Maybe something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Person { public void InputToDevice(char key, InputDevice device) { device.DoInput(key); } } public class InputDevice { public void DoInput(char key) { // ... } } |
Here, we’ve abstracted this down to a generic InputDevice
with a single method called DoInput
. But keep in mind, that every device is going to need to deal with a key press in a different way! For instance, a rotary phone takes its input much differently than a smart phone or a cash register. Ok so why not make the input method abstract so that each child class can override it to provide its own functionality. That should still allow the child class to be used in place of its parent (polymorphism). Maybe something like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
public abstract class InputDevice { public abstract void DoInput(char key); } public class CreditCardTerminal : InputDevice { public override void DoInput(char key) { // do work } } |
Simple enough. So that may solve the problem of many devices having a common interface, but remember that the devices aren’t related across groups and may have to play by different rules. For instance, what if the key pad needed each keystroke to be encrypted? Normally what I’d see people do is sub class the InputDevice and implement security in that subclass. Maybe something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class CreditCardTerminal : InputDevice { public override void DoInput(char key) { var eKey = GetEncryptedKey(key); // continue ... } private byte[] GetEncryptedKey(char key) { // do stuff and return encrypted key } } |
Splendid. But what if another object also needs to be secure. A security access pad, for instance. Oh ok… then just remove the security logic and use it in any class that needs it (composition) like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class Hsm { public static byte[] GetEncryptedKey(char key) { // do stuff and return encrypted key } } public class CreditCardTerminal : InputDevice { private Hsm _hsm; public override void DoInput(char key) { var eKey = _hsm.GetEncryptedKey(key); // continue ... } } public class AccessPanel : InputDevice { private Hsm _hsm; public override void DoInput(char key) { var eKey = _hsm.GetEncryptedKey(key); // continue ... } } |
Ahhh yes, and the solution is still simple. Each input device can still be used in place of InputDevice
but they both have their own security. But what if you’re working with a subsystem, that doesn’t just want an InputDevice
but specifically an input device that has implemented security? You still want to be able to use this keypad with anything that can work with keypads, but in this case, you need to be able to use it in an explicitly secure context. Maybe a solution like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class SecureInputDevice : InputDevice { public Hsm { get; set; } } public class AccessPanel : SecureInputDevice { public override void DoInput(char key) { var eKey = _hsm.GetEncryptedKey(key); // continue ... } } |
Ok cool! By subclassing InputDevice and abstracting the Hsm into the secure child, I should now be able to use any children of SecureInputDevice
in both a secure context as well as an insecure context. You see a lot of this in older software, the object graph can go on for days; a bajillion different variations of the same object.
Time to play dirty!
But what if the base class contained a trait that contradicted one or more of its subclasses? This would be a total violation of the Liskov Substitution Principle. Let’s change it up a bit — I’m going to change the linear history of the game to illustrate this point:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class InputDevice { public char LastKey { get; private set; } public void DoInput(char key) // cannot override this { LastKey = key; PerformInput(key); } protected abstract void PerformInput(char key); } public class SecureInputDevice : InputDevice { public Hsm { get; set; } } public class AccessPanel : SecureInputDevice { protected override void PerformInput(char key) { var eKey = _hsm.GetEncryptedKey(key); // do other stuff } } |
Uh oh. Now we have a problem. Despite the SecureInputDevice encrypting each key stroke, it’s still storing the unencrypted keystroke in a public property. How did this happen?! Well SecureInputDevice inherits from InputDevice, so it takes on the traits of InputDevice whether we want it to or not. And sometimes, we have no control over the base class implementation, or if we do, changing its behavior could have unforseen effects on other classes that use or derive from it.
So why did we do it? Well we wanted to inherit the DoInput(char) method so that we could use it across several different types and we were only allowed to inherit from a single class, so the only way to be an InputDevice
AND have security features without forcing InputDevice
to have security features was to subclass it into SecureInputDevice
— forcing us into inheriting behavior from InputDevice
which is actually not what we really wanted to do. Make sense? Well how would we get around that? Interfaces…
Consider the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
public interface IKeypadInput { void DoInput(char input); } public abstract class SecureInputDevice : IKeypadInput { private Hsm _hsm; public SecureInputDevice(Hsm hsm) { _hsm = hsm; } protected abstract void PerformInput(string input); public void DoInput(char input) { var eKey = _hsm.GetEncryptedKey(input); PerformInput(eKey.ToBase64()); } } public class CreditCardTerminal : SecureInputDevice { public CreditCardTerminal(Hsm hsm) : base(hsm) { } public override void PerformInput(string input) { // do work (e.g. store in buffer) } public void ChargeCard() { // use buffered (encrypted data) to charge card } } public class SmartPhone : IKeypadInput { private List<char> _phoneNumber = new List<char>(); public void DoInput(char input) { _phoneNumber.Add(input); // display on screen // emulate dial tones // show suggestions } public void DialNumber() { // do logic } } |
What we’ve done here is, without changing the public interface, we have created two classes that are totally unrelated but share a common method: DoInput
. One class is secured, the other isn’t. Both can be used by any method that accepts IKeypadInput
, and neither takes on traits that it doesn’t need nor want.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public void Main { var creditTerminal = new CreditCardTerminal(new DefaultHsm()) var cellPhone = new SmartPhone(); // input stuff into credit terminal DoInput("4.25", creditTerminal); // creditTerminal.ChargeCard(); // dial a phone numbers DoInput("6025551234", cellPhone); cellPhone.Dial(); // this should also work DoSecureInput(creditTerminal); // this would not DoSecureInput(cellPhone); } public void DoInput(string input, IKeypadInput inputDevice) { // we can technically input into any device that has the keypad interface, but we // cannot guarantee it is secure. We can only guarantee that we're sending input. // in otherwords, this method doesn't care if it's secure or not. it doesn't care about // The implementation. If you need a secure input device, then that is up to the caller to send one. foreach(char c in input.ToCharArray()) { inputDevice.DoInput(c); } } public void DoSecureInput(string input, SecureInputDevice inputDevice) { // here we can guarantee security because we know that SecureInputDevice is secure // the down side to this is that this is the same exact logic as DoInput, but this one // ONLY accepts SecureInputDevice foreach(var c in input.ToCharArray()) { inputDevice.DoInput(c); } } |
I’m showing two contexts here. One demands a secure input device, the other doesn’t. By abstracting a common interface, the DoSecureInput
method is actually unnecessary. SecureInputDevice
is a specific implementation of the device, but in both contexts, we really don’t care; we just want to throw some input at it. Also, by choosing to implement an interface instead of inheriting from a base class, we were able to declare common functionality without creating an unnecessary inheritance of traits between two unrelated objects. It’s worth noting that this is particularly useful in a scenario where you don’t know what the concrete type is going to be, but you know it’s going to be an object that implements IKeypadInput
— for instance when you’re getting the object from a factory.
I really hope this helps someone!