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();
}
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;
}
}
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
}
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";
}
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();
}
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 ;-)");
}
}
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?