Using Strategy Pattern in the Encryption-Decryption Problem

The Strategy Pattern is used when you have different algorithms that can be used to perform a task, and you want to be able to change or select which algorithm is used.

The way to apply this pattern is:

  1. Define an interface for the Strategy. It has whatever method(s) you need to perform the task.

  2. For each algorithm, define a class that implements the Strategy interface.

  3. In the application (called the Context) define an attribute to hold a reference to a concrete Strategy object. Use this reference to call the strategy method(s). In this way, your application can work with any concrete strategy.

UML for Strategy Pattern

Applying the Pattern

The Encryption-Decryption problem uses an algorithm to perform encryption and decryption. Such algorithms are called Ciphers, and there are many of them.

Define a Cipher interface and concrete classes for actual ciphers.

UML for Cipher Strategy

What Are the Parameters?

What should be the parameters to encrypt and decrypt? A Strategy needs parameters (data) in order to perform its task.

In the Encrypt-Decrypt problem we read characters from an InputStream or string. So we culd give the cipher strategy a Reader for input parameter.

The output goes to System.out or a file, so we could use an OutputStream or Writer as the parameter for output.

Finally, the Cipher needs a key for the algorithm.

So you could have an interface like this:

<<interface>>
Cipher
encrypt(Reader in, Writer out, int key)
decrypt(Reader in, Writer out, int key)

You can also use an array of chars as parameters, and let some other part of your application handle reading and writing to streams:

<<interface>>
Cipher
encrypt(char[] in, char[] out, int key)
decrypt(char[] in, char[] out, int key)

or return the result instead:

<<interface>>
Cipher
encrypt(char[] in, int key): char[]
decrypt(char[] in, int key): char[]

Writer is a standard interface for writing characters and arrays of characters. You can use a PrintWriter to write to a file. System.out is not a Writer, but you can “wrap” System.out in a PrintWriter object:

PrintWriter out = new PrintWriter(System.out);

Do we really want 3 parameters?

In the encrypt application, the key doesn’t change.

So, we can simplify the method signatures for encrypt and decrypt by setting the key in the constructor of the actual Cipher class. Or, add a setKey(int) method to the Cipher interface.

Then the interface would simplify to:

<<interface>>
Cipher
encrypt(Reader in, Writer out)
decrypt(Reader in, Writer out)

or use char[] arrays for input and output.

What About -data “string”?

In the Cipher interface, using a Reader for input works for reading from a file, but what about the case where you want to encrypt a String? The application has a command line argument -data "string to encrypt", so you need a way to encrypt a string.

There is StringReader class that can handle this case:

Reader in = new StringReader("encrypt this string");

Creating and Using a Cipher

In your application class, you would have code like this:

int key = /* key read from command line */;

Cipher cipher = new UnicodeCipher(key);
if (mode.equals("dec"))
    cipher.decrypt(in, out);
else
    cipher.encrypt(in, out);

Use a Factory Method to Create the Cipher

The application class needs to create a cipher somehow. This creates coupling between the application and the specific ciphers:

if (alg.equals("shift"))
    cipher = new AlphabetShiftCipher();
else if (alg.equals("unicode"))
    chiper = new UnicodeCipher();

As suggested in the Hyperskill project, you can make your app more flexible by writing a Factory class that creates Cipher objects.

The simplest possible factory just has a static method:

String alg = "unicode"; // read from command line arguments
int key = 0;            // read from command line arguments

Cipher cipher = CipherFactory.getCipher(alg, key);

To add a new Cipher just modify the CipherFactory. The rest of the application doesn’t change.