I am implementing a game in python, which will give the user a sample encryption, and explain each step from plaintext to cipher. The task for the player is to decrypt a word by undoing each step shown above on the new word.
For this, I have created an abstract algorithm which requires an encrypt function, and a current development status. In the future I think it will require other properties such as explanation text or a difficulty level.
The abstract algorithm looks like this:
from abc import ABC, abstractmethod
from enum import Enum
class Status(Enum):
"""The current development status"""
in_development = 0
under_testing = 1
production_ready = 2
known_bugs = 3
class BaseAlgorithm(ABC):
"""This is the base on which all other algs are to be built"""
@abstractmethod
def encrypt(self, word):
"""Encrypt the word using an internal alg"""
pass
@abstractmethod
def decrypt(self, word):
"""Decrypt the word using an internal alg
If this is not overloaded, it is assumed that"""
return self.encrypt(word)
@property
@abstractmethod
def status(self):
"""The current progress and readiness of the alg"""
pass
An example algorithm is implemented in rotate.py
from . import BaseAlgorithm, Status
class RotateAlg(BaseAlgorithm):
"""Takes a rotation amount which it uses to encrypt or decrypt
all words pass into it"""
def __init__(self, offset):
try:
self.offset = int(offset)
except ValueError:
raise ValueError("the offset must be an integer") from None
# Decides which direction is the default way to rotate
self.rotate_left = True
def encrypt(self, word):
"""Encrypts by rotating the word"""
return self.rotate(word)
def decrypt(self, word):
"""Decrypts by rotating the word back"""
return self.rotate(word, inverse=True)
def rotate(self, word, inverse=False):
"""Rotates the word to the left"""
offset = self.offset
# Inverse and rotate_left both flip the direction
# If both are the same the direction remains unchanged
if inverse == self.rotate_left:
offset *= -1
return rotate(word, offset)
@property
def status(self):
return Status.in_development
def rotate(word, offset):
"""Rotates the word right by the amount specified"""
offset %= len(word)
return word[offset:] + word[:offset]
Finally how these algorithms would be composed together is a simple accumulating loop
def decode(cipher, encryptions):
for encryption in reversed(encryptions):
cipher = encryption.decrypt(cipher)
return cipher
I feel like this is the wrong approach to take though, as there is a lot of boilerplate for what is essentially a function which is used in two ways. Is there a better design pattern to use in this scenario? I suspect a more functional approach would suit, such as passing the encryption functions into a class that binds them together. My concern is I lose the option of providing metadata with the function with that style.
1 Answer 1
For such a simple use case, I would drop the abc
module in favor of NotImplementedError
:
class BaseAlgorithm:
"""This is the base on which all other algorithms are to be built."""
# Unless explicitly stated otherwise, algorithms are in development
STATUS = Status.in_development
def encrypt(self, word):
"""Encrypt the word using an internal algorithm."""
raise NotImplementedError
def decrypt(self, word):
"""Decrypt the word using an internal algorithm.
If this is not overloaded, it is assumed that
encrypting and decrypting are the same operation.
"""
return self.encrypt(word)
Using such base, only encrypt
is explicitly required to be overloaded. I also extracted the status from a method to a class constant as this is the kind of information that does not depend of any state about this class. It also allows to focus on the algorithm first and only change the status when it is ready. Lastly, I also expanded the docstrings a bit so they are more explicit; you may want to explain, in the class docstring, what a user is expected to do when inheriting from BaseAlgorithm
.
As for RotateAlg
:
- you could name it
Rotate
as theAlg
part is already implied from the baseBaseAlgorithm
class; - you could incorporate the
rotate
function directly in therotate
method as there shouldn't be much use for a function on their own; - you could handle the
rotate_left
logic better by using this information directly as a parameter torotate
.
This would become something along the lines of:
class Rotate(BaseAlgorithm):
"""Takes a rotation amount which it uses to encrypt or decrypt
all words pass into it"""
def __init__(self, offset):
try:
self.offset = int(offset)
except ValueError:
raise ValueError("the offset must be an integer") from None
def encrypt(self, word):
"""Encrypts by rotating the word"""
return self.rotate(word, rotate_left=False)
def decrypt(self, word):
"""Decrypts by rotating the word back"""
return self.rotate(word, rotate_left=True)
def rotate(self, word, rotate_left=False):
"""Rotates the word to the chosen direction"""
offset = -self.offset if rotate_left else self.offset
offset %= len(word)
return word[offset:] + word[:offset]
And, unless you specify otherwise with:
class Rotate(BaseAlgorithm):
STATUS = Status.under_testing
...
then Rotate.STATUS
will still be Status.in_development
.
Explore related questions
See similar questions with these tags.