mardi 27 avril 2021

How to avoid downcasting in composite pattern

I use a composite pattern to represent a mechanical network, which describes the mechanical behavior of a material, see e.g. the Maxwell-Wiechert model, a spring in parallel with n Maxwell Elements. A maxwell element iteslf is a spring in series with a damper. The mechanical network is composed dynamically at runtime. To this end, I have derived Spring, MaxwellElement, and ParallelConnection from a common base class Component:

struct Component {
    virtual void add_component (Component* ) { }
    virtual Tensor<2> stress (const Tensor<2> &strain) const = 0;
    virtual ~Component () = default;
};

struct Spring : Component {    
    Tensor<2> stress (const Tensor<2> &strain) const override {return {};};
};

struct SpecialSpring : Spring {

    Tensor<1> principal_stresses (const Tensor<1> &strain_eigenvalues) const {return {};};
};

struct ParallelConnection : Component {

    void add_component (Component* c) override {components.emplace_back(c);}
    Tensor<2> stress (const Tensor<2> &strain) const override {
        Tensor<2> s; 
        for (const auto& c : components)
            s += c->stress(strain);
        return s;
    };
    std::vector<std::unique_ptr<Component>> components;
};

struct MaxwellElement : Component {

    Tensor<2> stress (const Tensor<2> &strain) const override {
        auto special_spring = dynamic_cast<SpecialSpring*>(spring.get());
        if (special_spring) {
            const auto ps = special_spring->principal_stresses (eigenvalues(strain));
            // Some cheap calculations using ps.
            return {}; // <- meaningful result
        } else {      
            const auto s = spring->stress (strain);
            // Some expensive calculations using s. 
            return {}; // <- meaningful result
        }   
    };    

    virtual void add_component (Component* c) {
        if (!spring)
            spring.reset(dynamic_cast<Spring*>(c));
        assert (spring);
    }

    std::unique_ptr<Spring> spring;
};

The goal is to compute the stress response of a material, represented by the network, for a given strain tensor (both are symmetric). Under certain assumptions, the computation within MaxwellElement::stress(const Tensor<2>&) can be significantly sped up by using the principal stresses, i.e. the eigenvalues of the stress tensor (and the corresponding eigenvectors, which has been neglected for simplicity, see this paper if you are interested in the theory). Lets assume that SpecialSpring fulfills these assumptions, but in general, Spring doesn't.

live demo of current implementation.

What really bothers me with my current implementation, is that I have to manually downcast to SpecialSpring in order to access SpecialSpring::principal_stresses(const Tensor<1> &). Of course, I could add a virtual function Component::get_type() to check whether I am dealing with a SpecialSpring, but I haven't done this here to keep the code short.

Now, I am thinking about using a Visitor like this

struct SpringVisitor {

void visit (const Spring* s) {
    result = s->stress (strain);
}
void visit (const SpecialSpring* s) {
    result = s->principal_stresses (eigenvalues(strain));
}

Tensor<2> strain;
std::variant<Tensor<1>, Tensor<2>> result;
}

Based on std::variant::index(), I can choose the correct algorithm. However, I still fells like a bad design.

Is there any better way to achieve what I have described above?

Is my current implementation the problem itself and I am dealing with sort of an XY problem?

Aucun commentaire:

Enregistrer un commentaire