Skip to content

Consciously Choosing OOP‐ness

Jonathan Bloedow edited this page Jul 23, 2024 · 6 revisions

This might just be a "me thing", but as we were discussing this morning, we might want to be deliberate in choose whether to go with a pure functional programming approach or a more object-oriented approach in our implementation. Rather than simply use techniques that appeal to us as software engineers, we might want to forego some software engineering constructs in the interest of making the codebase more accessible to more people.

But it isn't simply a matter of choosing to go "OO" or not, or to use classes or not. Here's 9 Levels of OOP-ness I got from ChatGPT that I rather like.

1. Pure Functions

Description: Functions that return the same output given the same input without side effects.
Example (Python):
    def add(a, b):
        return a + b

2. Encapsulation Using Structs (C-style)

Description: Grouping related data together in a single structure, but without methods.
Example (C):
    struct Point {
        int x;
        int y;
    };

Note that a python module (file) can function in many ways as a C struct. Also note that there all members are public. There are no private or protected members.

3. Encapsulation with Methods (Basic Classes)

Description: Bundling data (attributes) and behavior (methods) together in a class.
Example (Python):
    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
        
        def move(self, dx, dy):
            self.x += dx
            self.y += dy

3b. Singletons or Modules.

If you want the benefits of a class, such as encapsulation and member data and functions, but you will never need multiple instances, you can use a singleton pattern or simply utilize a module. Here's how you can do each:

Singleton Pattern

The singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is useful if you want to control the creation of an object and ensure that only one instance is used throughout your application.

Here’s an example of a singleton pattern in Python:

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
        return cls._instance

    def __init__(self):
        self.incubation_period = 7

    def get_incubation_period(self):
        return self.incubation_period

    def set_incubation_period(self, new_value):
        self.incubation_period = new_value

Usage

singleton = Singleton()
print(singleton.get_incubation_period())  # Output: 7
singleton.set_incubation_period(10)
print(singleton.get_incubation_period())  # Output: 10

another_singleton = Singleton()
print(another_singleton.get_incubation_period())  # Output: 10

In this example, Singleton class will always return the same instance when instantiated.

Module-Level Encapsulation

If you don't need the complexity of a singleton and want to keep things simpler, you can use module-level encapsulation. This is effective in Python because a module is only initialized once and can hold state between function calls.

Here’s how you can achieve encapsulation using a module:

my_module.py

_incubation_period = 7

def get_incubation_period():
    return _incubation_period

def set_incubation_period(new_value):
    global _incubation_period
    _incubation_period = new_value

You can then use this module in your application:

import my_module

print(my_module.get_incubation_period())  # Output: 7
my_module.set_incubation_period(10)
print(my_module.get_incubation_period())  # Output: 10

Comparison

Singleton Pattern:
    Use if you need more structured code and potential future expansion where you might benefit from object-oriented principles.
    Provides clear encapsulation and can be extended with inheritance or other OOP features.
    Slightly more complex to implement compared to a module.

Module-Level Encapsulation:
    Use if you want a simpler approach and don't need the additional structure or complexity of a class.
    Straightforward and leverages Python's module system.
    Ideal for small scripts or when you need to maintain state within a module.

Both approaches provide encapsulation and a single point of access to the data and functions, but the choice depends on the complexity and future scalability needs of your project.

4. Encapsulation and Inheritance

Description: Creating a new class from an existing class (base class), inheriting its attributes and methods.
Example (Python):
    class Animal:
        def __init__(self, name):
            self.name = name
        
        def speak(self):
            pass

    class Dog(Animal):
        def speak(self):
            return f"{self.name} says Woof!"

5. Polymorphism

Description: The ability to call the same method on different objects and have each of them respond in their own way.
Example (Python):
    class Cat(Animal):
        def speak(self):
            return f"{self.name} says Meow!"

    animals = [Dog("Rex"), Cat("Whiskers")]
    for animal in animals:
        print(animal.speak())

6. Abstract Base Classes

Description: Classes that cannot be instantiated on their own and require subclasses to provide implementations for abstract methods.
Example (Python):
    from abc import ABC, abstractmethod

    class Shape(ABC):
        @abstractmethod
        def area(self):
            pass

    class Rectangle(Shape):
        def __init__(self, width, height):
            self.width = width
            self.height = height
        
        def area(self):
            return self.width * self.height
  1. Interfaces

    Description: A contract that defines a set of methods that implementing classes must provide, without providing any implementation. Example (Java):

    interface Shape {
        double area();
    }

    class Circle implements Shape {
        private double radius;
        
        public Circle(double radius) {
            this.radius = radius;
        }

        public double area() {
            return Math.PI * radius * radius;
        }
    }

8. Templates (C++) / Generics (Java)

Description: Allowing classes and methods to operate on objects of various types while providing compile-time type safety.
Example (C++):
    template <typename T>
    class Box {
    private:
        T content;
    public:
        void setContent(T content) {
            this->content = content;
        }
        
        T getContent() {
            return content;
        }
    };

9. Templated Abstract Base Classes

Description: Combining templates/generics with abstract base classes to create flexible, type-safe base classes that enforce a contract for derived classes.
Example (C++):
    template <typename T>
    class AbstractShape {
    public:
        virtual T area() const = 0;  // Pure virtual function
    };

    class Rectangle : public AbstractShape<double> {
    private:
        double width, height;
    public:
        Rectangle(double w, double h) : width(w), height(h) {}
        double area() const override {
            return width * height;
        }
    };

This progression illustrates the gradual incorporation of OOP principles into a functional programming foundation, from simple encapsulation to advanced use of generics and abstract base classes.

I'm going to recommend that we consciously selected "Level 3" for LASER.

Multiple Instances of Simulation Objects

One of the key topics which has emerged is the question of whether we need to create multiple instances of objects such as Simulation, Demographics, Settings, etc. in a single run of a simulation.

Jonathan has been deliberately not doing that. Back in the early days of EMOD (DTK), some code was added to enable exactly that kind of thing: a single run of the executable could run multiple simulations. Over time, as we were running almost everything on COMPS where a single run was a single sim, we ended up deciding that the extra code to enable multiple non-overlapping Simulation instances in a single process was all cost and no benefit.

But researchers have suggested there might be a genuine demand for doing that. One might do that when one is running locally and wanting to do sweeps in a for loop and analyzing and consolidating results in a single process. In that case, we would not want singletons but rather actual instances of class types. But would we ever do that in our batch processing environment (COMPS, K8S)? Does that matter?

Clone this wiki locally