Dynamische Änderung von Zugriffsrechten bei Domänenobjekten

Alles, was nicht direkt mit Python-Problemen zu tun hat. Dies ist auch der perfekte Platz für Jobangebote.
Antworten
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Hallo,

ich stehe vor folgendem, simpel klingenden Problem: Es gibt eine Entität, bei der sich aufgrund von möglichen Operationen auf dem Objekt selber, dynamisch Schreib- und Lesezugriffe von Attributen ändern können.

Nehmen wir mal folgendes Beispiel: Eine Person hat einen Vor- und einen Nachnamen und ist lebendig oder tot. Zu Lebzeiten kann sich der Nachname ändern. Der Vorname bleibt immer gleich.

Mir fallen da verschiedene Wege ein, von denen mir keiner ideal erscheint... :(

(Die Beispiele sind in C# - einfach weil das die Zielsprache ist; konzeptionell sollte das aber imho auf alle Klassen basierten OO-Sprachen übertragbar sein)

Lösung 1 - veränderbares Objekt mit Logik beim Setter:

Code: Alles auswählen

public interface IPerson
{
    int Id { get; }
    string FirstName { get; }
    string LastName { get; set; }
    bool IsAlive { get; }
    void Die();
}
Als Interface modelliert sieht es eigentlich ganz einfach aus; nur die ``LastName``-Property ermöglicht den Schreibzugriff. Implementiert ergäbe sich dafür dann in etwa folgendes (nur die relevanten Stellen):

Code: Alles auswählen

public class Person : IPerson
{
    private bool isAlive;
    private string lastName;

    public string LastName
    {
        get
        {
            return lastName;
        }
        set
        {
            // Hier Logik in Abhängigkeit vom Zustand
            if (IsAlive)
            {
                lastName = value;
            }
            else
            {
                throw new InvalidOperationException(
                    "Der Nachname eines Toten kann nicht geändert werden!");
            }
        }
    }

    public void Die()
    {
        isAlive = false;
    }
}
Mir gefällt daran nicht, dass die Logik im Setter selber liegt! Zumal man das bei allen Properties implementieren muss (DRY), die dieser Logik folgen sollen... und die Logik an sich könnte sich ja auch einmal ändern (Open Closed).

Lösung 2 - Mutable und Immutable Varianten:
Idee wäre zwei Implementierungen zu schreiben, von denen nur die eine einen Setter anbietet. Dazu muss man das Interface ein wenig anpassen:

Code: Alles auswählen

public interface IPerson
{
    int Id { get; }
    string FirstName { get; }
    string LastName { get; } // kein Setter mehr!
    bool IsAlive { get; }
    IPerson Die(); // jetzt mit Rückgabewert
}
Die Implementierung von ``ImmutablePerson`` wäre offensichtlich analog zu Lösung 1 nur ohne Setter. Die ``Die``-Methode würde z.B. eine ``InvalidOperationException`` werfen, weil ein Toter nicht erneut sterben kann.

Bei der ``MutablePerson``-Implementierung, würde man einfach den Setter mit anbieten. Die ``Die``-Methode würde nun einfach ein neues Objekt vom Typ ``ImmutablePerson`` liefern.

Fachlich wäre damit das Problem zwar abgebildet, jedoch verliert man so ein *einheitliches* API! Man muss bei Repositories, die ``IPerson``-Objekte liefern irgend wann zwangsweise einen Downcast durchführen, um den Nachnamen zu ändern...

Code: Alles auswählen

var person = personRepository.GetById(...);
if(person.IsAlive)
{
    var lebendePerson = person as MutablePerson; // Downcast, um an Setter zu gelangen
    lebendePerson.LastName = "Anderer-Name";
}
Imho hässlich!

Lösung 3 - Dekoratoren:
Man könnte eine veränderbare Implementierung *mit Schreibzugriffen* als Basis nutzen und bei der ``Die``-Methode einen Dekorator zurückliefern, welcher die Schreibzugriffe unterbindet.

Dazu muss das Interface im Vergleich zu Lösung 2 leicht verändert werden:

Code: Alles auswählen

public interface IPerson
{
    int Id { get; }
    string FirstName { get; }
    string LastName { get; set; } // Wieder mit Setter, also "vollständig"
    bool IsAlive { get; }
    IPerson Die();
}
Als Implementierung könnte das ganze dann so aussehen:

Code: Alles auswählen

public class Person : IPerson
{
    private readonly int id;
    private readonly string firstName;
    private string lastName;
    private bool isAlive;

    public int Id
    {
        get { return id; }
    }

    public string FirstName
    {
        get { return firstName; }
    }

    public string LastName
    {
        get
        {
            return lastName;
        }
        set
        {
            lastName = value;
        }
    }

    public bool IsAlive
    {
        get { return isAlive; }
    }

    public Person(int id, string firstName, string lastName)
    {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        isAlive = true;
    }

    public override string ToString()
    {
        return String.Format("{0} {1}", FirstName, LastName);
    }

    public IPerson Die()
    {
        return new DeadPerson(this);
    }

}

public class DeadPerson : IPerson
{
    private readonly IPerson decoratedPerson;

    public int Id
    {
        get { return decoratedPerson.Id; }
    }

    public string FirstName
    {
        get { return decoratedPerson.FirstName; }
    }

    public string LastName
    {
        get
        {
            return decoratedPerson.LastName;
        }
        set
        {
            throw new InvalidOperationException(
                "Der Nachname eines Toten kann nicht geändert werden!");
        }
    }

    public bool IsAlive
    {
        get { return false; }
    }

    public DeadPerson(IPerson decoratedPerson)
    {
        if (decoratedPerson == null)
        {
            throw new ArgumentNullException("decoratedPerson");
        }
        this.decoratedPerson = decoratedPerson;
    }

    public override string ToString()
    {
        return String.Format("{0} (tot)", decoratedPerson.ToString());
    }

    public IPerson Die()
    {
        throw new InvalidOperationException("Was tot ist, kann niemals sterben ;-)");
    }
}
Vorteil wäre imho, dass man die Logik nur *einmal* in der fachlichen Methode ``Die`` stehen hat; wenn gewünscht könnte man dem Objekt sogar per Strategy-Pattern verschiedene ``DieBehavior``s zuweisen usw.

Bisher ist das mein Favorit!

Ein Nachteil bei Lösung 2 und 3 ist, dass das alte Objekt weiter Bestand hat... man könnte also eine Person "clonen" oder letztlich auch mehrfach sterben lassen... bei Lösung 1 hat man diesen Nachteil natürlich nicht.

Wie seht ihr das so? Wie würdet ihr vorgehen?
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Um das mal etwas abstrakter zu formulieren: Dir geht es darum, das Verhalten eines Setters unabhängig vom internen Zustand zu machen. Vielmehr soll das Verhalten rein über die jeweilige Implementierung bestimmt werden. Und beim Aufruf gewisser Operationen möchtest du eine andere Implementierung verwenden, welche die Basisdaten der Implementierung, auf der diese Operation getätigt wurde, übernimmt. Habe ich das richtig verstanden?

Handelt es sich denn tatsächlich um personenbezogene Daten?
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

snafu hat geschrieben:Um das mal etwas abstrakter zu formulieren: Dir geht es darum, das Verhalten eines Setters unabhängig vom internen Zustand zu machen. Vielmehr soll das Verhalten rein über die jeweilige Implementierung bestimmt werden. Habe ich das richtig verstanden?
Jein! Es geht nicht nur um einen Setter, sondern um mehrere. Das Verhalten wird aktuell vom inneren Zustand bestimmt; es ist aber abzusehen, dass das nicht der ausschließliche Fall sein wird, sprich, es kann auch von Kontextdaten abhängig sein. Insbesondere wird es verschiedene Fällt geben, die unterschiedliche Attribute von Änderungen ausschließen.
snafu hat geschrieben: Und beim Aufruf gewisser Operationen möchtest du eine andere Implementierung verwenden, welche die Basisdaten der Implementierung, auf der diese Operation getätigt wurde, übernimmt.
Nein! Mir ist es egal, *wie* ich das erreiche, solange die Lösung ein gutes API bietet ;-)
snafu hat geschrieben: Handelt es sich denn tatsächlich um personenbezogene Daten?
Nein. Die Domäne ist aber imho nicht relevant; Veränderlichkeit von Objekten in Abhängigkeit von Bedingungen ist doch ein ganz allgemeines Problem, welches man sicherlich in allen möglichen Domänen antreffen kann... daher mein imho sehr einfaches, leicht verständliches Beispiel :-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@Hyperion: Bestünde denn die Möglichkeit die Attribute weiter zu abstrahieren und dort den Mechanismus einzubauen? Also das ein Attribut selbst weiss das es (un)veränderbar ist und das umschaltbar ist oder das man dem Attribut eine Rechte-Instanz mitgeben kann wo es nachfragen kann!? In Python würde ich da ja über einen Decorator nachdenken oder das über das Descriptorprotokoll angehen.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

BlackJack hat geschrieben:@Hyperion: Bestünde denn die Möglichkeit die Attribute weiter zu abstrahieren und dort den Mechanismus einzubauen?
Hm... daran hatte ich gar nicht gedacht! Stimmt, das ginge natürlich auch... damit hätte man einen eigenen Datentypen für solche Attribute und könnte bei diesem die Zugriffe gezielt steuern... Danke für die Anregung! :-)

Kleiner Haken wäre dabei, dass man sich bei jedem Attribut *bewusst* für ein solches "custom"-Attribut entscheiden muss... bleibt man bei simplen Properties, so kann man den Typen leichter erweitern bzw. ein Zugriffsverhalten auch nachträglich einfach für Attribute erweitern, die bis dato keiner Einschränkung unterlegen waren. Bei einem eigenen Typen hätte das eine API-Änderung zur Folge.

Trotzdem werde ich damit wohl mal experimentieren!
BlackJack hat geschrieben: In Python würde ich da ja über einen Decorator nachdenken oder das über das Descriptorprotokoll angehen.
Leider muss es in C# sein ;-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

Mit State Pattern in Python 3, weil ich seit Jahren kein C# mehr programmiert habe:

Code: Alles auswählen

class AlreadyDeadException(Exception):
    pass


class PersonState:

    def __init__(self, owner, id, first_name, last_name):
        self.owner = owner
        self.id = id
        self._first_name = first_name
        self._last_name = last_name

    @property
    def first_name(self):
        return self._first_name

    @property
    def last_name(self):
        return self._last_name


class Living(PersonState):

    is_alive = True

    @PersonState.first_name.setter
    def first_name(self, first_name):
        self._first_name = first_name

    @PersonState.last_name.setter
    def last_name(self, last_name):
        self._last_name = last_name

    def die(self):
        self.owner._state = Dead(
            self.owner, self.id, self.first_name, self.last_name
        )


class Dead(PersonState):

    is_alive = False

    @PersonState.first_name.setter
    def first_name(self, first_name):
        raise AlreadyDeadException

    @PersonState.last_name.setter
    def last_name(self, last_name):
        raise AlreadyDeadException

    def die(self):
        raise AlreadyDeadException


class Person:

    def __init__(self, id, first_name, last_name):
        self._state = Living(self, id, first_name, last_name)

    @property
    def id(self):
        return self._state.id

    @property
    def first_name(self):
        return self._state.first_name

    @first_name.setter
    def first_name(self, first_name):
        self._state.first_name = first_name

    @property
    def last_name(self):
        return self._state.last_name

    @last_name.setter
    def last_name(self, last_name):
        self._state.last_name = last_name

    def is_alive(self):
        return self._state.is_alive

    def die(self):
        self._state.die()


def main():
    p = Person(123, 'joe', 'shmoe')
    assert p.id == 123 and p.first_name == 'joe' and p.last_name == 'shmoe'
    p.first_name = 'john'
    p.last_name = 'doe'
    assert p.id == 123 and p.first_name == 'john' and p.last_name == 'doe'
    assert p.is_alive()
    p.die()
    assert not p.is_alive()
    try:
        p.first_name = 'jane'
    except AlreadyDeadException:
        pass
    else:
        raise Exception
    assert p.id == 123 and p.first_name == 'john' and p.last_name == 'doe'
    try:
        p.die()
    except AlreadyDeadException:
        pass
    else:
        raise Exception
    assert not p.is_alive()


if __name__ == '__main__':
    main()
In specifications, Murphy's Law supersedes Ohm's.
Antworten