|
Modular Megafauna Model 1.1.5
A physiological, dynamic herbivore simulator in C++.
|
Some programming principles and paradigms in object-oriented software engineering.
In the megafauna model a couple of object-oriented design patterns were employed that are explained here along with general concepts of object-oriented programming.
Information or data hiding means that any parts that may be subject to later changes in the software design must not be accessible from other modules or from clients. Only the very necessary access may be granted in well-defined, minimal interfaces.
Always assume the worst: If access is given to any other part of the software, that part may change it in unpredictable ways!
Declaring class members private (encapsulation) is one way to data hiding.
If a class explicitely defines at least one of the following methods, it should most likely also define the other ones:
Copy Assignment Operator
A class should have only a single responsibility: A class should have only one reason to change.
A class/module/function should be open for extension, but closed for modification.
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
Many client-specific interfaces are better than one general-purpose interface.
The design principle of inversion of control is also called Hollywood Principle: “Don’t call us, we’ll call you.” An architecture following that principle is built around a generic framework which directs the flow of control by delegating tasks to various, interchangeable submodules. This approach makes the system more modular and extensible.
Inversion of control is related to the Dependency Inversion Principle, which differs in that it is concerned about the relationship between high-level and low-level modules rather than the framework design.
Dependency Injection is a major technique to implement inversion of control: One object (the framework) supplies the dependencies for another object (the client), which then become a part of the client’s state. This is a direct alternative to global objects/variables.
Two kinds of dependency injection are used
For example: The Fauna::HftList object is not a global variable, but is instead being passed down from Fauna::World to other classes that need it.
In the case of Fauna::Habitat, the Fauna::World is oblivious to the concrete realization of the interface, which makes it possible to substitute parts of the model without changing the framework.
A class is called singleton if it permits only one global instantiation in the program. This object-oriented approach has advantages over global variables because it is more generally flexible and the time of instantiation is flexible. However, it is only justified for a class that is at the very top of the management hierarchy in the software architecture.
The basic implementation is as follows:
class MySingleton{
public:
static MySingleton& get_instance(){
static MySingleton global_instance; // creates the instance on first call
return global_instance;
}
private:
MySingleton(){} // Constructor hidden from the outside
MySingleton(MySingleton const&); // deleted copy constructor
void operator=(MySingleton const&); // deleted assignment constructor
}
To get access to the instance or trigger the initial instantiation, use MySingleton::get_instance();.
Wrapping global variables and free functions into a singleton class is good, but it is better to avoid singletons all together and instead follow the principle of Inversion of Control.
The strategy design pattern defines a set of interchangable algorithms, each of which are encapsulated in a class that is derived from one abstract interface parent class. Thanks to C++ polymorphism, the used algorithm can change during runtime. Here is a basic implementation:
struct Strategy {
virtual operator()(const int param) const = 0;
};
struct StrategyOne: public Strategy {
virtual operator()(const int param) const{ /*...*/ };
};
struct StrategyTwo: public Strategy {
virtual operator()(const int param) const{ /*...*/ };
};
In this example, the classes are implemented as function objects (functors) because they overload the operator(). In contrast to simple functions, they could also hold state variables and make use of class inheritance. (They should still be light-weight, though.) Their implemented algorithm can be called by using the object like a function:
Strategy* do_algorithm = new StrategyOne; // just substitute the instantiated class here int i = 1; do_algorithm(i); // this line does not need to change when the strategy is replaced.
Naming Conventions: Obviously, the class name (and the names of their instances and pointers) should be verbs. The class name should of course be capitalized.
Strictly speaking, the strategy pattern aims to offer the possibility to substitute an algorithm during the lifetime of a client object. In the megafauna model that is usually not the case, but rather it is used as a means of [dependency injection](sec_dependency_inversion).
A facade class presents a simple interface to interact with another module or subsystem of the software. The complexity of the subsystem is hidden behind the public functions of the facade class. The subsystem doesn’t know about the facade.
This software documentation is licensed under a Creative Commons Attribution 4.0 International License.