When designing code, you alaways have two options.
- just get it done, in which case pretty much any solution will work for you
- be pedantic and design a solution which exploits the quirks of the language an its ideology (OO languages in this case - the use of polymorphism as a mean to provide the decision)
I am not going to focus on the first of the two, because there is really nothing to be said. If you just wanted to get it to work, you could leave the code as it is.
But what would happen, if you chose to do it the pedantic way and actually solved the problem with design patterns, in the way you wanted it do?
You could be looking at the following process:
When designing OO code, most of the if
s which are in a code do not have to be there. Naturally, if you want to compare two scalar types, such as int
s or float
s, you are likely to have an if
, but if you want to change procedures based on configuration, you can use polymorphism to achieve what you want, move the decisions (the if
s) from your business logic to a place, where objects are instantiated - to factories.
As of now, your process can go through 4 separate paths:
data
is neither encrypted nor compressed (call nothing, return data
)
data
is compressed (call compress(data)
and return it)
data
is encrypted (call encrypt(data)
and return it)
data
is compressed and encrypted (call encrypt(compress(data))
and return it)
Just by looking at the 4 paths, you find a problem.
You have one process which calls 3 (theoretically 4, if you count not calling anything as one) different methods that manipulate the data and then returns it. The methods have different names, different so called public API (the way through which the methods communicate their behaviour).
Using the adapter pattern, we can solve the name colision (we can unite the public API) that has occured. Simply said, adapter helps two incompatible interfaces work together. Also, adapter works by defining a new adapter interface, which classes trying to unite their API implement.
This is not a concrete language. It is a generic approach, the any keyword is there to represent it may be of any type, in a language like C# you can replace it with generics (<T>
).
I am going to assume, that right now you can have two classes responsible for compression and encryption.
class Compression
{
Compress(data : any) : any { ... }
}
class Encryption
{
Encrypt(data : any) : any { ... }
}
In an enterprise world, even these specific classes are very likely to be replaced by interfaces, such as the class
keyword would be replaced with interface
(should you be dealing with languages like C#, Java and/or PHP) or the class
keyword would stay, but the Compress
and Encrypt
methods would be defined as a pure virtual, should you code in C++.
To make an adapter, we define a common interface.
interface DataProcessing
{
Process(data : any) : any;
}
Then we have to provide implementations of the interface to make it useful.
// when neither encryption nor compression is enabled
class DoNothingAdapter : DataProcessing
{
public Process(data : any) : any
{
return data;
}
}
// when only compression is enabled
class CompressionAdapter : DataProcessing
{
private compression : Compression;
public Process(data : any) : any
{
return this.compression.Compress(data);
}
}
// when only encryption is enabled
class EncryptionAdapter : DataProcessing
{
private encryption : Encryption;
public Process(data : any) : any
{
return this.encryption.Encrypt(data);
}
}
// when both, compression and encryption are enabled
class CompressionEncryptionAdapter : DataProcessing
{
private compression : Compression;
private encryption : Encryption;
public Process(data : any) : any
{
return this.encryption.Encrypt(
this.compression.Compress(data)
);
}
}
By doing this, you end up with 4 classes, each doing something completely different, but each of them providing the same public API. The Process
method.
In your business logic, where you deal with the none/encryption/compression/both decision, you will design your object to make it depend on the DataProcessing
interface we designed before.
class DataService
{
private dataProcessing : DataProcessing;
public DataService(dataProcessing : DataProcessing)
{
this.dataProcessing = dataProcessing;
}
}
The process itself could then be as simple as this:
public ComplicatedProcess(data : any) : any
{
data = this.dataProcessing.Process(data);
// ... perhaps work with the data
return data;
}
No more conditionals. The class DataService
has no idea what will really be done with the data when it is passed to the dataProcessing
member, and it does not really care about it, it is not its responsibility.
Ideally, you would have unit tests testing the 4 adapter classes you created to make sure they work, you make your test pass. And if they pass, you can be pretty sure they will work no matter where you call them in your code.
So doing it this way I will never have if
s in my code anymore?
No. You are less likely to have conditionals in your business logic, but they still have to be somewhere. The place is your factories.
And this is good. You separate the concerns of creation and actually using the code. If you make your factories reliable (in Java you could even go as far as using something like the Guice framework by Google), in your business logic you are not worried about chosing the right class to be injected. Because you know your factories work and will deliver what is asked.
Is it necessary to have all these classes, interfaces, etc.?
This brings us back to the begining.
In OOP, if you choose the path to use polymorphism, really want to use design patterns, want to exploit the features of the language and/or want to follow the everything is an object ideology, then it is. And even then, this example does not even show all the factories you are going to need and if you were to refactor the Compression
and Encryption
classes and make them interfaces instead, you have to include their implementations as well.
In the end you end up with hundreds of little classes and interfaces, focused on very specific things. Which is not necessarily bad, but might not be the best solution for you if all you want is to do something as simple as adding two numbers.
If you want to get it done and quickly, you can grab Ixrec's solution, who at least managed to eliminate the else if
and else
blocks, which, in my opinion, are even a tad worse than a plain if
.
Take into consideration this is my way of making good OO
design. Coding to interfaces rather than implementations, this is how
I have done it for the past few years and it is the approach I am the
most comfortable with.
I personally like the if-less programming more and would much
more appreciate the longer solution over the 5 lines of code. It is
the way I am used to designing code and am very comfortable reading
it.
Update 2: There has been a wild discussion about the first version of my solution. Discussion mostly caused by me, for which I apologize.
I decided to edit the answer in a way that it is one of the ways to look at the solution but not the only one. I also removed the decorator part, where I meant facade instead, which I in the end decided to leave out completely, because an adapter is a facade variation.
if
statements?