Ein wichtiges Prinzip beim Softwaredesign ist die lose Kopplung von Komponenten. Je stärker Komponenten gekoppelt sind desto schwieriger wird es diese Komponenten weiter zu entwickeln und zu pflegen. Das Law of Demeter dient dazu eine zu enge Kopplung zu erkennen. Es wurde 1989 von Prof. Ian Holland beschrieben. Die Regeln sind einfach.
Eine Methode sollte nur folgende andere Methoden verwenden:
- Methoden der eigenen Klasse
- Methoden der übergebenen Parameter
- Methoden der mit eigenen Klasse assoziierten Klassen
- Methoden von Objekten die die Methode selbst erzeugt hat
Desgleichen gilt natürlich für Properties und public Fields.
Lange Lindwürmer wie customer.Orders[0].OrderItem[0].Product.Name sind “verboten” (sie sind zumindest ein Hinweis auf eine enge Kopplung).
In der Praxis sollte man das Einhalten der Regeln mit Augenmaß befolgen und sich nicht sklavisch daran halten. In diesem Sinne ist der Begriff “Law” (Gesetz) eher irreführend. Im folgenden möchte ich anhand eines Beispiels zeigen wie man das Law of Demeter anwenden kann um zu einem lose gekoppelten Design zu kommen. Als Beispiel soll folgender Use Case dienen:
- Ein Kunde soll über Statusänderungen seiner Aufträge per EMail informiert werden.
Eine erste Implementierung könnte wie folgt aussehen:
public class Order
{
// ...
public OrderStatus Status {
get { return m_Status; }
set {
if (m_Status == value) {
return;
}
SendOrderStatusChangedEMail(Customer.DefaultContact.EMail, m_Number, m_Status, value);
//*****************************\\
m_Status = value;
}
}
private static void SendOrderStatusChangedEMail(string email, string number, OrderStatus oldStatus, OrderStatus newStatus) {
m_MailService.Send(
email,
"Order status changed",
string.Format("The status of your order {0} changed from {1} to {2}.",
number, oldStatus, newStatus));
}
}
Die kritische Stelle ist durch Sternchen markiert. Der Zugriff auf Customer.DefaultContact.EMail verstößt gegen das LoD. Während der Zugriff auf das Property Customer noch in Ordnung ist, sind die nachfolgenden Zugriffe innerhalb des Customer Objekts nicht mehr durch die LoD Regeln gedeckt. Doch wo liegt das Problem?
Die Klasse Order verwendet auf diese Weise Details der Klasse Customer. So “weiß” die Klasse Order dass Customer einen DefaultContact hat und in diesem wiederum eine EMailadresse hinterlegt ist. Möchte man die Details der Customer oder Contact Klassen später ändern, muss die Klasse Order angepasst werden.
Ein weiteres Problem ergibt sich beim Testen der Klasse Order. Um zu prüfen ob eine korrekte EMail abgesendet wird muss das zu testende Order Objekt ein Customer Objekt enthalten, dieses muss wieder einen DefaultContact enthalten bei dem die EMailadresse gesetzt ist. Der Aufbau der Testobjekte wird so recht aufwendig:
[SetUp]
public void Setup() {
mocks = new MockRepository();
customer = mocks.DynamicMock<ICustomer>();
contact = mocks.DynamicMock<IContact>();
mailService = mocks.DynamicMock<IMailService>();
Order.MailService = mailService;
order = new Order(customer, "1234");
}
[Test]
public void Order_Status_change_is_mailed_to_Customer() {
using (mocks.Record()) {
Expect.Call(customer.DefaultContact).Return(contact);
Expect.Call(contact.EMail).Return("stefan@lieser-online.de");
Expect.Call(delegate {
mailService.Send(
"stefan@lieser-online.de",
"Order status changed",
"The status of your order 1234 changed from Open to Verified.");
});
}
using (mocks.Playback()) {
order.Status = Order.OrderStatus.Verified;
}
}
Eine alternative Implementierung vermeidet die enge Kopplung und vereinfacht damit auch die Tests:
public OrderStatus Status {
get { return m_Status; }
set {
if (m_Status == value) {
return;
}
Customer.SendEMail("Order status changed",
string.Format("The status of your order {0} changed from {1} to {2}.", Number, m_Status, value));
m_Status = value;
}
}
Statt sich durch das Customer Objekt durchzuhangeln gibt das Order Objekt dem Customer Objekt die Anweisung eine EMail zu versenden. Wie das Customer Objekt dies bewerkstelligt interessiert die Order Klasse nicht. Statt das Customer Objekt nach Details zu befragen wird ihm ein Auftrag erteilt (Tell don’t ask). Der Test sieht wie folgt aus:
[SetUp]
public void Setup() {
mocks = new MockRepository();
customer = mocks.DynamicMock<ICustomer>();
order = new Order(customer, "56");
}
[Test]
public void Customer_gets_informed_about_order_status_change() {
using (mocks.Record()) {
Expect.Call(delegate {
customer.SendEMail("Order status changed",
"The status of your order 56 changed from Open to Verified.");
});
}
using (mocks.Playback()) {
order.Status = Order.OrderStatus.Verified;
}
}
So führt die Berücksichtigung des Law of Demeter dazu dass das Single Responsibility Principle (SRP) befolgt wird. Die Order Klasse beschäftigt sich nun nicht mehr mit den Details des EMailversand.
Aber auch die strikte Einhaltung des Law of Demeter bringt Probleme mit sich: um nicht auf Methoden zuzugreifen die tief verschachtelt in anderen Objekten implementiert sind müsste man diese Methoden durch Wrapper in der Hierarchie nach oben ziehen:
// Verletzung des Law of Demeter
Color color = customer.Orders[i].OrderItems[j].Product.Color;
// Verwendung von Wrapper Methoden
Color color = customer.GetColorOf(i, j);
// Dazu müssen folgende Wrapper implementiert werden:
class Customer {
Color GetColorOf(int orderIndex, int orderItemIndex) {
return Orders[orderIndex].GetColorOf(orderItemIndex);
}
}
class Order {
Color GetColorOf(int orderItemIndex) {
return OrderItems[orderItemIndex].ProductColor;
}
}
class OrderItem {
Color ProductColor {
get { return Product.Color; }
}
}
Vorteil dieser Vorgehensweise: beim Testen mit Mock Objekten reicht es aus für das Customer Objekt einen Mock zu erstellen und das Ergebnis der GetColorOf Methode zu definieren. Bei der Verkettung der Aufrufe müsste für jede Ebene der Verkettung ein Mock erzeugt werden. Dies gilt allerdings nur für Mock Frameworks die nicht mit dem Profiler API arbeiten, TypeMock kann solche “chained calls” unmittelbar behandeln.
Nachteil der Vorgehensweise: in den Klassen werden zahlreiche Wrapper implementiert um weiter unten liegende Eigenschaften nach oben zu transportieren. Die Klassen werden dadurch eher unübersichtlich. Abhilfe können Adapter schaffen, doch dazu schreibe ich ein anderes mal…
Mein persönliches Fazit: das Law of Demeter gibt mir Hilfestellung beim Erkennen von Kopplung. Die Designentscheidung ist damit jedoch nicht automatisch getroffen.
Mehr zum Thema gibt es übrigens am 26. Februar beim nächsten Bonn-to-Code Treffen siehe http://www.bonn-to-code.net/1540.aspx.