Моделирование абстрактных композиций с типобезопасностью - PullRequest
2 голосов
/ 22 февраля 2012

У меня есть структурная проблема, с которой я мог бы использовать вашу помощь. Сначала я объясню абстрактную проблему, а затем приведу пример, иллюстрирующий проблему.

Рассмотрим абстрактный класс A , содержащий несколько экземпляров абстрактного класса B , также предположим, что следующие методы в A : void a .foo1 (B val) и B a.foo2 () . Проблема возникает, когда мы наследуем классы ( A ' наследует A и B' наследует B ) и требуем, чтобы отношение A должно B должно совпадать с A ' должно B' . То есть в A ': void a.foo1 (B' val) и B 'a.foo2 () . Второй метод будет работать, но не первый (если мы не делаем небезопасное приведение типов). Другими словами, в A ': a.foo1 (B val) должно быть недопустимым, если параметр не является экземпляром B' . Я попытался смоделировать эти отношения с помощью дженериков / шаблонов с небольшим успехом.

Эта проблема возникает при создании графовой структуры. Здесь у нас есть классы Graph и GraphVertex . (В моей реальной реализации я перегружаю оператор удаления GraphVertex.) В C ++:

template<class T> class Graph; // Forward reference.

template<class T> class GraphVertex
{
    public:
        GraphVertex(Graph<T> &graph) : m_graph(graph){}
        virtual ~GraphVertex() {}

        ... // Abstract methods, using T parameter.

        void remove()
        { m_graph.removeVertex(this); }
    protected:
        Graph<T> &m_graph;
}

template<class T> class Graph
{
    public:
        virtual ~Graph(){}

        ...

        virtual GraphVertex<T> *add(T val) = 0;
        virtual void removeVertex(GraphVertex<T> *vertex) = 0; // <--- !!!
}

Проблема здесь заключается в методе removeVertex . Допустим, мы реализовали классы с AdjacencyMatrixGraph и AMGVertex . При удалении вершины в AdjacencyMatrixGraph нам нужно больше данных, чем предусмотрено в абстрактном базовом классе GraphVertex . Мы знаем, что тип параметра должен быть AMGVertex , но отправка другого типа в качестве параметра не приведет к ошибке (время компиляции).

Чтобы решить эту проблему, я попытался добавить новый параметр шаблона, указав реализованный тип. То есть:

template<class T, class G> class GraphVertex
{
    public:
        GraphVertex(G &graph) : m_graph(graph) {}
        ~GraphVertex() {}

        ...

        void remove() { m_graph.removeVertex(this); }

    protected:
        G &m_graph;
}

template<class T, class V> class Graph
{
    public:
        virtual ~Graph() {}

        ...

        virtual V *add(T val) = 0;
        virtual void removeVertex(V *vertex) = 0;
}

template<class T> AdjacencyMatrixGraph; // Forward declaration.

template<class T> AMGVertex : public GraphVertex<T, AdjacencyMatrixGraph<T>>
{ ... }

template<class T> AdjacencyMatrixGraph : public Graph<T, AMGVertex<T>>
{ ... }

Однако это не будет работать. Невозможно использовать базовый класс График из-за циклической ссылки на базовые классы.

Graph<int> *p = new AdjacencyMatrixGraph<int>(); // Won't work.

Пример выше имеет те же проблемы в Java с обобщениями.

Итак, есть ли способ смоделировать отношение безопасным для типов образом? Или я застрял с указателями поворота вокруг?

Спасибо за чтение!

EDIT:

Пример использования приведенного выше может выглядеть следующим образом:

Graph<int> *someGraph = getSomeGraph();
GraphVertex<int> *newVertex = someGraph->add(3);
...
newVertex->remove();

Ответы [ 3 ]

0 голосов
/ 22 февраля 2012

"При удалении вершины в AdjacencyMatrixGraph нам нужно больше данных, чем предусмотрено в абстрактном базовом классе GraphVertex".Я думаю, вы должны написать Interface , который предлагает необходимые данные.

Если у вас есть два подкласса с одинаковым поведением в одной точке, и базовый класс не может добавить это поведение вэто класс, потому что он не подходит для каждого подкласса, вы можете охарактеризовать подклассы с тем же поведением с дополнительным интерфейсом.

0 голосов
/ 23 февраля 2012

После некоторых экспериментов я пришел к выводу, что общий дизайн не может быть изменен, чтобы быть на 100% безопасным для типов. Однако в некоторых ситуациях вы можете убедиться, что обиженный метод вызывается только с правильным подтипом.

В примере с графиком нам нужно обеспечить, чтобы подкласс GraphVertex (в данном случае AMGVertex ) мог получить доступ только к связанному подклассу График (то есть для AMGVertex , класс AdjacencyMatrixGraph ). Следовательно, необходимо убедиться, что в метод removeVertex (GraphVertex * v) вставлен правильный тип. Для этого можно использовать закрытую видимость для методов, к которым обращается абстрактный класс GraphVertex . Также видимость конструкторов всех подтипов GraphVertex должна быть закрытой. Конечно, конструктор AMGVertex должен быть видимым из AdjacencyMatrixGraph , а removeVertex должен быть видимым из AMGVertex . В C ++ это можно сделать с друзьями .

template<class T> class GraphVertex
{
    public:
        virtual ~GraphVertex() {}
        void remove() { m_graph.removeVertex(this); }

    protected:
        GraphVertex(Graph<T> &graph) : m_graph(graph) {}
        Graph<T> &m_graph;
}

template<class T> class Graph
{
    friend class GraphVertex<T>;
    public:
        virtual GraphVertex<T> add(T value) = 0;

    protected: // Or private?
        virtual void removeVertex(GraphVertex<T> *v) = 0;
}

template<class T> class AMGVertex : public GraphVertex<T>
{
    friend class AdjacencyMatrixGraph<T>;

    protected: // Or private?
        AMGVertex(AdjacencyMatrixGraph<T> &graph)
        : GraphVertex<T>(graph) {}
}

template<class T> class AdjacencyMatrixGraph : public Graph<T>
{
    public:
        AMGVertex *add(T value) { ... } // Calls AMGVertex's constructor.

    protected: // Or private?
        void removeVertex(GraphVertex<T> *v) // Only visible from this class and through base class.
        { ... }
}

Таким образом, AMGVertex можно создать только из AdjacencyMatrixGraph , и, следовательно, вызов AMGVertex :: remove () всегда будет вызывать AdjacencyMatrixGraph: : removeVertex (GraphVertex * v) с правильным подклассом в качестве параметра.

Однако все еще возможно обойти это, просто создав новый тип вершины с AdjacencyMatrixGraph как m_graph . Это потому, что дружба от вершины до графа находится в абстрактном базовом классе.

Так что 100% безопасное для типов решение (по крайней мере из того, что я собрал) невозможно.

В Java я считаю, что аналогичные результаты могут быть достигнуты с помощью вложенных классов (чтобы преодолеть ограничения видимости).

Спасибо за ответы! Если у кого-то есть лучшее решение, дайте нам знать.

0 голосов
/ 22 февраля 2012

Хороший пример (+1).Проблема заключается в дизайне OO, поэтому безопасное решение не представляется возможным:

Поскольку A' также имеет тип A, он должен следовать контракту описано в A.Следовательно, foo1 из A' должен принимать произвольный B в качестве параметра, а не только B'.

Тип возврата foo2 в A' должен иметь тип B.Поскольку B' имеет тип B, ковариант тип возврата B' разрешен для foo2 (начиная с n J2SE 5.0.).

Обновление:

Чтобы решить эту дилемму, я бы сделал редизайн, поскольку ваш A' на самом деле не A.Так что либо

  • A' не наследуется от A, либо
  • A вообще не содержит foo1, либо
  • A должен содержать void a.foo1(B' val).

Если редизайн слишком сложен для вас, вам придется отказаться от безопасности типов и, например, выбросить исключение недопустимого аргумента, если foo1в A' вызывается с B, который не является B'.Это очень похоже на то, как java.util.Collection обрабатывает необязательные операции.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...