Skip to content

Rethinking properties.association handling of subset changes #3720

@pbrown12303

Description

@pbrown12303

The current implementation of the properties.association handling of subset changes appears to be inconsistent with the logic of the UML specification. The current Gaphor implementation of the editing of relationships between Property and Association seems to support this idea, but since the current UML implementation does not fully express the subset relations in the UML specification, the problem has not yet manifested itself.

In bringing the Gaphor UML model into alignment with the UML specification, the problem has surfaced.

Discussion

Here is the current Gaphor model of the associations linking UML.Property and UML.Association:
image

And here is the UML specification model of these associations:
image

Take a look at the UML specification of the UML.Association properties related to UML.Property. Shown in set-theoretic terms, these properties are related to each other as follows:
image

After implementing the full UML specification model in Gaphor, some of the built-in tests failed. One, in particular, illustrates the problem. We have to dig through three layers of code for the issue to become apparent.

At the top level, we have the test_copy_paste_items_with_connections, the first few lines of which are as follows:

    gen_cls_item, spc_cls_item, assoc_item = two_classes_and_an_association(
        diagram, element_factory
    )

The invocation of two_classes_and_an_association executes the following code:

def two_classes_and_an_association(diagram, element_factory):
    gen_cls = element_factory.create(UML.Class)
    spc_cls = element_factory.create(UML.Class)
    gen_cls_item = diagram.create(ClassItem, subject=gen_cls)
    spc_cls_item = diagram.create(ClassItem, subject=spc_cls)
    assoc_item = diagram.create(AssociationItem)
    gen_cls.name = "Gen"
    spc_cls.name = "Spc"

    connect(assoc_item, assoc_item.handles()[0], spc_cls_item)
    connect(assoc_item, assoc_item.handles()[1], gen_cls_item)
    UML.recipes.set_navigability(
        assoc_item.subject, assoc_item.subject.memberEnd[0], True
    )

    assert (
        assoc_item.subject.memberEnd[0]
        in assoc_item.subject.memberEnd[1].type.ownedAttribute
    )
    # assert gen_cls_item.ownedAttribute

    return gen_cls_item, spc_cls_item, assoc_item

The cause of the problem occurs inside the invocation of UML.recipes.set_navigability, the existence of the problem surfaces in the assertion after this invocation: it fails because assoc_item.subject.memberEnd[1] no longer exists at this point (array index out of bounds).

Stepping into UML.recipes.set_navigability we have (omitting the lengthy comment at the beginning):

    assert end.opposite
    owner = end.opposite.type
    # remove "navigable" and "unspecified" navigation indicators first
    if isinstance(owner, TYPES_WITH_OWNED_ATTRIBUTE) and end in owner.ownedAttribute:
        owner.ownedAttribute.remove(end)
    if end in assoc.ownedEnd:
        assoc.ownedEnd.remove(end)
    if end in assoc.navigableOwnedEnd:
        assoc.navigableOwnedEnd.remove(end)

    assert end not in assoc.ownedEnd
    assert end not in assoc.navigableOwnedEnd

    if nav is True:
        if isinstance(owner, TYPES_WITH_OWNED_ATTRIBUTE):
            owner.ownedAttribute = end
        else:
            assoc.navigableOwnedEnd = end
    elif nav is None:
        assoc.ownedEnd = end
    # elif nav is False, non-navigable

The problem occurs when assoc.ownedEnd.remove(end) is invoked. After this invocation, the end is no longer in the association's memberEnd collection. Recall the assertion in two_classes_and_an_association mentioned above that this end is expected to still be in that collection. Why is it no longer there?

Root Cause

In the UML specification and the newly implemented Gaphor representation of it, ownedEnd is declared to be a subset of memberEnd. The currently implemented Gaphor logic for handling subset changes is documented in properties.association:

The logic of maintaining the set contets is complex. While this logic is constrained by set
theory, there are cases that arise in which set theory alone would allow multiple outcomes.
Making the outcomes deterministic requires making some policy choices. In the cases below,
those marked with ***POLICY*** are not solely determined by the logic of set theory.

There is a significant design assumption that drives the policy decisions: there is only one superset-subset path
by which an element may become a member of the superset. In other words, if the superset has two or more subsets,
a given element can be a member of at most one subset. This is really an assumption about the metamodel architecture.
The implication is that if an element is removed from a subset, it must be removed from the superset. Furthermore,
if an element is present in both the superset and the subset, and the element is removed from the superset, it must
also be removed from the subset.

Subset changes, both sets have a multiplicity of 1:
    Both sets {}: subset {a1} => superset {a1}
    Superset {a1}, subset {}: subset {a2} => superset {a2} ***POLICY***
    Superset {a1}, subset {a1}: subset {} => superset {} ***POLICY***
    Superset {a1}, subset {a1}: subset {a2} => superset {a2} ***POLICY***

Superset changes, both sets have a multiplicity of 1:
    Both sets {}: superset {a1} => subset {}
    Subset {a1}, superset {a1}: superset {}, => subset {}
    Superset {a1}, subset {a1}: superset {a2} => subset {}

Subset changes, superset has multiplicity *, subset has multiplicity 1 or *:
    Both sets {}: subset {a1} => superset {a1}
    Superset {a1}, subset {}, subset {a2} => superset {a1, a2}
    Superset {a1}, subset {a1}: subset {} => superset {} ***POLICY***
    Superset {a1}, subset {a1}: subset {a2}, => superset {a2} ***POLICY***

Superset changes, superset has multiplicity *, subset has multiplicity 1 or *:
    Both sets {}: superset {a1} => subset {}
    Superset {a1}, subset {a1}: superset {} => subset {}
    Superset {a1}, subset {a1}, superset {a2} => subset {}

Note the POLICY logic regarding changes in which the removal of an element from the subset automatically removes it from the superset. I believe this policy to be in error. The specific example cited above indicates that the current implementation supports this perspective.

Discussion of Remedies

I think to resolve this we have to acknowledge that there are two types of supersets, derived sets (derived unions) and ordinary sets, and that different rules are required for each.

For derived sets (derived unions), I believe the current logic is appropriate: changes in the membership in the subset should be directly reflected as a corresponding changes for the superset.

However, for ordinary sets (writable properties), we have to recognize that these sets can be directly changed - independent of the membership of any subsets. Thus removal of a member from the subset should not trigger the removal of that element from the superset. This behavior is the root cause of the test failure described above. Conversely, it would seem that the removal of the element from the superset ought to also remove it from the subset (if it is present in the subset).

I have not yet thought through the full set of rules (I'm working on that), but I wanted to share these thoughts and get some feedback in parallel with working through both the rule changes and the implications for the implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions