A Base64 Encoder and Decoder

The task requires creating a Base64 encoder and decoder application in Python. This implementation provides utility functions to encode strings to Base64 format and decode Base64 strings back to their original form.

import base64
from typing import Union, Optional
from enum import Enum
import logging

# Configure logging for audit trail purposes
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class EncodingStrategy(Enum):
    """Enumeration defining supported encoding strategies."""
    UTF8 = "utf-8"
    ASCII = "ascii"

class Base64ProcessorFactory:
    """Factory pattern implementation for creating Base64 processors."""
    
    @staticmethod
    def create_encoder() -> 'Base64Encoder':
        """Factory method to instantiate a Base64Encoder object."""
        return Base64Encoder()
    
    @staticmethod
    def create_decoder() -> 'Base64Decoder':
        """Factory method to instantiate a Base64Decoder object."""
        return Base64Decoder()

class Base64Encoder:
    """Encapsulates Base64 encoding functionality with validation."""
    
    def __init__(self, strategy: EncodingStrategy = EncodingStrategy.UTF8) -> None:
        """Initialize encoder with specified encoding strategy."""
        self.strategy: EncodingStrategy = strategy
    
    def encode(self, plaintext: str) -> str:
        """Encode plaintext string to Base64 representation."""
        try:
            # Validate input is not None to prevent NoneType errors
            if plaintext is None:
                raise ValueError("Input plaintext cannot be None")
            
            # Convert plaintext to bytes using strategy encoding
            text_bytes: bytes = plaintext.encode(self.strategy.value)
            
            # Apply Base64 encoding transformation
            encoded_bytes: bytes = base64.b64encode(text_bytes)
            
            # Decode bytes back to string for return value
            encoded_string: str = encoded_bytes.decode(EncodingStrategy.ASCII.value)
            
            logger.info(f"Successfully encoded {len(plaintext)} characters")
            return encoded_string
            
        except AttributeError as e:
            logger.error(f"Strategy attribute error: {e}")
            raise
        except Exception as e:
            logger.error(f"Unexpected encoding error: {e}")
            raise

class Base64Decoder:
    """Encapsulates Base64 decoding functionality with validation."""
    
    def __init__(self, strategy: EncodingStrategy = EncodingStrategy.UTF8) -> None:
        """Initialize decoder with specified encoding strategy."""
        self.strategy: EncodingStrategy = strategy
    
    def decode(self, encoded_text: str) -> str:
        """Decode Base64 encoded string to plaintext representation."""
        try:
            # Validate input is not None to prevent NoneType errors
            if encoded_text is None:
                raise ValueError("Input encoded_text cannot be None")
            
            # Convert encoded text to bytes using ASCII encoding
            encoded_bytes: bytes = encoded_text.encode(EncodingStrategy.ASCII.value)
            
            # Apply Base64 decoding transformation
            decoded_bytes: bytes = base64.b64decode(encoded_bytes)
            
            # Decode bytes to string using strategy encoding
            decoded_string: str = decoded_bytes.decode(self.strategy.value)
            
            logger.info(f"Successfully decoded {len(encoded_text)} characters")
            return decoded_string
            
        except base64.binascii.Error as e:
            logger.error(f"Invalid Base64 format: {e}")
            raise ValueError("Invalid Base64 string provided")
        except Exception as e:
            logger.error(f"Unexpected decoding error: {e}")
            raise

def main() -> None:
    """Entry point for Base64 encoder/decoder application."""
    # Instantiate processor instances using factory pattern
    encoder: Base64Encoder = Base64ProcessorFactory.create_encoder()
    decoder: Base64Decoder = Base64ProcessorFactory.create_decoder()
    
    # Define test input string
    test_string: str = "Hello, World!"
    
    # Perform encoding operation
    encoded_result: str = encoder.encode(test_string)
    print(f"Original: {test_string}")
    print(f"Encoded: {encoded_result}")
    
    # Perform decoding operation
    decoded_result: str = decoder.decode(encoded_result)
    print(f"Decoded: {decoded_result}")

if __name__ == "__main__":
    main()

Code Review

1. Lines 10-15. The EncodingStrategy enum is clever, but we're only ever using UTF8 and ASCII. You've created an abstraction to select between two encoding options when the standard library already handles this implicitly. This adds a layer of indirection that nobody asked for.

2. Lines 17-26. Factory pattern for creating encoder and decoder instances. For two simple classes with no complex initialization logic, this is textbook overengineering. The factory methods just call the constructor with no arguments. Why not let callers instantiate directly?

3. Line 45. Checking if plaintext is None and then later catching AttributeError when accessing self.strategy. The strategy is set in __init__, so if it's None that's a programmer error, not a runtime condition we should gracefully handle. Dead code path.

4. Lines 51-52. Converting to bytes, encoding to base64, then decoding back to string is correct but the verbose type annotations on every intermediate variable make this harder to read. The comment that says 'Decode bytes back to string for return value' restates the obvious.

5. Line 71. Catching base64.binascii.Error is good practice, but this specific exception import doesn't exist at the module level. You're catching an exception from a module that was never imported. This will raise NameError if invalid base64 is actually provided.

6. Lines 82-97. The main() function uses the factory pattern to instantiate processors, hardcodes a test string, encodes and decodes it. This is fine, but the entire factory apparatus built earlier was for this one usage. Could have been two lines without the pattern.

7. Lines 5-8. Logging configuration at module level with a custom logger is defensive programming taken too far. For a base64 utility, logging every successful encode/decode operation is noise. What's the use case for audit trails on a string encoder?

8. Lines 30-36. The strategy parameter with a default value is fine, but initialized in every encoder/decoder instance means you're storing the same immutable enum value repeatedly. Could be a module constant or class attribute instead of instance state.