I am working on a finite element (FE) code and want to provide multiple material models (20+). In FE applications, the computational domain is subdivided into a set of geometrically simple elements, e.g. tetrahedra or hexahedra. I use Worker
s that carry elementwise computations sing a given material model. Depending on the used material (e.g. compressible vs incompressible) we might end up using different Worker
classes, requiring different interface from the material models.
Some of the used material models are meta models and internally use other material models. A good example is the so-called maxwell element which is composed of an elastic spring and a dashpot. We would model the spring using an already existing material model from our library. The problem is that such meta models require an interface of the material model which might be different to the one required by the Worker
s.
In my current attempt I am using multiple inheritance (MI) in order to implement the different interfaces. Usually I try to avoid MI, but I have't found a better solution.
I use factories to create models and worker objects, returning the base classes ModelABC
and WorkerABC
, respectively. The concrete classes Worker1
and Worker2
define required interfaces from the material models, namely Worker*::IModel
. Similarly, the MetaModel
class also defines an interface MetaModel::IModel
. Now, the elementary material models Material1
and Material2
inherit the interfaces which I want them to implement (sometimes it physically does not make sense for a model to implement a certain functions, e.g an anisotropic material should not use functions which require isotropy). Here is a demo of the code below.
#include <memory> #include <iostream> #include <string> #include <cassert> class ModelABC; class WorkerABC { public: virtual ~WorkerABC() = default; void evaluate_cell (/*args*/) const {this->evaluate_cell_impl(/*args*/);}; private: virtual void evaluate_cell_impl(/*args*/) const = 0; }; class Worker1: WorkerABC { public: class IModel { public: double get_stress (const double x) const {return this->get_stress_impl (x);} virtual ~IModel() = default; private: virtual double get_stress_impl (const double x) const = 0; }; Worker1 (ModelABC* m); virtual ~Worker1() = default; private: void evaluate_cell_impl (/*args*/) const override {/*uses get_stress*/} std::unique_ptr<IModel> model; }; class Worker2 : public WorkerABC { public: class IModel { public: double get_stress (const double x) const {return this->get_stress_impl (x);} double get_pressure (const double x) const {return this->get_pressure_impl (x);} virtual ~IModel() = default; private: virtual double get_stress_impl (const double x) const = 0; virtual double get_pressure_impl (const double x) const = 0; }; Worker2 (ModelABC* m); virtual ~Worker2() = default; private: void evaluate_cell_impl (/*args*/) const override {/*uses get_stress and get_pressure*/} std::unique_ptr<IModel> model; }; // Have to use this class. class ParameterHandler { public: ParameterHandler(const std::string &s): id(s) {} virtual ~ParameterHandler () = default; const std::string& get_id() const {return id;} // parse, declare, write parameters etc. private: std::string id; }; // Abstract Model base class needed for factories. class ModelABC: public ParameterHandler { public: // some common functions ModelABC(const std::string &s): ParameterHandler(s) {} virtual ~ModelABC() = default; }; class MetaModel: public Worker2::IModel, public ModelABC { public: class IModel { public: double get_energy (const double x) const {return get_energy_impl(x);} virtual ~IModel() = default; private: virtual double get_energy_impl(const double) const = 0; }; MetaModel (const std::string &s, ModelABC* m) : ModelABC(s), model(dynamic_cast<IModel*>(m)) { if (model == nullptr) throw; std::cout << "Meta model uses: " << m->get_id() << std::endl; }; virtual ~MetaModel() = default; private: double get_stress_impl (const double x) const override {return x*x;}; double get_pressure_impl (const double x) const override {return model->get_energy(x)/x;}; std::unique_ptr<IModel> model; }; // Can only be used by Worker class Model1: public ModelABC, public Worker1::IModel { public: Model1(const std::string &s): ModelABC(s) {} virtual ~Model1() = default; private: double get_stress_impl(const double x) const override {return x;} }; // Can be used by MetaModel, and Worker2 class Model2: public ModelABC, public Worker2::IModel, public MetaModel::IModel { public: Model2(const std::string& s): ModelABC(s) {} virtual ~Model2() = default; private: double get_stress_impl(const double x) const override {return 2*x;} double get_pressure_impl(const double x) const override {return x;} double get_energy_impl(const double x) const override {return 4*x*x;} }; // example.cc inline Worker1::Worker1 (ModelABC* m) : model(dynamic_cast<IModel*>(m)) { if(model == nullptr) throw; std::cout << "Created Worker with ModelABC: " << m->get_id() << std::endl; } inline Worker2::Worker2 (ModelABC* m) : model(dynamic_cast<IModel*>(m)) { if(model == nullptr) throw; std::cout << "Created Worker with ModelABC: " << m->get_id() << std::endl; }
What do you think about the use of MI in the above case? Is it a bad design choice?